diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91bffef70..0face42d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,23 +60,6 @@ repos: args: ['-f', 'json', '--severity-level', 'high', '--confidence-level', 'high'] exclude: ^surfsense_backend/(tests/|test_.*\.py|.*test.*\.py|alembic/) - # TypeScript compilation check - - repo: local - hooks: - - id: typescript-check-web - name: TypeScript Check (Web) - entry: bash -c 'cd surfsense_web && (command -v pnpm >/dev/null 2>&1 && pnpm build --dry-run || npx next build --dry-run)' - language: system - files: ^surfsense_web/.*\.(ts|tsx)$ - pass_filenames: false - - - id: typescript-check-extension - name: TypeScript Check (Browser Extension) - entry: bash -c 'cd surfsense_browser_extension && npx tsc --noEmit' - language: system - files: ^surfsense_browser_extension/.*\.(ts|tsx)$ - pass_filenames: false - # Commit message linting - repo: https://github.com/commitizen-tools/commitizen rev: v4.8.3 diff --git a/surfsense_web/.vscode/launch.json b/surfsense_web/.vscode/launch.json index 0f7a24de3..13a8a9b00 100644 --- a/surfsense_web/.vscode/launch.json +++ b/surfsense_web/.vscode/launch.json @@ -1,31 +1,31 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Next.js: debug client-side", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}" - }, - { - "name": "Next.js: debug server-side", - "type": "node-terminal", - "request": "launch", - "command": "pnpm run debug:server", - "skipFiles": ["/**"] - }, - { - "name": "Next.js: debug full stack", - "type": "node-terminal", - "request": "launch", - "command": "pnpm run debug", - "serverReadyAction": { - "pattern": "- Local:.+(https?://.+)", - "uriFormat": "%s", - "action": "debugWithChrome" - }, - "skipFiles": ["/**"] - } - ] -} \ No newline at end of file + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "pnpm run debug:server", + "skipFiles": ["/**"] + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "pnpm run debug", + "serverReadyAction": { + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + }, + "skipFiles": ["/**"] + } + ] +} diff --git a/surfsense_web/app/api/search/route.ts b/surfsense_web/app/api/search/route.ts index 01401b7f5..d86bfc5ba 100644 --- a/surfsense_web/app/api/search/route.ts +++ b/surfsense_web/app/api/search/route.ts @@ -1,4 +1,4 @@ -import { source } from '@/lib/source'; -import { createFromSource } from 'fumadocs-core/search/server'; - -export const { GET } = createFromSource(source); \ No newline at end of file +import { source } from "@/lib/source"; +import { createFromSource } from "fumadocs-core/search/server"; + +export const { GET } = createFromSource(source); diff --git a/surfsense_web/app/auth/callback/page.tsx b/surfsense_web/app/auth/callback/page.tsx index d4ec41442..da868c316 100644 --- a/surfsense_web/app/auth/callback/page.tsx +++ b/surfsense_web/app/auth/callback/page.tsx @@ -1,19 +1,23 @@ -import { Suspense } from 'react'; -import TokenHandler from '@/components/TokenHandler'; +import { Suspense } from "react"; +import TokenHandler from "@/components/TokenHandler"; export default function AuthCallbackPage() { - return ( -
-

Authentication Callback

- -
-
}> - - - - ); -} \ No newline at end of file + return ( +
+

Authentication Callback

+ +
+
+ } + > + + + + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx index 45f6d4610..e33a7a2b3 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx @@ -1,874 +1,914 @@ -'use client'; +"use client"; -import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useSearchParams } from 'next/navigation'; -import { MessageCircleMore, Search, Calendar, Tag, Trash2, ExternalLink, MoreHorizontal, Radio, CheckCircle, Circle, Podcast } from 'lucide-react'; -import { format } from 'date-fns'; +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useSearchParams } from "next/navigation"; +import { + MessageCircleMore, + Search, + Calendar, + Tag, + Trash2, + ExternalLink, + MoreHorizontal, + Radio, + CheckCircle, + Circle, + Podcast, +} from "lucide-react"; +import { format } from "date-fns"; // UI Components -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator -} from '@/components/ui/dropdown-menu'; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from '@/components/ui/pagination'; + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, } from "@/components/ui/dialog"; import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; interface Chat { - created_at: string; - id: number; - type: string; - title: string; - messages: ChatMessage[]; - search_space_id: number; + created_at: string; + id: number; + type: string; + title: string; + messages: ChatMessage[]; + search_space_id: number; } interface ChatMessage { - id: string; - createdAt: string; - role: string; - content: string; - parts?: any; + id: string; + createdAt: string; + role: string; + content: string; + parts?: any; } interface ChatsPageClientProps { - searchSpaceId: string; + searchSpaceId: string; } const pageVariants = { - 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 = { - 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 [chats, setChats] = useState([]); - const [filteredChats, setFilteredChats] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [selectedType, setSelectedType] = useState('all'); - const [sortOrder, setSortOrder] = useState('newest'); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [chatToDelete, setChatToDelete] = useState<{ id: number, title: string } | null>(null); - const [isDeleting, setIsDeleting] = useState(false); - - // New state for podcast generation - const [selectedChats, setSelectedChats] = useState([]); - const [selectionMode, setSelectionMode] = useState(false); - const [podcastDialogOpen, setPodcastDialogOpen] = useState(false); - const [podcastTitle, setPodcastTitle] = useState(""); - const [isGeneratingPodcast, setIsGeneratingPodcast] = useState(false); - - // New state for individual podcast generation - const [currentChatIndex, setCurrentChatIndex] = useState(0); - const [podcastTitles, setPodcastTitles] = useState<{[key: number]: string}>({}); - const [processingChat, setProcessingChat] = useState(null); - - 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 (!isNaN(pageNumber) && pageNumber > 0) { - setCurrentPage(pageNumber); - } - } - }, [searchParams]); + const [chats, setChats] = useState([]); + const [filteredChats, setFilteredChats] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [selectedType, setSelectedType] = useState("all"); + const [sortOrder, setSortOrder] = useState("newest"); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [chatToDelete, setChatToDelete] = useState<{ id: number; title: string } | null>(null); + const [isDeleting, setIsDeleting] = useState(false); - // Fetch chats from API - useEffect(() => { - const fetchChats = async () => { - try { - setIsLoading(true); - - // Get token from localStorage - const token = localStorage.getItem('surfsense_bearer_token'); - - if (!token) { - setError('Authentication token not found. Please log in again.'); - setIsLoading(false); - return; - } + // New state for podcast generation + const [selectedChats, setSelectedChats] = useState([]); + const [selectionMode, setSelectionMode] = useState(false); + const [podcastDialogOpen, setPodcastDialogOpen] = useState(false); + const [podcastTitle, setPodcastTitle] = useState(""); + const [isGeneratingPodcast, setIsGeneratingPodcast] = useState(false); - // Fetch all chats for this search space - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/?search_space_id=${searchSpaceId}`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - cache: 'no-store', - } - ); + // New state for individual podcast generation + const [currentChatIndex, setCurrentChatIndex] = useState(0); + const [podcastTitles, setPodcastTitles] = useState<{ [key: number]: string }>({}); + const [processingChat, setProcessingChat] = useState(null); - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(`Failed to fetch chats: ${response.status} ${errorData?.error || ''}`); - } + const chatsPerPage = 9; + const searchParams = useSearchParams(); - const data: Chat[] = await response.json(); - setChats(data); - setFilteredChats(data); - setError(null); - } catch (error) { - console.error('Error fetching chats:', error); - setError(error instanceof Error ? error.message : 'Unknown error occurred'); - setChats([]); - setFilteredChats([]); - } finally { - setIsLoading(false); - } - }; + // Get initial page from URL params if it exists + useEffect(() => { + const pageParam = searchParams.get("page"); + if (pageParam) { + const pageNumber = parseInt(pageParam, 10); + if (!isNaN(pageNumber) && pageNumber > 0) { + setCurrentPage(pageNumber); + } + } + }, [searchParams]); - fetchChats(); - }, [searchSpaceId]); + // Fetch chats from API + useEffect(() => { + const fetchChats = async () => { + try { + setIsLoading(true); - // 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 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(); - - return sortOrder === 'newest' ? dateB - dateA : dateA - dateB; - }); - - 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]); + // Get token from localStorage + const token = localStorage.getItem("surfsense_bearer_token"); - // Function to handle chat deletion - const handleDeleteChat = async () => { - if (!chatToDelete) return; - - setIsDeleting(true); - try { - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - setIsDeleting(false); - return; - } - - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatToDelete.id}`, { - method: 'DELETE', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`Failed to delete chat: ${response.statusText}`); - } - - // Close dialog and refresh chats - setDeleteDialogOpen(false); - setChatToDelete(null); - - // Update local state by removing the deleted chat - setChats(prevChats => prevChats.filter(chat => chat.id !== chatToDelete.id)); - } catch (error) { - console.error('Error deleting chat:', error); - } finally { - setIsDeleting(false); - } - }; + if (!token) { + setError("Authentication token not found. Please log in again."); + setIsLoading(false); + return; + } - // Calculate pagination - const indexOfLastChat = currentPage * chatsPerPage; - const indexOfFirstChat = indexOfLastChat - chatsPerPage; - const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); - - // Get unique chat types for filter dropdown - const chatTypes = ['all', ...Array.from(new Set(chats.map(chat => chat.type)))]; + // Fetch all chats for this search space + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/?search_space_id=${searchSpaceId}`, + { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + cache: "no-store", + } + ); - // Generate individual podcasts from selected chats - const handleGeneratePodcast = async () => { - if (selectedChats.length === 0) { - toast.error("Please select at least one chat"); - return; - } - - const currentChatId = selectedChats[currentChatIndex]; - const currentTitle = podcastTitles[currentChatId] || podcastTitle; - - if (!currentTitle.trim()) { - toast.error("Please enter a podcast title"); - return; - } - - setIsGeneratingPodcast(true); - try { - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - toast.error("Authentication error. Please log in again."); - setIsGeneratingPodcast(false); - return; - } - - // Create payload for single chat - const payload = { - type: "CHAT", - ids: [currentChatId], // Single chat ID - search_space_id: parseInt(searchSpaceId), - podcast_title: currentTitle - }; - - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.detail || "Failed to generate podcast"); - } - - const data = await response.json(); - toast.success(`Podcast "${currentTitle}" generation started!`); - - // Move to the next chat or finish - if (currentChatIndex < selectedChats.length - 1) { - // Set up for next chat - setCurrentChatIndex(currentChatIndex + 1); - - // Find the next chat from the chats array - const nextChatId = selectedChats[currentChatIndex + 1]; - const nextChat = chats.find(chat => chat.id === nextChatId) || null; - setProcessingChat(nextChat); - - // Default title for the next chat - if (!podcastTitles[nextChatId]) { - setPodcastTitle(nextChat?.title || `Podcast from Chat ${nextChatId}`); - } else { - setPodcastTitle(podcastTitles[nextChatId]); - } - - setIsGeneratingPodcast(false); - } else { - // All done - finishPodcastGeneration(); - } - } catch (error) { - console.error('Error generating podcast:', error); - toast.error(error instanceof Error ? error.message : 'Failed to generate podcast'); - setIsGeneratingPodcast(false); - } - }; - - // Helper to finish the podcast generation process - const finishPodcastGeneration = () => { - toast.success("All podcasts are being generated! Check the logs tab to see their status."); - setPodcastDialogOpen(false); - setSelectedChats([]); - setSelectionMode(false); - setCurrentChatIndex(0); - setPodcastTitles({}); - setProcessingChat(null); - setPodcastTitle(""); - setIsGeneratingPodcast(false); - }; + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(`Failed to fetch chats: ${response.status} ${errorData?.error || ""}`); + } - // Start podcast generation flow - const startPodcastGeneration = () => { - if (selectedChats.length === 0) { - toast.error("Please select at least one chat"); - return; - } - - // Reset the state for podcast generation - setCurrentChatIndex(0); - setPodcastTitles({}); - - // Set up for the first chat - const firstChatId = selectedChats[0]; - const firstChat = chats.find(chat => chat.id === firstChatId) || null; - setProcessingChat(firstChat); - - // Set default title for the first chat - setPodcastTitle(firstChat?.title || `Podcast from Chat ${firstChatId}`); - setPodcastDialogOpen(true); - }; - - // Update the title for the current chat - const updateCurrentChatTitle = (title: string) => { - const currentChatId = selectedChats[currentChatIndex]; - setPodcastTitle(title); - setPodcastTitles(prev => ({ - ...prev, - [currentChatId]: title - })); - }; - - // Skip generating a podcast for the current chat - const skipCurrentChat = () => { - if (currentChatIndex < selectedChats.length - 1) { - // Move to the next chat - setCurrentChatIndex(currentChatIndex + 1); - - // Find the next chat - const nextChatId = selectedChats[currentChatIndex + 1]; - const nextChat = chats.find(chat => chat.id === nextChatId) || null; - setProcessingChat(nextChat); - - // Set default title for the next chat - if (!podcastTitles[nextChatId]) { - setPodcastTitle(nextChat?.title || `Podcast from Chat ${nextChatId}`); - } else { - setPodcastTitle(podcastTitles[nextChatId]); - } - } else { - // All done (all skipped) - finishPodcastGeneration(); - } - }; + const data: Chat[] = await response.json(); + setChats(data); + setFilteredChats(data); + setError(null); + } catch (error) { + console.error("Error fetching chats:", error); + setError(error instanceof Error ? error.message : "Unknown error occurred"); + setChats([]); + setFilteredChats([]); + } finally { + setIsLoading(false); + } + }; - // Toggle chat selection - const toggleChatSelection = (chatId: number) => { - setSelectedChats(prev => - prev.includes(chatId) - ? prev.filter(id => id !== chatId) - : [...prev, chatId] - ); - }; + fetchChats(); + }, [searchSpaceId]); - // Select all visible chats - const selectAllVisibleChats = () => { - const visibleChatIds = currentChats.map(chat => chat.id); - setSelectedChats(prev => { - const allSelected = visibleChatIds.every(id => prev.includes(id)); - return allSelected - ? prev.filter(id => !visibleChatIds.includes(id)) // Deselect all visible if all are selected - : [...new Set([...prev, ...visibleChatIds])]; // Add all visible, ensuring no duplicates - }); - }; + // Filter and sort chats based on search query, type, and sort order + useEffect(() => { + let result = [...chats]; - // Cancel selection mode - const cancelSelectionMode = () => { - setSelectionMode(false); - setSelectedChats([]); - }; + // Filter by search term + if (searchQuery) { + const query = searchQuery.toLowerCase(); + result = result.filter((chat) => chat.title.toLowerCase().includes(query)); + } - return ( - -
-
-

All Chats

-

View, search, and manage all your chats.

-
- - {/* Filter and Search Bar */} -
-
-
- - setSearchQuery(e.target.value)} - /> -
- - -
- -
- {selectionMode ? ( - <> - - - - - ) : ( - <> - - - - )} -
-
- - {/* Status Messages */} - {isLoading && ( -
-
-
-

Loading chats...

-
-
- )} - - {error && !isLoading && ( -
-

Error loading chats

-

{error}

-
- )} - - {!isLoading && !error && filteredChats.length === 0 && ( -
- -

No chats found

-

- {searchQuery || selectedType !== 'all' - ? 'Try adjusting your search filters' - : 'Start a new chat to get started'} -

-
- )} - - {/* Chat Grid */} - {!isLoading && !error && filteredChats.length > 0 && ( - -
- {currentChats.map((chat, index) => ( - { - if (!selectionMode) return; - // Ignore clicks coming from interactive elements - if ((e.target as HTMLElement).closest('button, a, [data-stop-selection]')) return; - toggleChatSelection(chat.id); - }} - > - -
-
- {selectionMode && ( -
- {selectedChats.includes(chat.id) - ? - : } -
- )} -
- {chat.title || `Chat ${chat.id}`} - - - - {format(new Date(chat.created_at), 'MMM d, yyyy')} - - -
-
- {!selectionMode && ( - - - - - - window.location.href = `/dashboard/${chat.search_space_id}/researcher/${chat.id}`}> - - View Chat - - { - setSelectedChats([chat.id]); - setPodcastTitle(chat.title || `Chat ${chat.id}`); - setPodcastDialogOpen(true); - }} - > - - Generate Podcast - - - { - e.stopPropagation(); - setChatToDelete({ id: chat.id, title: chat.title || `Chat ${chat.id}` }); - setDeleteDialogOpen(true); - }} - > - - Delete Chat - - - - )} -
-
- -
- {chat.messages && chat.messages.length > 0 - ? typeof chat.messages[0] === 'string' - ? chat.messages[0] - : chat.messages[0]?.content || 'No message content' - : 'No messages in this chat.'} -
-
- -
- - {chat.messages?.length || 0} messages -
- - - {chat.type || 'Unknown'} - -
-
- ))} -
-
- )} - - {/* Pagination */} - {!isLoading && !error && totalPages > 1 && ( - - - - { - e.preventDefault(); - if (currentPage > 1) setCurrentPage(currentPage - 1); - }} - className={currentPage <= 1 ? 'pointer-events-none opacity-50' : ''} - /> - - - {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 ( - - ... - - ); - } - return null; - } - - return ( - - { - e.preventDefault(); - setCurrentPage(pageNumber); - }} - isActive={pageNumber === currentPage} - > - {pageNumber} - - - ); - })} - - - { - e.preventDefault(); - if (currentPage < totalPages) setCurrentPage(currentPage + 1); - }} - className={currentPage >= totalPages ? 'pointer-events-none opacity-50' : ''} - /> - - - - )} -
- - {/* Delete Confirmation Dialog */} - - - - - - Delete Chat - - - Are you sure you want to delete {chatToDelete?.title}? This action cannot be undone. - - - - - - - - - - {/* Podcast Generation Dialog */} - { - if (!isOpen) { - // Cancel the process if dialog is closed - setPodcastDialogOpen(false); - setSelectedChats([]); - setSelectionMode(false); - setCurrentChatIndex(0); - setPodcastTitles({}); - setProcessingChat(null); - setPodcastTitle(""); - } else { - setPodcastDialogOpen(true); - } - }} - > - - - - - Generate Podcast {currentChatIndex + 1} of {selectedChats.length} - - - {selectedChats.length > 1 ? ( - <>Creating individual podcasts for each selected chat. Currently processing: {processingChat?.title || `Chat ${selectedChats[currentChatIndex]}`} - ) : ( - <>Create a podcast from this chat. The podcast will be available in the podcasts section once generated. - )} - - - -
-
- - updateCurrentChatTitle(e.target.value)} - /> -
- - {selectedChats.length > 1 && ( -
-
-
- )} -
- - - {selectedChats.length > 1 && !isGeneratingPodcast && ( - - )} - - - -
-
-
- ); -} \ No newline at end of file + // 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(); + + return sortOrder === "newest" ? dateB - dateA : dateA - dateB; + }); + + 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]); + + // Function to handle chat deletion + const handleDeleteChat = async () => { + if (!chatToDelete) return; + + setIsDeleting(true); + try { + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + setIsDeleting(false); + return; + } + + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatToDelete.id}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Failed to delete chat: ${response.statusText}`); + } + + // Close dialog and refresh chats + setDeleteDialogOpen(false); + setChatToDelete(null); + + // Update local state by removing the deleted chat + setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatToDelete.id)); + } catch (error) { + console.error("Error deleting chat:", error); + } finally { + setIsDeleting(false); + } + }; + + // Calculate pagination + const indexOfLastChat = currentPage * chatsPerPage; + const indexOfFirstChat = indexOfLastChat - chatsPerPage; + const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); + + // Get unique chat types for filter dropdown + const chatTypes = ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))]; + + // Generate individual podcasts from selected chats + const handleGeneratePodcast = async () => { + if (selectedChats.length === 0) { + toast.error("Please select at least one chat"); + return; + } + + const currentChatId = selectedChats[currentChatIndex]; + const currentTitle = podcastTitles[currentChatId] || podcastTitle; + + if (!currentTitle.trim()) { + toast.error("Please enter a podcast title"); + return; + } + + setIsGeneratingPodcast(true); + try { + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + toast.error("Authentication error. Please log in again."); + setIsGeneratingPodcast(false); + return; + } + + // Create payload for single chat + const payload = { + type: "CHAT", + ids: [currentChatId], // Single chat ID + search_space_id: parseInt(searchSpaceId), + podcast_title: currentTitle, + }; + + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/podcasts/generate/`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to generate podcast"); + } + + const data = await response.json(); + toast.success(`Podcast "${currentTitle}" generation started!`); + + // Move to the next chat or finish + if (currentChatIndex < selectedChats.length - 1) { + // Set up for next chat + setCurrentChatIndex(currentChatIndex + 1); + + // Find the next chat from the chats array + const nextChatId = selectedChats[currentChatIndex + 1]; + const nextChat = chats.find((chat) => chat.id === nextChatId) || null; + setProcessingChat(nextChat); + + // Default title for the next chat + if (!podcastTitles[nextChatId]) { + setPodcastTitle(nextChat?.title || `Podcast from Chat ${nextChatId}`); + } else { + setPodcastTitle(podcastTitles[nextChatId]); + } + + setIsGeneratingPodcast(false); + } else { + // All done + finishPodcastGeneration(); + } + } catch (error) { + console.error("Error generating podcast:", error); + toast.error(error instanceof Error ? error.message : "Failed to generate podcast"); + setIsGeneratingPodcast(false); + } + }; + + // Helper to finish the podcast generation process + const finishPodcastGeneration = () => { + toast.success("All podcasts are being generated! Check the logs tab to see their status."); + setPodcastDialogOpen(false); + setSelectedChats([]); + setSelectionMode(false); + setCurrentChatIndex(0); + setPodcastTitles({}); + setProcessingChat(null); + setPodcastTitle(""); + setIsGeneratingPodcast(false); + }; + + // Start podcast generation flow + const startPodcastGeneration = () => { + if (selectedChats.length === 0) { + toast.error("Please select at least one chat"); + return; + } + + // Reset the state for podcast generation + setCurrentChatIndex(0); + setPodcastTitles({}); + + // Set up for the first chat + const firstChatId = selectedChats[0]; + const firstChat = chats.find((chat) => chat.id === firstChatId) || null; + setProcessingChat(firstChat); + + // Set default title for the first chat + setPodcastTitle(firstChat?.title || `Podcast from Chat ${firstChatId}`); + setPodcastDialogOpen(true); + }; + + // Update the title for the current chat + const updateCurrentChatTitle = (title: string) => { + const currentChatId = selectedChats[currentChatIndex]; + setPodcastTitle(title); + setPodcastTitles((prev) => ({ + ...prev, + [currentChatId]: title, + })); + }; + + // Skip generating a podcast for the current chat + const skipCurrentChat = () => { + if (currentChatIndex < selectedChats.length - 1) { + // Move to the next chat + setCurrentChatIndex(currentChatIndex + 1); + + // Find the next chat + const nextChatId = selectedChats[currentChatIndex + 1]; + const nextChat = chats.find((chat) => chat.id === nextChatId) || null; + setProcessingChat(nextChat); + + // Set default title for the next chat + if (!podcastTitles[nextChatId]) { + setPodcastTitle(nextChat?.title || `Podcast from Chat ${nextChatId}`); + } else { + setPodcastTitle(podcastTitles[nextChatId]); + } + } else { + // All done (all skipped) + finishPodcastGeneration(); + } + }; + + // Toggle chat selection + const toggleChatSelection = (chatId: number) => { + setSelectedChats((prev) => + prev.includes(chatId) ? prev.filter((id) => id !== chatId) : [...prev, chatId] + ); + }; + + // Select all visible chats + const selectAllVisibleChats = () => { + const visibleChatIds = currentChats.map((chat) => chat.id); + setSelectedChats((prev) => { + const allSelected = visibleChatIds.every((id) => prev.includes(id)); + return allSelected + ? prev.filter((id) => !visibleChatIds.includes(id)) // Deselect all visible if all are selected + : [...new Set([...prev, ...visibleChatIds])]; // Add all visible, ensuring no duplicates + }); + }; + + // Cancel selection mode + const cancelSelectionMode = () => { + setSelectionMode(false); + setSelectedChats([]); + }; + + return ( + +
+
+

All Chats

+

View, search, and manage all your chats.

+
+ + {/* Filter and Search Bar */} +
+
+
+ + setSearchQuery(e.target.value)} + /> +
+ + +
+ +
+ {selectionMode ? ( + <> + + + + + ) : ( + <> + + + + )} +
+
+ + {/* Status Messages */} + {isLoading && ( +
+
+
+

Loading chats...

+
+
+ )} + + {error && !isLoading && ( +
+

Error loading chats

+

{error}

+
+ )} + + {!isLoading && !error && filteredChats.length === 0 && ( +
+ +

No chats found

+

+ {searchQuery || selectedType !== "all" + ? "Try adjusting your search filters" + : "Start a new chat to get started"} +

+
+ )} + + {/* Chat Grid */} + {!isLoading && !error && filteredChats.length > 0 && ( + +
+ {currentChats.map((chat, index) => ( + { + if (!selectionMode) return; + // Ignore clicks coming from interactive elements + if ((e.target as HTMLElement).closest("button, a, [data-stop-selection]")) + return; + toggleChatSelection(chat.id); + }} + > + +
+
+ {selectionMode && ( +
+ {selectedChats.includes(chat.id) ? ( + + ) : ( + + )} +
+ )} +
+ + {chat.title || `Chat ${chat.id}`} + + + + + {format(new Date(chat.created_at), "MMM d, yyyy")} + + +
+
+ {!selectionMode && ( + + + + + + + (window.location.href = `/dashboard/${chat.search_space_id}/researcher/${chat.id}`) + } + > + + View Chat + + { + setSelectedChats([chat.id]); + setPodcastTitle(chat.title || `Chat ${chat.id}`); + setPodcastDialogOpen(true); + }} + > + + Generate Podcast + + + { + e.stopPropagation(); + setChatToDelete({ + id: chat.id, + title: chat.title || `Chat ${chat.id}`, + }); + setDeleteDialogOpen(true); + }} + > + + Delete Chat + + + + )} +
+
+ +
+ {chat.messages && chat.messages.length > 0 + ? typeof chat.messages[0] === "string" + ? chat.messages[0] + : chat.messages[0]?.content || "No message content" + : "No messages in this chat."} +
+
+ +
+ + {chat.messages?.length || 0} messages +
+ + + {chat.type || "Unknown"} + +
+
+ ))} +
+
+ )} + + {/* Pagination */} + {!isLoading && !error && totalPages > 1 && ( + + + + { + e.preventDefault(); + if (currentPage > 1) setCurrentPage(currentPage - 1); + }} + className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""} + /> + + + {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 ( + + ... + + ); + } + return null; + } + + return ( + + { + e.preventDefault(); + setCurrentPage(pageNumber); + }} + isActive={pageNumber === currentPage} + > + {pageNumber} + + + ); + })} + + + { + e.preventDefault(); + if (currentPage < totalPages) setCurrentPage(currentPage + 1); + }} + className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""} + /> + + + + )} +
+ + {/* Delete Confirmation Dialog */} + + + + + + Delete Chat + + + Are you sure you want to delete{" "} + {chatToDelete?.title}? This action cannot be + undone. + + + + + + + + + + {/* Podcast Generation Dialog */} + { + if (!isOpen) { + // Cancel the process if dialog is closed + setPodcastDialogOpen(false); + setSelectedChats([]); + setSelectionMode(false); + setCurrentChatIndex(0); + setPodcastTitles({}); + setProcessingChat(null); + setPodcastTitle(""); + } else { + setPodcastDialogOpen(true); + } + }} + > + + + + + + Generate Podcast {currentChatIndex + 1} of {selectedChats.length} + + + + {selectedChats.length > 1 ? ( + <> + Creating individual podcasts for each selected chat. Currently processing:{" "} + + {processingChat?.title || `Chat ${selectedChats[currentChatIndex]}`} + + + ) : ( + <> + Create a podcast from this chat. The podcast will be available in the podcasts + section once generated. + + )} + + + +
+
+ + updateCurrentChatTitle(e.target.value)} + /> +
+ + {selectedChats.length > 1 && ( +
+
+
+ )} +
+ + + {selectedChats.length > 1 && !isGeneratingPodcast && ( + + )} + + + +
+
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx index f382d633c..9a2bb0f48 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx @@ -1,21 +1,25 @@ -import { Suspense } from 'react'; -import ChatsPageClient from './chats-client'; +import { Suspense } from "react"; +import ChatsPageClient from "./chats-client"; interface PageProps { - params: { - search_space_id: string; - }; + params: { + search_space_id: string; + }; } export default async function ChatsPage({ params }: PageProps) { - // Get search space ID from the route parameter - const { search_space_id: searchSpaceId } = await Promise.resolve(params); - - return ( - -
- }> - -
- ); -} \ No newline at end of file + // Get search space ID from the route parameter + const { search_space_id: searchSpaceId } = await Promise.resolve(params); + + return ( + +
+ + } + > + +
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx index c9a768461..ff6be78e7 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -1,44 +1,40 @@ -'use client'; +"use client"; -import { - SidebarInset, - SidebarProvider, - SidebarTrigger, -} from "@/components/ui/sidebar" -import { ThemeTogglerComponent } from "@/components/theme/theme-toggle" -import React from 'react' -import { Separator } from "@/components/ui/separator" -import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider" +import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; +import { ThemeTogglerComponent } from "@/components/theme/theme-toggle"; +import type React from "react"; +import { Separator } from "@/components/ui/separator"; +import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider"; 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[]; }) { - return ( - - {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} - - -
-
- - - -
-
- {children} -
-
- ) -} \ No newline at end of file + return ( + + {/* Use AppSidebarProvider which fetches user, search space, and recent chats */} + + +
+
+ + + +
+
+ {children} +
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx index d01d54ad5..745fcbf64 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/(manage)/page.tsx @@ -4,23 +4,11 @@ import { useState, useEffect } from "react"; import { useRouter, useParams } from "next/navigation"; import { motion } from "framer-motion"; import { toast } from "sonner"; -import { - Edit, - Plus, - Trash2, - RefreshCw, - Calendar as CalendarIcon, -} from "lucide-react"; +import { Edit, Plus, Trash2, RefreshCw, Calendar as CalendarIcon } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, @@ -40,12 +28,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Dialog, DialogContent, @@ -83,14 +66,12 @@ export default function ConnectorsPage() { const { connectors, isLoading, error, deleteConnector, indexConnector } = useSearchSourceConnectors(); - const [connectorToDelete, setConnectorToDelete] = useState( - null, - ); - const [indexingConnectorId, setIndexingConnectorId] = useState( - null, - ); + const [connectorToDelete, setConnectorToDelete] = useState(null); + const [indexingConnectorId, setIndexingConnectorId] = useState(null); const [datePickerOpen, setDatePickerOpen] = useState(false); - const [selectedConnectorForIndexing, setSelectedConnectorForIndexing] = useState(null); + const [selectedConnectorForIndexing, setSelectedConnectorForIndexing] = useState( + null + ); const [startDate, setStartDate] = useState(undefined); const [endDate, setEndDate] = useState(undefined); @@ -127,21 +108,17 @@ export default function ConnectorsPage() { if (selectedConnectorForIndexing === null) return; setDatePickerOpen(false); - + try { setIndexingConnectorId(selectedConnectorForIndexing); const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined; const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined; - + await indexConnector(selectedConnectorForIndexing, searchSpaceId, startDateStr, endDateStr); toast.success("Connector content indexing started"); } catch (error) { console.error("Error indexing connector content:", error); - toast.error( - error instanceof Error - ? error.message - : "Failed to index connector content", - ); + toast.error(error instanceof Error ? error.message : "Failed to index connector content"); } finally { setIndexingConnectorId(null); setSelectedConnectorForIndexing(null); @@ -158,11 +135,7 @@ export default function ConnectorsPage() { toast.success("Connector content indexing started"); } catch (error) { console.error("Error indexing connector content:", error); - toast.error( - error instanceof Error - ? error.message - : "Failed to index connector content", - ); + toast.error(error instanceof Error ? error.message : "Failed to index connector content"); } finally { setIndexingConnectorId(null); } @@ -182,11 +155,7 @@ export default function ConnectorsPage() { Manage your connected services and data sources.

- @@ -195,9 +164,7 @@ export default function ConnectorsPage() { Your Connectors - - View and manage all your connected services. - + View and manage all your connected services. {isLoading ? ( @@ -211,14 +178,9 @@ export default function ConnectorsPage() {

No connectors found

- You haven't added any connectors yet. Add one to enhance your - search capabilities. + You haven't added any connectors yet. Add one to enhance your search capabilities.

- @@ -237,12 +199,8 @@ export default function ConnectorsPage() { {connectors.map((connector) => ( - - {connector.name} - - - {getConnectorIcon(connector.connector_type)} - + {connector.name} + {getConnectorIcon(connector.connector_type)} {connector.is_indexable ? formatDateTime(connector.last_indexed_at) @@ -258,21 +216,15 @@ export default function ConnectorsPage() { @@ -286,21 +238,15 @@ export default function ConnectorsPage() { @@ -315,7 +261,7 @@ export default function ConnectorsPage() { size="sm" onClick={() => router.push( - `/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`, + `/dashboard/${searchSpaceId}/connectors/${connector.id}/edit` ) } > @@ -328,9 +274,7 @@ export default function ConnectorsPage() { variant="outline" size="sm" className="text-destructive-foreground hover:bg-destructive/10" - onClick={() => - setConnectorToDelete(connector.id) - } + onClick={() => setConnectorToDelete(connector.id)} > Delete @@ -338,18 +282,14 @@ export default function ConnectorsPage() { - - Delete Connector - + Delete Connector - Are you sure you want to delete this - connector? This action cannot be undone. + Are you sure you want to delete this connector? This action cannot + be undone. - setConnectorToDelete(null)} - > + setConnectorToDelete(null)}> Cancel - date > new Date() || (endDate ? date > endDate : false) - } + disabled={(date) => date > new Date() || (endDate ? date > endDate : false)} initialFocus /> @@ -493,9 +431,7 @@ export default function ConnectorsPage() { > Cancel - + diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx index 918a625d5..ffbd21b4a 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.tsx @@ -9,12 +9,12 @@ import { ArrowLeft, Check, Loader2, Github } from "lucide-react"; import { Form } from "@/components/ui/form"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; // Import Utils, Types, Hook, and Components @@ -27,227 +27,218 @@ import { EditSimpleTokenForm } from "@/components/editConnector/EditSimpleTokenF import { getConnectorIcon } from "@/components/chat"; export default function EditConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - // Ensure connectorId is parsed safely - const connectorIdParam = params.connector_id as string; - const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN; + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + // Ensure connectorId is parsed safely + const connectorIdParam = params.connector_id as string; + const connectorId = connectorIdParam ? parseInt(connectorIdParam, 10) : NaN; - // Use the custom hook to manage state and logic - const { - connectorsLoading, - connector, - isSaving, - editForm, - patForm, // Needed for GitHub child component - handleSaveChanges, - // GitHub specific props for the child component - editMode, - setEditMode, // Pass down if needed by GitHub component - originalPat, - currentSelectedRepos, - fetchedRepos, - setFetchedRepos, - newSelectedRepos, - setNewSelectedRepos, - isFetchingRepos, - handleFetchRepositories, - handleRepoSelectionChange, - } = useConnectorEditPage(connectorId, searchSpaceId); + // Use the custom hook to manage state and logic + const { + connectorsLoading, + connector, + isSaving, + editForm, + patForm, // Needed for GitHub child component + handleSaveChanges, + // GitHub specific props for the child component + editMode, + setEditMode, // Pass down if needed by GitHub component + originalPat, + currentSelectedRepos, + fetchedRepos, + setFetchedRepos, + newSelectedRepos, + setNewSelectedRepos, + isFetchingRepos, + handleFetchRepositories, + handleRepoSelectionChange, + } = useConnectorEditPage(connectorId, searchSpaceId); - // Redirect if connectorId is not a valid number after parsing - useEffect(() => { - if (isNaN(connectorId)) { - toast.error("Invalid Connector ID."); - router.push(`/dashboard/${searchSpaceId}/connectors`); - } - }, [connectorId, router, searchSpaceId]); + // Redirect if connectorId is not a valid number after parsing + useEffect(() => { + if (isNaN(connectorId)) { + toast.error("Invalid Connector ID."); + router.push(`/dashboard/${searchSpaceId}/connectors`); + } + }, [connectorId, router, searchSpaceId]); - // Loading State - if (connectorsLoading || !connector) { - // Handle NaN case before showing skeleton - if (isNaN(connectorId)) return null; - return ; - } + // Loading State + if (connectorsLoading || !connector) { + // Handle NaN case before showing skeleton + if (isNaN(connectorId)) return null; + return ; + } - // Main Render using data/handlers from the hook - return ( -
- + // Main Render using data/handlers from the hook + return ( +
+ - - - - - {getConnectorIcon(connector.connector_type)} - Edit {getConnectorTypeDisplay(connector.connector_type)} Connector - - - Modify connector name and configuration. - - + + + + + {getConnectorIcon(connector.connector_type)} + Edit {getConnectorTypeDisplay(connector.connector_type)} Connector + + Modify connector name and configuration. + -
- {/* Pass hook's handleSaveChanges */} - - - {/* Pass form control from hook */} - + + {/* Pass hook's handleSaveChanges */} + + + {/* Pass form control from hook */} + -
+
-

Configuration

+

Configuration

- {/* == GitHub == */} - {connector.connector_type === "GITHUB_CONNECTOR" && ( - - )} + {/* == GitHub == */} + {connector.connector_type === "GITHUB_CONNECTOR" && ( + + )} - {/* == Slack == */} - {connector.connector_type === "SLACK_CONNECTOR" && ( - - )} - {/* == Notion == */} - {connector.connector_type === "NOTION_CONNECTOR" && ( - - )} - {/* == Serper == */} - {connector.connector_type === "SERPER_API" && ( - - )} - {/* == Tavily == */} - {connector.connector_type === "TAVILY_API" && ( - - )} + {/* == Slack == */} + {connector.connector_type === "SLACK_CONNECTOR" && ( + + )} + {/* == Notion == */} + {connector.connector_type === "NOTION_CONNECTOR" && ( + + )} + {/* == Serper == */} + {connector.connector_type === "SERPER_API" && ( + + )} + {/* == Tavily == */} + {connector.connector_type === "TAVILY_API" && ( + + )} - {/* == Linear == */} - {connector.connector_type === "LINEAR_CONNECTOR" && ( - - )} + {/* == Linear == */} + {connector.connector_type === "LINEAR_CONNECTOR" && ( + + )} - {/* == Jira == */} - {connector.connector_type === "JIRA_CONNECTOR" && ( -
- - - -
- )} + {/* == Jira == */} + {connector.connector_type === "JIRA_CONNECTOR" && ( +
+ + + +
+ )} - {/* == Linkup == */} - {connector.connector_type === "LINKUP_API" && ( - - )} + {/* == Linkup == */} + {connector.connector_type === "LINKUP_API" && ( + + )} - {/* == Discord == */} - {connector.connector_type === "DISCORD_CONNECTOR" && ( - - )} -
- - - - - -
-
-
- ); + {/* == Discord == */} + {connector.connector_type === "DISCORD_CONNECTOR" && ( + + )} + + + + + + + + +
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx index 9ed3f94b9..ea623f21b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/[connector_id]/page.tsx @@ -10,302 +10,278 @@ import { toast } from "sonner"; import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { - useSearchSourceConnectors, - SearchSourceConnector, + useSearchSourceConnectors, + type SearchSourceConnector, } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; // Define the form schema with Zod const apiConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - api_key: z.string().min(10, { - message: "API key is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z.string().min(10, { + message: "API key is required and must be valid.", + }), }); // Helper function to get connector type display name const getConnectorTypeDisplay = (type: string): string => { - const typeMap: Record = { - SERPER_API: "Serper API", - TAVILY_API: "Tavily API", - SLACK_CONNECTOR: "Slack Connector", - NOTION_CONNECTOR: "Notion Connector", - GITHUB_CONNECTOR: "GitHub Connector", - LINEAR_CONNECTOR: "Linear Connector", - JIRA_CONNECTOR: "Jira Connector", - DISCORD_CONNECTOR: "Discord Connector", - LINKUP_API: "Linkup", - // Add other connector types here as needed - }; - return typeMap[type] || type; + const typeMap: Record = { + SERPER_API: "Serper API", + TAVILY_API: "Tavily API", + SLACK_CONNECTOR: "Slack Connector", + NOTION_CONNECTOR: "Notion Connector", + GITHUB_CONNECTOR: "GitHub Connector", + LINEAR_CONNECTOR: "Linear Connector", + JIRA_CONNECTOR: "Jira Connector", + DISCORD_CONNECTOR: "Discord Connector", + LINKUP_API: "Linkup", + // Add other connector types here as needed + }; + return typeMap[type] || type; }; // Define the type for the form values type ApiConnectorFormValues = z.infer; export default function EditConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const connectorId = parseInt(params.connector_id as string, 10); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const connectorId = parseInt(params.connector_id as string, 10); - const { connectors, updateConnector } = useSearchSourceConnectors(); - const [connector, setConnector] = useState( - null, - ); - const [isLoading, setIsLoading] = useState(true); - const [isSubmitting, setIsSubmitting] = useState(false); - // console.log("connector", connector); - // Initialize the form - const form = useForm({ - resolver: zodResolver(apiConnectorFormSchema), - defaultValues: { - name: "", - api_key: "", - }, - }); + const { connectors, updateConnector } = useSearchSourceConnectors(); + const [connector, setConnector] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + // console.log("connector", connector); + // Initialize the form + const form = useForm({ + resolver: zodResolver(apiConnectorFormSchema), + defaultValues: { + name: "", + api_key: "", + }, + }); - // Get API key field name based on connector type - const getApiKeyFieldName = (connectorType: string): string => { - const fieldMap: Record = { - SERPER_API: "SERPER_API_KEY", - TAVILY_API: "TAVILY_API_KEY", - SLACK_CONNECTOR: "SLACK_BOT_TOKEN", - NOTION_CONNECTOR: "NOTION_INTEGRATION_TOKEN", - GITHUB_CONNECTOR: "GITHUB_PAT", - DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN", - LINKUP_API: "LINKUP_API_KEY", - }; - return fieldMap[connectorType] || ""; - }; + // Get API key field name based on connector type + const getApiKeyFieldName = (connectorType: string): string => { + const fieldMap: Record = { + SERPER_API: "SERPER_API_KEY", + TAVILY_API: "TAVILY_API_KEY", + SLACK_CONNECTOR: "SLACK_BOT_TOKEN", + NOTION_CONNECTOR: "NOTION_INTEGRATION_TOKEN", + GITHUB_CONNECTOR: "GITHUB_PAT", + DISCORD_CONNECTOR: "DISCORD_BOT_TOKEN", + LINKUP_API: "LINKUP_API_KEY", + }; + return fieldMap[connectorType] || ""; + }; - // Find connector in the list - useEffect(() => { - const currentConnector = connectors.find((c) => c.id === connectorId); + // Find connector in the list + useEffect(() => { + const currentConnector = connectors.find((c) => c.id === connectorId); - if (currentConnector) { - setConnector(currentConnector); + if (currentConnector) { + setConnector(currentConnector); - // Check if connector type is supported - const apiKeyField = getApiKeyFieldName(currentConnector.connector_type); - if (apiKeyField) { - form.reset({ - name: currentConnector.name, - api_key: currentConnector.config[apiKeyField] || "", - }); - } else { - // Redirect if not a supported connector type - toast.error("This connector type is not supported for editing"); - router.push(`/dashboard/${searchSpaceId}/connectors`); - } + // Check if connector type is supported + const apiKeyField = getApiKeyFieldName(currentConnector.connector_type); + if (apiKeyField) { + form.reset({ + name: currentConnector.name, + api_key: currentConnector.config[apiKeyField] || "", + }); + } else { + // Redirect if not a supported connector type + toast.error("This connector type is not supported for editing"); + router.push(`/dashboard/${searchSpaceId}/connectors`); + } - setIsLoading(false); - } else if (!isLoading && connectors.length > 0) { - // If connectors are loaded but this one isn't found - toast.error("Connector not found"); - router.push(`/dashboard/${searchSpaceId}/connectors`); - } - }, [connectors, connectorId, form, router, searchSpaceId, isLoading]); + setIsLoading(false); + } else if (!isLoading && connectors.length > 0) { + // If connectors are loaded but this one isn't found + toast.error("Connector not found"); + router.push(`/dashboard/${searchSpaceId}/connectors`); + } + }, [connectors, connectorId, form, router, searchSpaceId, isLoading]); - // Handle form submission - const onSubmit = async (values: ApiConnectorFormValues) => { - if (!connector) return; + // Handle form submission + const onSubmit = async (values: ApiConnectorFormValues) => { + if (!connector) return; - setIsSubmitting(true); - try { - const apiKeyField = getApiKeyFieldName(connector.connector_type); + setIsSubmitting(true); + try { + const apiKeyField = getApiKeyFieldName(connector.connector_type); - // Only update the API key if a new one was provided - const updatedConfig = { ...connector.config }; - if (values.api_key) { - updatedConfig[apiKeyField] = values.api_key; - } + // Only update the API key if a new one was provided + const updatedConfig = { ...connector.config }; + if (values.api_key) { + updatedConfig[apiKeyField] = values.api_key; + } - await updateConnector(connectorId, { - name: values.name, - connector_type: connector.connector_type, - config: updatedConfig, - is_indexable: connector.is_indexable, - last_indexed_at: connector.last_indexed_at, - }); + await updateConnector(connectorId, { + name: values.name, + connector_type: connector.connector_type, + config: updatedConfig, + is_indexable: connector.is_indexable, + last_indexed_at: connector.last_indexed_at, + }); - toast.success("Connector updated successfully!"); - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error updating connector:", error); - toast.error( - error instanceof Error ? error.message : "Failed to update connector", - ); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Connector updated successfully!"); + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error updating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to update connector"); + } finally { + setIsSubmitting(false); + } + }; - if (isLoading) { - return ( -
-
-
-
-
-
- ); - } + if (isLoading) { + return ( +
+
+
+
+
+
+ ); + } - return ( -
- + return ( +
+ - - - - - Edit{" "} - {connector - ? getConnectorTypeDisplay(connector.connector_type) - : ""}{" "} - Connector - - Update your connector settings. - - - - - API Key Security - - Your API key is stored securely. For security reasons, we don't - display your existing API key. If you don't update the API key - field, your existing key will be preserved. - - + + + + + Edit {connector ? getConnectorTypeDisplay(connector.connector_type) : ""} Connector + + Update your connector settings. + + + + + API Key Security + + Your API key is stored securely. For security reasons, we don't display your + existing API key. If you don't update the API key field, your existing key will be + preserved. + + -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + ( + + Connector Name + + + + A friendly name to identify this connector. + + + )} + /> - ( - - - {connector?.connector_type === "SLACK_CONNECTOR" - ? "Slack Bot Token" - : connector?.connector_type === "NOTION_CONNECTOR" - ? "Notion Integration Token" - : connector?.connector_type === "GITHUB_CONNECTOR" - ? "GitHub Personal Access Token (PAT)" - : connector?.connector_type === "LINKUP_API" - ? "Linkup API Key" - : "API Key"} - - - - - - {connector?.connector_type === "SLACK_CONNECTOR" - ? "Enter a new Slack Bot Token or leave blank to keep your existing token." - : connector?.connector_type === "NOTION_CONNECTOR" - ? "Enter a new Notion Integration Token or leave blank to keep your existing token." - : connector?.connector_type === "GITHUB_CONNECTOR" - ? "Enter a new GitHub PAT or leave blank to keep your existing token." - : connector?.connector_type === "LINKUP_API" - ? "Enter a new Linkup API Key or leave blank to keep your existing key." - : "Enter a new API key or leave blank to keep your existing key."} - - - - )} - /> + ( + + + {connector?.connector_type === "SLACK_CONNECTOR" + ? "Slack Bot Token" + : connector?.connector_type === "NOTION_CONNECTOR" + ? "Notion Integration Token" + : connector?.connector_type === "GITHUB_CONNECTOR" + ? "GitHub Personal Access Token (PAT)" + : connector?.connector_type === "LINKUP_API" + ? "Linkup API Key" + : "API Key"} + + + + + + {connector?.connector_type === "SLACK_CONNECTOR" + ? "Enter a new Slack Bot Token or leave blank to keep your existing token." + : connector?.connector_type === "NOTION_CONNECTOR" + ? "Enter a new Notion Integration Token or leave blank to keep your existing token." + : connector?.connector_type === "GITHUB_CONNECTOR" + ? "Enter a new GitHub PAT or leave blank to keep your existing token." + : connector?.connector_type === "LINKUP_API" + ? "Enter a new Linkup API Key or leave blank to keep your existing key." + : "Enter a new API key or leave blank to keep your existing key."} + + + + )} + /> -
- -
- - -
-
-
-
- ); +
+ +
+ + + + + +
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx index 8a61747b5..159cafe40 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/discord-connector/page.tsx @@ -11,305 +11,336 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // Define the form schema with Zod const discordConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - bot_token: z.string() - .min(50, { message: "Discord Bot Token appears to be too short." }) - .regex(/^[A-Za-z0-9._-]+$/, { message: "Discord Bot Token contains invalid characters." }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + bot_token: z + .string() + .min(50, { message: "Discord Bot Token appears to be too short." }) + .regex(/^[A-Za-z0-9._-]+$/, { message: "Discord Bot Token contains invalid characters." }), }); // Define the type for the form values type DiscordConnectorFormValues = z.infer; export default function DiscordConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(discordConnectorFormSchema), - defaultValues: { - name: "Discord Connector", - bot_token: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(discordConnectorFormSchema), + defaultValues: { + name: "Discord Connector", + bot_token: "", + }, + }); - // Handle form submission - const onSubmit = async (values: DiscordConnectorFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "DISCORD_CONNECTOR", - config: { - DISCORD_BOT_TOKEN: values.bot_token, - }, - is_indexable: true, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: DiscordConnectorFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "DISCORD_CONNECTOR", + config: { + DISCORD_BOT_TOKEN: values.bot_token, + }, + is_indexable: true, + last_indexed_at: null, + }); - toast.success("Discord connector created successfully!"); - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Discord connector created successfully!"); + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - return ( -
- + return ( +
+ - - - - Connect - Documentation - - - - - - Connect Discord Server - - Integrate with Discord to search and retrieve information from your servers and channels. This connector can index your Discord messages for search. - - - - - - Bot Token Required - - You'll need a Discord Bot Token to use this connector. You can create a Discord bot and get the token from the{" "} - - Discord Developer Portal - . - - + + + + Connect + Documentation + -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect Discord Server + + Integrate with Discord to search and retrieve information from your servers and + channels. This connector can index your Discord messages for search. + + + + + + Bot Token Required + + You'll need a Discord Bot Token to use this connector. You can create a Discord + bot and get the token from the{" "} + + Discord Developer Portal + + . + + - ( - - Discord Bot Token - - - - - Your Discord Bot Token will be encrypted and stored securely. You can find it in the Bot section of your application in the Discord Developer Portal. - - - - )} - /> + + + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> -
- -
- - -
- -

What you get with Discord integration:

-
    -
  • Search through your Discord servers and channels
  • -
  • Access historical messages and shared files
  • -
  • Connect your team's knowledge directly to your search space
  • -
  • Keep your search results up-to-date with latest communications
  • -
  • Index your Discord messages for enhanced search capabilities
  • -
-
-
-
- - - - - Discord Connector Documentation - - Learn how to set up and use the Discord connector to index your server data. - - - -
-

How it works

-

- The Discord connector indexes all accessible channels for a given bot in your servers. -

-
    -
  • Upcoming: Support for private channels by granting the bot access.
  • -
-
- - - - Authorization - - - - Bot Setup Required - - You must create a Discord bot and add it to your server with the correct permissions. - - - -
    -
  1. Go to https://discord.com/developers/applications.
  2. -
  3. Create a new application and add a bot to it.
  4. -
  5. Copy the Bot Token from the Bot section.
  6. -
  7. Invite the bot to your server with the following OAuth2 scopes and permissions: -
      -
    • Scopes: bot
    • -
    • Bot Permissions: Read Messages/View Channels, Read Message History, Send Messages
    • -
    -
  8. -
  9. Paste the Bot Token above to connect.
  10. -
-
-
- - - Indexing - -
    -
  1. Navigate to the Connector Dashboard and select the Discord Connector.
  2. -
  3. Place the Bot Token under Step 1 Provide Credentials.
  4. -
  5. Click Connect to establish the connection.
  6. -
+ ( + + Discord Bot Token + + + + + Your Discord Bot Token will be encrypted and stored securely. You can + find it in the Bot section of your application in the Discord Developer + Portal. + + + + )} + /> - - - Important: Bot Channel Access - - After connecting, ensure the bot has access to all channels you want to index. You may need to adjust channel permissions in Discord. - - +
+ +
+ + +
+ +

What you get with Discord integration:

+
    +
  • Search through your Discord servers and channels
  • +
  • Access historical messages and shared files
  • +
  • Connect your team's knowledge directly to your search space
  • +
  • Keep your search results up-to-date with latest communications
  • +
  • Index your Discord messages for enhanced search capabilities
  • +
+
+
+
- - - First Indexing - - The first indexing pulls all accessible channels and may take longer than future updates. Only channels where the bot has access will be indexed. - - - -
-

Troubleshooting:

-
    -
  • - Missing messages: If you don't see messages from a channel, check the bot's permissions for that channel. -
  • -
  • - Bot not responding: Make sure the bot is online and the token is correct. -
  • -
  • - Private channels: The bot must be explicitly granted access to private channels. -
  • -
-
- - - -
-
-
-
-
-
- ); + + + + + Discord Connector Documentation + + + Learn how to set up and use the Discord connector to index your server data. + + + +
+

How it works

+

+ The Discord connector indexes all accessible channels for a given bot in your + servers. +

+
    +
  • Upcoming: Support for private channels by granting the bot access.
  • +
+
+ + + + + Authorization + + + + + Bot Setup Required + + You must create a Discord bot and add it to your server with the correct + permissions. + + + +
    +
  1. + Go to{" "} + + https://discord.com/developers/applications + + . +
  2. +
  3. Create a new application and add a bot to it.
  4. +
  5. Copy the Bot Token from the Bot section.
  6. +
  7. + Invite the bot to your server with the following OAuth2 scopes and + permissions: +
      +
    • + Scopes: bot +
    • +
    • + Bot Permissions: Read Messages/View Channels,{" "} + Read Message History, Send Messages +
    • +
    +
  8. +
  9. Paste the Bot Token above to connect.
  10. +
+
+
+ + + Indexing + +
    +
  1. + Navigate to the Connector Dashboard and select the{" "} + Discord Connector. +
  2. +
  3. + Place the Bot Token under{" "} + Step 1 Provide Credentials. +
  4. +
  5. + Click Connect to establish the connection. +
  6. +
+ + + + Important: Bot Channel Access + + After connecting, ensure the bot has access to all channels you want to + index. You may need to adjust channel permissions in Discord. + + + + + + First Indexing + + The first indexing pulls all accessible channels and may take longer than + future updates. Only channels where the bot has access will be indexed. + + + +
+

Troubleshooting:

+
    +
  • + Missing messages: If you don't see messages from a + channel, check the bot's permissions for that channel. +
  • +
  • + Bot not responding: Make sure the bot is online and the + token is correct. +
  • +
  • + Private channels: The bot must be explicitly granted + access to private channels. +
  • +
+
+
+
+
+
+
+
+ + +
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx index fc7a60276..d061c92dd 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/github-connector/page.tsx @@ -12,50 +12,49 @@ import { ArrowLeft, Check, Info, Loader2, Github, CircleAlert, ListChecks } from // Assuming useSearchSourceConnectors hook exists and works similarly import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Checkbox } from "@/components/ui/checkbox"; // Define the form schema with Zod for GitHub PAT entry step const githubPatFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - github_pat: z.string() - .min(20, { // Apply min length first - message: "GitHub Personal Access Token seems too short.", - }) - .refine(pat => pat.startsWith('ghp_') || pat.startsWith('github_pat_'), { // Then refine the pattern - message: "GitHub PAT should start with 'ghp_' or 'github_pat_'", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + github_pat: z + .string() + .min(20, { + // Apply min length first + message: "GitHub Personal Access Token seems too short.", + }) + .refine((pat) => pat.startsWith("ghp_") || pat.startsWith("github_pat_"), { + // Then refine the pattern + message: "GitHub PAT should start with 'ghp_' or 'github_pat_'", + }), }); // Define the type for the form values @@ -63,394 +62,468 @@ type GithubPatFormValues = z.infer; // Type for fetched GitHub repositories interface GithubRepo { - id: number; - name: string; - full_name: string; - private: boolean; - url: string; - description: string | null; - last_updated: string | null; + id: number; + name: string; + full_name: string; + private: boolean; + url: string; + description: string | null; + last_updated: string | null; } export default function GithubConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [step, setStep] = useState<'enter_pat' | 'select_repos'>('enter_pat'); - const [isFetchingRepos, setIsFetchingRepos] = useState(false); - const [isCreatingConnector, setIsCreatingConnector] = useState(false); - const [repositories, setRepositories] = useState([]); - const [selectedRepos, setSelectedRepos] = useState([]); - const [connectorName, setConnectorName] = useState("GitHub Connector"); - const [validatedPat, setValidatedPat] = useState(""); // Store the validated PAT + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [step, setStep] = useState<"enter_pat" | "select_repos">("enter_pat"); + const [isFetchingRepos, setIsFetchingRepos] = useState(false); + const [isCreatingConnector, setIsCreatingConnector] = useState(false); + const [repositories, setRepositories] = useState([]); + const [selectedRepos, setSelectedRepos] = useState([]); + const [connectorName, setConnectorName] = useState("GitHub Connector"); + const [validatedPat, setValidatedPat] = useState(""); // Store the validated PAT - const { createConnector } = useSearchSourceConnectors(); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form for PAT entry - const form = useForm({ - resolver: zodResolver(githubPatFormSchema), - defaultValues: { - name: connectorName, - github_pat: "", - }, - }); + // Initialize the form for PAT entry + const form = useForm({ + resolver: zodResolver(githubPatFormSchema), + defaultValues: { + name: connectorName, + github_pat: "", + }, + }); - // Function to fetch repositories using the new backend endpoint - const fetchRepositories = async (values: GithubPatFormValues) => { - setIsFetchingRepos(true); - setConnectorName(values.name); // Store the name - setValidatedPat(values.github_pat); // Store the PAT temporarily - try { - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - throw new Error('No authentication token found'); - } + // Function to fetch repositories using the new backend endpoint + const fetchRepositories = async (values: GithubPatFormValues) => { + setIsFetchingRepos(true); + setConnectorName(values.name); // Store the name + setValidatedPat(values.github_pat); // Store the PAT temporarily + try { + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + throw new Error("No authentication token found"); + } - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories/`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - }, - body: JSON.stringify({ github_pat: values.github_pat }) - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/github/repositories/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ github_pat: values.github_pat }), + } + ); - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || `Failed to fetch repositories: ${response.statusText}`); - } + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.detail || `Failed to fetch repositories: ${response.statusText}`); + } - const data: GithubRepo[] = await response.json(); - setRepositories(data); - setStep('select_repos'); // Move to the next step - toast.success(`Found ${data.length} repositories.`); - } catch (error) { - console.error("Error fetching GitHub repositories:", error); - const errorMessage = error instanceof Error ? error.message : "Failed to fetch repositories. Please check the PAT and try again."; - toast.error(errorMessage); - } finally { - setIsFetchingRepos(false); - } - }; + const data: GithubRepo[] = await response.json(); + setRepositories(data); + setStep("select_repos"); // Move to the next step + toast.success(`Found ${data.length} repositories.`); + } catch (error) { + console.error("Error fetching GitHub repositories:", error); + const errorMessage = + error instanceof Error + ? error.message + : "Failed to fetch repositories. Please check the PAT and try again."; + toast.error(errorMessage); + } finally { + setIsFetchingRepos(false); + } + }; - // Handle final connector creation - const handleCreateConnector = async () => { - if (selectedRepos.length === 0) { - toast.warning("Please select at least one repository to index."); - return; - } + // Handle final connector creation + const handleCreateConnector = async () => { + if (selectedRepos.length === 0) { + toast.warning("Please select at least one repository to index."); + return; + } - setIsCreatingConnector(true); - try { - await createConnector({ - name: connectorName, // Use the stored name - connector_type: "GITHUB_CONNECTOR", - config: { - GITHUB_PAT: validatedPat, // Use the stored validated PAT - repo_full_names: selectedRepos, // Add the selected repo names - }, - is_indexable: true, - last_indexed_at: null, - }); + setIsCreatingConnector(true); + try { + await createConnector({ + name: connectorName, // Use the stored name + connector_type: "GITHUB_CONNECTOR", + config: { + GITHUB_PAT: validatedPat, // Use the stored validated PAT + repo_full_names: selectedRepos, // Add the selected repo names + }, + is_indexable: true, + last_indexed_at: null, + }); - toast.success("GitHub connector created successfully!"); - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating GitHub connector:", error); - const errorMessage = error instanceof Error ? error.message : "Failed to create GitHub connector."; - toast.error(errorMessage); - } finally { - setIsCreatingConnector(false); - } - }; + toast.success("GitHub connector created successfully!"); + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating GitHub connector:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to create GitHub connector."; + toast.error(errorMessage); + } finally { + setIsCreatingConnector(false); + } + }; - // Handle checkbox changes - const handleRepoSelection = (repoFullName: string, checked: boolean) => { - setSelectedRepos(prev => - checked - ? [...prev, repoFullName] - : prev.filter(name => name !== repoFullName) - ); - }; + // Handle checkbox changes + const handleRepoSelection = (repoFullName: string, checked: boolean) => { + setSelectedRepos((prev) => + checked ? [...prev, repoFullName] : prev.filter((name) => name !== repoFullName) + ); + }; - return ( -
- + return ( +
+ - - - - Connect GitHub - Setup Guide - + + + + Connect GitHub + Setup Guide + - - - - - {step === 'enter_pat' ? : } - {step === 'enter_pat' ? "Connect GitHub Account" : "Select Repositories to Index"} - - - {step === 'enter_pat' - ? "Provide a name and GitHub Personal Access Token (PAT) to fetch accessible repositories." - : `Select which repositories you want SurfSense to index for search. Found ${repositories.length} repositories accessible via your PAT.` - } - - + + + + + {step === "enter_pat" ? ( + + ) : ( + + )} + {step === "enter_pat" ? "Connect GitHub Account" : "Select Repositories to Index"} + + + {step === "enter_pat" + ? "Provide a name and GitHub Personal Access Token (PAT) to fetch accessible repositories." + : `Select which repositories you want SurfSense to index for search. Found ${repositories.length} repositories accessible via your PAT.`} + + -
- {step === 'enter_pat' && ( - - - - GitHub Personal Access Token (PAT) Required - - You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to fetch repositories. You can create one from your{' '} - - GitHub Developer Settings - . The PAT will be used to fetch repositories and then stored securely to enable indexing. - - + + {step === "enter_pat" && ( + + + + GitHub Personal Access Token (PAT) Required + + You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to fetch + repositories. You can create one from your{" "} + + GitHub Developer Settings + + . The PAT will be used to fetch repositories and then stored securely to + enable indexing. + + - - ( - - Connector Name - - - - - A friendly name to identify this GitHub connection. - - - - )} - /> + + ( + + Connector Name + + + + + A friendly name to identify this GitHub connection. + + + + )} + /> - ( - - GitHub Personal Access Token (PAT) - - - - - Enter your GitHub PAT here to fetch your repositories. It will be stored encrypted later. - - - - )} - /> + ( + + GitHub Personal Access Token (PAT) + + + + + Enter your GitHub PAT here to fetch your repositories. It will be + stored encrypted later. + + + + )} + /> -
- -
- -
- )} +
+ +
+ +
+ )} - {step === 'select_repos' && ( - - {repositories.length === 0 ? ( - - - No Repositories Found - - No repositories were found or accessible with the provided PAT. Please check the token and its permissions, then go back and try again. - - - ) : ( -
- Repositories ({selectedRepos.length} selected) -
- {repositories.map((repo) => ( -
- handleRepoSelection(repo.full_name, !!checked)} - /> - -
- ))} -
- - Select the repositories you wish to index. Only checked repositories will be processed. - + {step === "select_repos" && ( + + {repositories.length === 0 ? ( + + + No Repositories Found + + No repositories were found or accessible with the provided PAT. Please + check the token and its permissions, then go back and try again. + + + ) : ( +
+ Repositories ({selectedRepos.length} selected) +
+ {repositories.map((repo) => ( +
+ + handleRepoSelection(repo.full_name, !!checked) + } + /> + +
+ ))} +
+ + Select the repositories you wish to index. Only checked repositories will + be processed. + -
- - -
-
- )} -
- )} - +
+ + +
+
+ )} +
+ )} + - -

What you get with GitHub integration:

-
    -
  • Search through code and documentation in your selected repositories
  • -
  • Access READMEs, Markdown files, and common code files
  • -
  • Connect your project knowledge directly to your search space
  • -
  • Index your selected repositories for enhanced search capabilities
  • -
-
-
-
+ +

What you get with GitHub integration:

+
    +
  • Search through code and documentation in your selected repositories
  • +
  • Access READMEs, Markdown files, and common code files
  • +
  • Connect your project knowledge directly to your search space
  • +
  • Index your selected repositories for enhanced search capabilities
  • +
+
+
+
- - - - GitHub Connector Setup Guide - - Learn how to generate a Personal Access Token (PAT) and connect your GitHub account. - - - -
-

How it works

-

- The GitHub connector uses a Personal Access Token (PAT) to authenticate with the GitHub API. First, it fetches a list of repositories accessible to the token. You then select which repositories you want to index. The connector indexes relevant files (code, markdown, text) from only the selected repositories. -

-
    -
  • The connector indexes files based on common code and documentation extensions.
  • -
  • Large files (over 1MB) are skipped during indexing.
  • -
  • Only selected repositories are indexed.
  • -
  • Indexing runs periodically (check connector settings for frequency) to keep content up-to-date.
  • -
-
+ + + + GitHub Connector Setup Guide + + Learn how to generate a Personal Access Token (PAT) and connect your GitHub + account. + + + +
+

How it works

+

+ The GitHub connector uses a Personal Access Token (PAT) to authenticate with the + GitHub API. First, it fetches a list of repositories accessible to the token. + You then select which repositories you want to index. The connector indexes + relevant files (code, markdown, text) from only the selected repositories. +

+
    +
  • + The connector indexes files based on common code and documentation extensions. +
  • +
  • Large files (over 1MB) are skipped during indexing.
  • +
  • Only selected repositories are indexed.
  • +
  • + Indexing runs periodically (check connector settings for frequency) to keep + content up-to-date. +
  • +
+
- - - Step 1: Generate GitHub PAT - -
-
-

Generating a Token:

-
    -
  1. Go to your GitHub Developer settings.
  2. -
  3. Click on Personal access tokens, then choose Tokens (classic) or Fine-grained tokens (recommended if available and suitable).
  4. -
  5. Click Generate new token (and choose the appropriate type).
  6. -
  7. Give your token a descriptive name (e.g., "SurfSense Connector").
  8. -
  9. Set an expiration date for the token (recommended for security).
  10. -
  11. Under Select scopes (for classic tokens) or Repository access (for fine-grained), grant the necessary permissions. At minimum, the `repo` scope (or equivalent read access to repositories for fine-grained tokens) is required to read repository content.
  12. -
  13. Click Generate token.
  14. -
  15. Important: Copy your new PAT immediately. You won't be able to see it again after leaving the page.
  16. -
-
-
-
-
+ + + + Step 1: Generate GitHub PAT + + +
+
+

Generating a Token:

+
    +
  1. + Go to your GitHub{" "} + + Developer settings + + . +
  2. +
  3. + Click on Personal access tokens, then choose{" "} + Tokens (classic) or{" "} + Fine-grained tokens (recommended if available and + suitable). +
  4. +
  5. + Click Generate new token (and choose the appropriate + type). +
  6. +
  7. + Give your token a descriptive name (e.g., "SurfSense Connector"). +
  8. +
  9. + Set an expiration date for the token (recommended for security). +
  10. +
  11. + Under Select scopes (for classic tokens) or{" "} + Repository access (for fine-grained), grant the + necessary permissions. At minimum, the `repo` scope + (or equivalent read access to repositories for fine-grained tokens) is + required to read repository content. +
  12. +
  13. + Click Generate token. +
  14. +
  15. + Important: Copy your new PAT immediately. You won't + be able to see it again after leaving the page. +
  16. +
+
+
+
+
- - Step 2: Connect in SurfSense - -
    -
  1. Navigate to the "Connect GitHub" tab.
  2. -
  3. Enter a name for your connector.
  4. -
  5. Paste the copied GitHub PAT into the "GitHub Personal Access Token (PAT)" field.
  6. -
  7. Click Fetch Repositories.
  8. -
  9. If the PAT is valid, you'll see a list of your accessible repositories.
  10. -
  11. Select the repositories you want SurfSense to index using the checkboxes.
  12. -
  13. Click the Create Connector button.
  14. -
  15. If the connection is successful, you will be redirected and can start indexing from the Connectors page.
  16. -
-
-
-
-
-
-
-
-
-
- ); -} + + + Step 2: Connect in SurfSense + + +
    +
  1. Navigate to the "Connect GitHub" tab.
  2. +
  3. Enter a name for your connector.
  4. +
  5. + Paste the copied GitHub PAT into the "GitHub Personal Access Token (PAT)" + field. +
  6. +
  7. + Click Fetch Repositories. +
  8. +
  9. + If the PAT is valid, you'll see a list of your accessible repositories. +
  10. +
  11. + Select the repositories you want SurfSense to index using the checkboxes. +
  12. +
  13. + Click the Create Connector button. +
  14. +
  15. + If the connection is successful, you will be redirected and can start + indexing from the Connectors page. +
  16. +
+
+
+ + + + + + +
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx index 23e128f1f..b586d05f2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/jira-connector/page.tsx @@ -11,462 +11,392 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // Define the form schema with Zod const jiraConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - base_url: z - .string() - .url({ - message: - "Please enter a valid Jira URL (e.g., https://yourcompany.atlassian.net)", - }) - .refine( - (url) => { - return url.includes("atlassian.net") || url.includes("jira"); - }, - { - message: "Please enter a valid Jira instance URL", - }, - ), - email: z.string().email({ - message: "Please enter a valid email address.", - }), - api_token: z.string().min(10, { - message: "Jira API Token is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + base_url: z + .string() + .url({ + message: "Please enter a valid Jira URL (e.g., https://yourcompany.atlassian.net)", + }) + .refine( + (url) => { + return url.includes("atlassian.net") || url.includes("jira"); + }, + { + message: "Please enter a valid Jira instance URL", + } + ), + email: z.string().email({ + message: "Please enter a valid email address.", + }), + api_token: z.string().min(10, { + message: "Jira API Token is required and must be valid.", + }), }); // Define the type for the form values type JiraConnectorFormValues = z.infer; export default function JiraConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(jiraConnectorFormSchema), - defaultValues: { - name: "Jira Connector", - base_url: "", - email: "", - api_token: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(jiraConnectorFormSchema), + defaultValues: { + name: "Jira Connector", + base_url: "", + email: "", + api_token: "", + }, + }); - // Handle form submission - const onSubmit = async (values: JiraConnectorFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "JIRA_CONNECTOR", - config: { - JIRA_BASE_URL: values.base_url, - JIRA_EMAIL: values.email, - JIRA_API_TOKEN: values.api_token, - }, - is_indexable: true, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: JiraConnectorFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "JIRA_CONNECTOR", + config: { + JIRA_BASE_URL: values.base_url, + JIRA_EMAIL: values.email, + JIRA_API_TOKEN: values.api_token, + }, + is_indexable: true, + last_indexed_at: null, + }); - toast.success("Jira connector created successfully!"); + toast.success("Jira connector created successfully!"); - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error( - error instanceof Error ? error.message : "Failed to create connector", - ); - } finally { - setIsSubmitting(false); - } - }; + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - return ( -
- + return ( +
+ - - - - Connect - Documentation - + + + + Connect + Documentation + - - - - - Connect Jira Instance - - - Integrate with Jira to search and retrieve information from - your issues, tickets, and comments. This connector can index - your Jira content for search. - - - - - - Jira Personal Access Token Required - - You'll need a Jira Personal Access Token to use this - connector. You can create one from{" "} - - Atlassian Account Settings - - - + + + + Connect Jira Instance + + Integrate with Jira to search and retrieve information from your issues, tickets, + and comments. This connector can index your Jira content for search. + + + + + + Jira Personal Access Token Required + + You'll need a Jira Personal Access Token to use this connector. You can create + one from{" "} + + Atlassian Account Settings + + + -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> - ( - - Jira Instance URL - - - - - Your Jira instance URL. For Atlassian Cloud, this is - typically https://yourcompany.atlassian.net - - - - )} - /> + ( + + Jira Instance URL + + + + + Your Jira instance URL. For Atlassian Cloud, this is typically + https://yourcompany.atlassian.net + + + + )} + /> - ( - - Email Address - - - - - Your Atlassian account email address. - - - - )} - /> + ( + + Email Address + + + + Your Atlassian account email address. + + + )} + /> - ( - - API Token - - - - - Your Jira API Token will be encrypted and stored securely. - - - - )} - /> + ( + + API Token + + + + + Your Jira API Token will be encrypted and stored securely. + + + + )} + /> -
- -
- - -
- -

- What you get with Jira integration: -

-
    -
  • Search through all your Jira issues and tickets
  • -
  • - Access issue descriptions, comments, and full discussion - threads -
  • -
  • - Connect your team's project management directly to your - search space -
  • -
  • - Keep your search results up-to-date with latest Jira content -
  • -
  • - Index your Jira issues for enhanced search capabilities -
  • -
  • - Search by issue keys, status, priority, and assignee - information -
  • -
-
-
-
+
+ +
+ + +
+ +

What you get with Jira integration:

+
    +
  • Search through all your Jira issues and tickets
  • +
  • Access issue descriptions, comments, and full discussion threads
  • +
  • Connect your team's project management directly to your search space
  • +
  • Keep your search results up-to-date with latest Jira content
  • +
  • Index your Jira issues for enhanced search capabilities
  • +
  • Search by issue keys, status, priority, and assignee information
  • +
+
+
+
- - - - - Jira Connector Documentation - - - Learn how to set up and use the Jira connector to index your - project management data. - - - -
-

How it works

-

- The Jira connector uses the Jira REST API with Basic Authentication - to fetch all issues and comments that your account has - access to within your Jira instance. -

-
    -
  • - For follow up indexing runs, the connector retrieves - issues and comments that have been updated since the last - indexing attempt. -
  • -
  • - Indexing is configured to run periodically, so updates - should appear in your search results within minutes. -
  • -
-
+ + + + Jira Connector Documentation + + Learn how to set up and use the Jira connector to index your project management + data. + + + +
+

How it works

+

+ The Jira connector uses the Jira REST API with Basic Authentication to fetch all + issues and comments that your account has access to within your Jira instance. +

+
    +
  • + For follow up indexing runs, the connector retrieves issues and comments that + have been updated since the last indexing attempt. +
  • +
  • + Indexing is configured to run periodically, so updates should appear in your + search results within minutes. +
  • +
+
- - - - Authorization - - - - - Read-Only Access is Sufficient - - You only need read access for this connector to work. - The API Token will only be used to read your Jira data. - - + + + + Authorization + + + + + Read-Only Access is Sufficient + + You only need read access for this connector to work. The API Token will + only be used to read your Jira data. + + -
-
-

- Step 1: Create an API Token -

-
    -
  1. Log in to your Atlassian account
  2. -
  3. - Navigate to{" "} - - https://id.atlassian.com/manage-profile/security/api-tokens - -
  4. -
  5. - Click Create API token -
  6. -
  7. - Enter a label for your token (like "SurfSense - Connector") -
  8. -
  9. - Click Create -
  10. -
  11. - Copy the generated token as it will only be shown - once -
  12. -
-
+
+
+

Step 1: Create an API Token

+
    +
  1. Log in to your Atlassian account
  2. +
  3. + Navigate to{" "} + + https://id.atlassian.com/manage-profile/security/api-tokens + +
  4. +
  5. + Click Create API token +
  6. +
  7. Enter a label for your token (like "SurfSense Connector")
  8. +
  9. + Click Create +
  10. +
  11. Copy the generated token as it will only be shown once
  12. +
+
-
-

- Step 2: Grant necessary access -

-

- The API Token will have access to all projects and - issues that your user account can see. Make sure your - account has appropriate permissions for the projects - you want to index. -

- - - Data Privacy - - Only issues, comments, and basic metadata will be - indexed. Jira attachments and linked files are not - indexed by this connector. - - -
-
- - +
+

Step 2: Grant necessary access

+

+ The API Token will have access to all projects and issues that your user + account can see. Make sure your account has appropriate permissions for + the projects you want to index. +

+ + + Data Privacy + + Only issues, comments, and basic metadata will be indexed. Jira + attachments and linked files are not indexed by this connector. + + +
+
+
+
- - - Indexing - - -
    -
  1. - Navigate to the Connector Dashboard and select the{" "} - Jira Connector. -
  2. -
  3. - Enter your Jira Instance URL (e.g., - https://yourcompany.atlassian.net) -
  4. -
  5. - Place your Personal Access Token in - the form field. -
  6. -
  7. - Click Connect to establish the - connection. -
  8. -
  9. - Once connected, your Jira issues will be indexed - automatically. -
  10. -
+ + Indexing + +
    +
  1. + Navigate to the Connector Dashboard and select the Jira{" "} + Connector. +
  2. +
  3. + Enter your Jira Instance URL (e.g., + https://yourcompany.atlassian.net) +
  4. +
  5. + Place your Personal Access Token in the form field. +
  6. +
  7. + Click Connect to establish the connection. +
  8. +
  9. Once connected, your Jira issues will be indexed automatically.
  10. +
- - - What Gets Indexed - -

- The Jira connector indexes the following data: -

-
    -
  • Issue keys and summaries (e.g., PROJ-123)
  • -
  • Issue descriptions
  • -
  • Issue comments and discussion threads
  • -
  • - Issue status, priority, and type information -
  • -
  • Assignee and reporter information
  • -
  • Project information
  • -
-
-
-
-
-
-
-
-
-
-
-
- ); + + + What Gets Indexed + +

The Jira connector indexes the following data:

+
    +
  • Issue keys and summaries (e.g., PROJ-123)
  • +
  • Issue descriptions
  • +
  • Issue comments and discussion threads
  • +
  • Issue status, priority, and type information
  • +
  • Assignee and reporter information
  • +
  • Project information
  • +
+
+
+ + + + + + + + +
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx index a594ed31e..1593c2f57 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linear-connector/page.tsx @@ -11,311 +11,344 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // Define the form schema with Zod const linearConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - api_key: z.string().min(10, { - message: "Linear API Key is required and must be valid.", - }).regex(/^lin_api_/, { - message: "Linear API Key should start with 'lin_api_'", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z + .string() + .min(10, { + message: "Linear API Key is required and must be valid.", + }) + .regex(/^lin_api_/, { + message: "Linear API Key should start with 'lin_api_'", + }), }); // Define the type for the form values type LinearConnectorFormValues = z.infer; export default function LinearConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(linearConnectorFormSchema), - defaultValues: { - name: "Linear Connector", - api_key: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(linearConnectorFormSchema), + defaultValues: { + name: "Linear Connector", + api_key: "", + }, + }); - // Handle form submission - const onSubmit = async (values: LinearConnectorFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "LINEAR_CONNECTOR", - config: { - LINEAR_API_KEY: values.api_key, - }, - is_indexable: true, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: LinearConnectorFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "LINEAR_CONNECTOR", + config: { + LINEAR_API_KEY: values.api_key, + }, + is_indexable: true, + last_indexed_at: null, + }); - toast.success("Linear connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Linear connector created successfully!"); - return ( -
- + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - - - - Connect - Documentation - - - - - - Connect Linear Workspace - - Integrate with Linear to search and retrieve information from your issues and comments. This connector can index your Linear content for search. - - - - - - Linear API Key Required - - You'll need a Linear API Key to use this connector. You can create a Linear API key from{" "} - - Linear API Settings - - - + return ( +
+ -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect + Documentation + - ( - - Linear API Key - - - - - Your Linear API Key will be encrypted and stored securely. It typically starts with "lin_api_". - - - - )} - /> + + + + Connect Linear Workspace + + Integrate with Linear to search and retrieve information from your issues and + comments. This connector can index your Linear content for search. + + + + + + Linear API Key Required + + You'll need a Linear API Key to use this connector. You can create a Linear API + key from{" "} + + Linear API Settings + + + -
- -
- - -
- -

What you get with Linear integration:

-
    -
  • Search through all your Linear issues and comments
  • -
  • Access issue titles, descriptions, and full discussion threads
  • -
  • Connect your team's project management directly to your search space
  • -
  • Keep your search results up-to-date with latest Linear content
  • -
  • Index your Linear issues for enhanced search capabilities
  • -
-
-
-
- - - - - Linear Connector Documentation - - Learn how to set up and use the Linear connector to index your project management data. - - - -
-

How it works

-

- The Linear connector uses the Linear GraphQL API to fetch all issues and comments that the API key has access to within a workspace. -

-
    -
  • For follow up indexing runs, the connector retrieves issues and comments that have been updated since the last indexing attempt.
  • -
  • Indexing is configured to run periodically, so updates should appear in your search results within minutes.
  • -
-
- - - - Authorization - - - - Read-Only Access is Sufficient - - You only need a read-only API key for this connector to work. This limits the permissions to just reading your Linear data. - - - -
-
-

Step 1: Create an API key

-
    -
  1. Log in to your Linear account
  2. -
  3. Navigate to https://linear.app/settings/api in your browser.
  4. -
  5. Alternatively, click on your profile picture → Settings → API
  6. -
  7. Click the + New API key button.
  8. -
  9. Enter a description for your key (like "Search Connector").
  10. -
  11. Select "Read-only" as the permission.
  12. -
  13. Click Create to generate the API key.
  14. -
  15. Copy the generated API key that starts with 'lin_api_' as it will only be shown once.
  16. -
-
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> -
-

Step 2: Grant necessary access

-

- The API key will have access to all issues and comments that your user account can see. If you're creating the key as an admin, it will have access to all issues in the workspace. -

- - - Data Privacy - - Only issues and comments will be indexed. Linear attachments and linked files are not indexed by this connector. - - -
-
-
-
- - - Indexing - -
    -
  1. Navigate to the Connector Dashboard and select the Linear Connector.
  2. -
  3. Place the API Key in the form field.
  4. -
  5. Click Connect to establish the connection.
  6. -
  7. Once connected, your Linear issues will be indexed automatically.
  8. -
- - - - What Gets Indexed - -

The Linear connector indexes the following data:

-
    -
  • Issue titles and identifiers (e.g., PROJ-123)
  • -
  • Issue descriptions
  • -
  • Issue comments
  • -
  • Issue status and metadata
  • -
-
-
-
-
-
-
-
-
-
-
-
- ); + ( + + Linear API Key + + + + + Your Linear API Key will be encrypted and stored securely. It typically + starts with "lin_api_". + + + + )} + /> + +
+ +
+ + +
+ +

What you get with Linear integration:

+
    +
  • Search through all your Linear issues and comments
  • +
  • Access issue titles, descriptions, and full discussion threads
  • +
  • Connect your team's project management directly to your search space
  • +
  • Keep your search results up-to-date with latest Linear content
  • +
  • Index your Linear issues for enhanced search capabilities
  • +
+
+
+
+ + + + + Linear Connector Documentation + + Learn how to set up and use the Linear connector to index your project management + data. + + + +
+

How it works

+

+ The Linear connector uses the Linear GraphQL API to fetch all issues and + comments that the API key has access to within a workspace. +

+
    +
  • + For follow up indexing runs, the connector retrieves issues and comments that + have been updated since the last indexing attempt. +
  • +
  • + Indexing is configured to run periodically, so updates should appear in your + search results within minutes. +
  • +
+
+ + + + + Authorization + + + + + Read-Only Access is Sufficient + + You only need a read-only API key for this connector to work. This limits + the permissions to just reading your Linear data. + + + +
+
+

Step 1: Create an API key

+
    +
  1. Log in to your Linear account
  2. +
  3. + Navigate to{" "} + + https://linear.app/settings/api + {" "} + in your browser. +
  4. +
  5. Alternatively, click on your profile picture → Settings → API
  6. +
  7. + Click the + New API key button. +
  8. +
  9. Enter a description for your key (like "Search Connector").
  10. +
  11. Select "Read-only" as the permission.
  12. +
  13. + Click Create to generate the API key. +
  14. +
  15. + Copy the generated API key that starts with 'lin_api_' as it will only + be shown once. +
  16. +
+
+ +
+

Step 2: Grant necessary access

+

+ The API key will have access to all issues and comments that your user + account can see. If you're creating the key as an admin, it will have + access to all issues in the workspace. +

+ + + Data Privacy + + Only issues and comments will be indexed. Linear attachments and + linked files are not indexed by this connector. + + +
+
+
+
+ + + Indexing + +
    +
  1. + Navigate to the Connector Dashboard and select the Linear{" "} + Connector. +
  2. +
  3. + Place the API Key in the form field. +
  4. +
  5. + Click Connect to establish the connection. +
  6. +
  7. Once connected, your Linear issues will be indexed automatically.
  8. +
+ + + + What Gets Indexed + +

The Linear connector indexes the following data:

+
    +
  • Issue titles and identifiers (e.g., PROJ-123)
  • +
  • Issue descriptions
  • +
  • Issue comments
  • +
  • Issue status and metadata
  • +
+
+
+
+
+
+
+
+
+
+
+
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx index 291bdfb36..677bfc524 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/linkup-api/page.tsx @@ -11,197 +11,184 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; // Define the form schema with Zod const linkupApiFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - api_key: z.string().min(10, { - message: "API key is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z.string().min(10, { + message: "API key is required and must be valid.", + }), }); // Define the type for the form values type LinkupApiFormValues = z.infer; export default function LinkupApiPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(linkupApiFormSchema), - defaultValues: { - name: "Linkup API Connector", - api_key: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(linkupApiFormSchema), + defaultValues: { + name: "Linkup API Connector", + api_key: "", + }, + }); - // Handle form submission - const onSubmit = async (values: LinkupApiFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "LINKUP_API", - config: { - LINKUP_API_KEY: values.api_key, - }, - is_indexable: false, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: LinkupApiFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "LINKUP_API", + config: { + LINKUP_API_KEY: values.api_key, + }, + is_indexable: false, + last_indexed_at: null, + }); - toast.success("Linkup API connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Linkup API connector created successfully!"); - return ( -
- + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - - - - Connect Linkup API - - Integrate with Linkup API to enhance your search capabilities with AI-powered search results. - - - - - - API Key Required - - You'll need a Linkup API key to use this connector. You can get one by signing up at{" "} - - linkup.so - - - + return ( +
+ -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect Linkup API + + Integrate with Linkup API to enhance your search capabilities with AI-powered search + results. + + + + + + API Key Required + + You'll need a Linkup API key to use this connector. You can get one by signing up at{" "} + + linkup.so + + + - ( - - Linkup API Key - - - - - Your API key will be encrypted and stored securely. - - - - )} - /> + + + ( + + Connector Name + + + + A friendly name to identify this connector. + + + )} + /> -
- -
- - -
- -

What you get with Linkup API:

-
    -
  • AI-powered search results tailored to your queries
  • -
  • Real-time information from the web
  • -
  • Enhanced search capabilities for your projects
  • -
-
-
-
-
- ); + ( + + Linkup API Key + + + + + Your API key will be encrypted and stored securely. + + + + )} + /> + +
+ +
+ + +
+ +

What you get with Linkup API:

+
    +
  • AI-powered search results tailored to your queries
  • +
  • Real-time information from the web
  • +
  • Enhanced search capabilities for your projects
  • +
+
+
+
+
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx index 1d9e3efa0..e739aa5ac 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/notion-connector/page.tsx @@ -11,307 +11,355 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // Define the form schema with Zod const notionConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - integration_token: z.string().min(10, { - message: "Notion Integration Token is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + integration_token: z.string().min(10, { + message: "Notion Integration Token is required and must be valid.", + }), }); // Define the type for the form values type NotionConnectorFormValues = z.infer; export default function NotionConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(notionConnectorFormSchema), - defaultValues: { - name: "Notion Connector", - integration_token: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(notionConnectorFormSchema), + defaultValues: { + name: "Notion Connector", + integration_token: "", + }, + }); - // Handle form submission - const onSubmit = async (values: NotionConnectorFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "NOTION_CONNECTOR", - config: { - NOTION_INTEGRATION_TOKEN: values.integration_token, - }, - is_indexable: true, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: NotionConnectorFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "NOTION_CONNECTOR", + config: { + NOTION_INTEGRATION_TOKEN: values.integration_token, + }, + is_indexable: true, + last_indexed_at: null, + }); - toast.success("Notion connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Notion connector created successfully!"); - return ( -
- + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - - - - Connect - Documentation - - - - - - Connect Notion Workspace - - Integrate with Notion to search and retrieve information from your workspace pages and databases. This connector can index your Notion content for search. - - - - - - Notion Integration Token Required - - You'll need a Notion Integration Token to use this connector. You can create a Notion integration and get the token from{" "} - - Notion Integrations Dashboard - - - + return ( +
+ -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect + Documentation + - ( - - Notion Integration Token - - - - - Your Notion Integration Token will be encrypted and stored securely. It typically starts with "ntn_". - - - - )} - /> + + + + Connect Notion Workspace + + Integrate with Notion to search and retrieve information from your workspace pages + and databases. This connector can index your Notion content for search. + + + + + + Notion Integration Token Required + + You'll need a Notion Integration Token to use this connector. You can create a + Notion integration and get the token from{" "} + + Notion Integrations Dashboard + + + -
- -
- - -
- -

What you get with Notion integration:

-
    -
  • Search through your Notion pages and databases
  • -
  • Access documents, wikis, and knowledge bases
  • -
  • Connect your team's knowledge directly to your search space
  • -
  • Keep your search results up-to-date with latest Notion content
  • -
  • Index your Notion documents for enhanced search capabilities
  • -
-
-
-
- - - - - Notion Connector Documentation - - Learn how to set up and use the Notion connector to index your workspace data. - - - -
-

How it works

-

- The Notion connector uses the Notion search API to fetch all pages that the connector has access to within a workspace. -

-
    -
  • For follow up indexing runs, the connector only retrieves pages that have been updated since the last indexing attempt.
  • -
  • Indexing is configured to run every 10 minutes, so page updates should appear within 10 minutes.
  • -
-
- - - - Authorization - - - - No Admin Access Required - - There's no requirement to be an Admin to share information with an integration. Any member can share pages and databases with it. - - - -
-
-

Step 1: Create an integration

-
    -
  1. Visit https://www.notion.com/my-integrations in your browser.
  2. -
  3. Click the + New integration button.
  4. -
  5. Name the integration (something like "Search Connector" could work).
  6. -
  7. Select "Read content" as the only capability required.
  8. -
  9. Click Submit to create the integration.
  10. -
  11. On the next page, you'll find your Notion integration token. Make a copy of it as you'll need it to configure the connector.
  12. -
-
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> -
-

Step 2: Share pages/databases with your integration

-

- To keep your information secure, integrations don't have access to any pages or databases in the workspace at first. - You must share specific pages with an integration in order for the connector to access those pages. -

-
    -
  1. Go to the page/database in your workspace.
  2. -
  3. Click the ••• on the top right corner of the page.
  4. -
  5. Scroll to the bottom of the pop-up and click Add connections.
  6. -
  7. Search for and select the new integration in the Search for connections... menu.
  8. -
  9. - Important: -
      -
    • If you've added a page, all child pages also become accessible.
    • -
    • If you've added a database, all rows (and their children) become accessible.
    • -
    -
  10. -
-
-
-
-
- - - Indexing - -
    -
  1. Navigate to the Connector Dashboard and select the Notion Connector.
  2. -
  3. Place the Integration Token under Step 1 Provide Credentials.
  4. -
  5. Click Connect to establish the connection.
  6. -
- - - - Indexing Behavior - - The Notion connector currently indexes everything it has access to. If you want to limit specific content being indexed, simply unshare the database from Notion with the integration. - - -
-
-
-
-
-
-
-
-
- ); + ( + + Notion Integration Token + + + + + Your Notion Integration Token will be encrypted and stored securely. It + typically starts with "ntn_". + + + + )} + /> + +
+ +
+ + +
+ +

What you get with Notion integration:

+
    +
  • Search through your Notion pages and databases
  • +
  • Access documents, wikis, and knowledge bases
  • +
  • Connect your team's knowledge directly to your search space
  • +
  • Keep your search results up-to-date with latest Notion content
  • +
  • Index your Notion documents for enhanced search capabilities
  • +
+
+
+
+ + + + + Notion Connector Documentation + + Learn how to set up and use the Notion connector to index your workspace data. + + + +
+

How it works

+

+ The Notion connector uses the Notion search API to fetch all pages that the + connector has access to within a workspace. +

+
    +
  • + For follow up indexing runs, the connector only retrieves pages that have been + updated since the last indexing attempt. +
  • +
  • + Indexing is configured to run every 10 minutes, so page + updates should appear within 10 minutes. +
  • +
+
+ + + + + Authorization + + + + + No Admin Access Required + + There's no requirement to be an Admin to share information with an + integration. Any member can share pages and databases with it. + + + +
+
+

Step 1: Create an integration

+
    +
  1. + Visit{" "} + + https://www.notion.com/my-integrations + {" "} + in your browser. +
  2. +
  3. + Click the + New integration button. +
  4. +
  5. + Name the integration (something like "Search Connector" could work). +
  6. +
  7. Select "Read content" as the only capability required.
  8. +
  9. + Click Submit to create the integration. +
  10. +
  11. + On the next page, you'll find your Notion integration token. Make a + copy of it as you'll need it to configure the connector. +
  12. +
+
+ +
+

+ Step 2: Share pages/databases with your integration +

+

+ To keep your information secure, integrations don't have access to any + pages or databases in the workspace at first. You must share specific + pages with an integration in order for the connector to access those + pages. +

+
    +
  1. Go to the page/database in your workspace.
  2. +
  3. + Click the ••• on the top right corner of the page. +
  4. +
  5. + Scroll to the bottom of the pop-up and click{" "} + Add connections. +
  6. +
  7. + Search for and select the new integration in the{" "} + Search for connections... menu. +
  8. +
  9. + Important: +
      +
    • + If you've added a page, all child pages also become accessible. +
    • +
    • + If you've added a database, all rows (and their children) become + accessible. +
    • +
    +
  10. +
+
+
+
+
+ + + Indexing + +
    +
  1. + Navigate to the Connector Dashboard and select the Notion{" "} + Connector. +
  2. +
  3. + Place the Integration Token under{" "} + Step 1 Provide Credentials. +
  4. +
  5. + Click Connect to establish the connection. +
  6. +
+ + + + Indexing Behavior + + The Notion connector currently indexes everything it has access to. If you + want to limit specific content being indexed, simply unshare the database + from Notion with the integration. + + +
+
+
+
+
+
+
+
+
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx index 3d0e59d9b..9384736bd 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/page.tsx @@ -1,31 +1,22 @@ "use client"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { - Card, - CardContent, - CardFooter, - CardHeader, -} from "@/components/ui/card"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - IconBrandDiscord, - IconBrandGithub, - IconBrandNotion, - IconBrandSlack, - IconBrandWindows, - IconBrandZoom, - IconChevronDown, - IconChevronRight, - IconMail, - IconWorldWww, - IconTicket, - IconLayoutKanban, - IconLinkPlus, + IconBrandDiscord, + IconBrandGithub, + IconBrandNotion, + IconBrandSlack, + IconBrandWindows, + IconBrandZoom, + IconChevronDown, + IconChevronRight, + IconMail, + IconWorldWww, + IconTicket, + IconLayoutKanban, + IconLinkPlus, } from "@tabler/icons-react"; import { AnimatePresence, motion } from "framer-motion"; import Link from "next/link"; @@ -34,366 +25,337 @@ import { useState } from "react"; // Define the Connector type interface Connector { - id: string; - title: string; - description: string; - icon: React.ReactNode; - status: "available" | "coming-soon" | "connected"; + id: string; + title: string; + description: string; + icon: React.ReactNode; + status: "available" | "coming-soon" | "connected"; } interface ConnectorCategory { - id: string; - title: string; - connectors: Connector[]; + id: string; + title: string; + connectors: Connector[]; } // Define connector categories and their connectors const connectorCategories: ConnectorCategory[] = [ - { - id: "search-engines", - title: "Search Engines", - connectors: [ - { - id: "tavily-api", - title: "Tavily API", - description: "Search the web using the Tavily API", - icon: , - status: "available", - }, - { - id: "linkup-api", - title: "Linkup API", - description: "Search the web using the Linkup API", - icon: , - status: "available", - }, - ], - }, - { - id: "team-chats", - title: "Team Chats", - connectors: [ - { - id: "slack-connector", - title: "Slack", - description: - "Connect to your Slack workspace to access messages and channels.", - icon: , - status: "available", - }, - { - id: "ms-teams", - title: "Microsoft Teams", - description: - "Connect to Microsoft Teams to access your team's conversations.", - icon: , - status: "coming-soon", - }, - { - id: "discord-connector", - title: "Discord", - description: - "Connect to Discord servers to access messages and channels.", - icon: , - status: "available", - }, - ], - }, - { - id: "project-management", - title: "Project Management", - connectors: [ - { - id: "linear-connector", - title: "Linear", - description: - "Connect to Linear to search issues, comments and project data.", - icon: , - status: "available", - }, - { - id: "jira-connector", - title: "Jira", - description: - "Connect to Jira to search issues, tickets and project data.", - icon: , - status: "available", - }, - ], - }, - { - id: "knowledge-bases", - title: "Knowledge Bases", - connectors: [ - { - id: "notion-connector", - title: "Notion", - description: - "Connect to your Notion workspace to access pages and databases.", - icon: , - status: "available", - }, - { - id: "github-connector", - title: "GitHub", - description: - "Connect a GitHub PAT to index code and docs from accessible repositories.", - icon: , - status: "available", - }, - ], - }, - { - id: "communication", - title: "Communication", - connectors: [ - { - id: "gmail", - title: "Gmail", - description: "Connect to your Gmail account to access emails.", - icon: , - status: "coming-soon", - }, - { - id: "zoom", - title: "Zoom", - description: - "Connect to Zoom to access meeting recordings and transcripts.", - icon: , - status: "coming-soon", - }, - ], - }, + { + id: "search-engines", + title: "Search Engines", + connectors: [ + { + id: "tavily-api", + title: "Tavily API", + description: "Search the web using the Tavily API", + icon: , + status: "available", + }, + { + id: "linkup-api", + title: "Linkup API", + description: "Search the web using the Linkup API", + icon: , + status: "available", + }, + ], + }, + { + id: "team-chats", + title: "Team Chats", + connectors: [ + { + id: "slack-connector", + title: "Slack", + description: "Connect to your Slack workspace to access messages and channels.", + icon: , + status: "available", + }, + { + id: "ms-teams", + title: "Microsoft Teams", + description: "Connect to Microsoft Teams to access your team's conversations.", + icon: , + status: "coming-soon", + }, + { + id: "discord-connector", + title: "Discord", + description: "Connect to Discord servers to access messages and channels.", + icon: , + status: "available", + }, + ], + }, + { + id: "project-management", + title: "Project Management", + connectors: [ + { + id: "linear-connector", + title: "Linear", + description: "Connect to Linear to search issues, comments and project data.", + icon: , + status: "available", + }, + { + id: "jira-connector", + title: "Jira", + description: "Connect to Jira to search issues, tickets and project data.", + icon: , + status: "available", + }, + ], + }, + { + id: "knowledge-bases", + title: "Knowledge Bases", + connectors: [ + { + id: "notion-connector", + title: "Notion", + description: "Connect to your Notion workspace to access pages and databases.", + icon: , + status: "available", + }, + { + id: "github-connector", + title: "GitHub", + description: "Connect a GitHub PAT to index code and docs from accessible repositories.", + icon: , + status: "available", + }, + ], + }, + { + id: "communication", + title: "Communication", + connectors: [ + { + id: "gmail", + title: "Gmail", + description: "Connect to your Gmail account to access emails.", + icon: , + status: "coming-soon", + }, + { + id: "zoom", + title: "Zoom", + description: "Connect to Zoom to access meeting recordings and transcripts.", + icon: , + status: "coming-soon", + }, + ], + }, ]; // Animation variants const fadeIn = { - hidden: { opacity: 0 }, - visible: { opacity: 1, transition: { duration: 0.4 } }, + hidden: { opacity: 0 }, + visible: { opacity: 1, transition: { duration: 0.4 } }, }; const staggerContainer = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - }, - }, + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, }; const cardVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { - type: "spring", - stiffness: 260, - damping: 20, - }, - }, - hover: { - scale: 1.02, - boxShadow: - "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", - transition: { - type: "spring", - stiffness: 400, - damping: 10, - }, - }, + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + type: "spring", + stiffness: 260, + damping: 20, + }, + }, + hover: { + scale: 1.02, + boxShadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)", + transition: { + type: "spring", + stiffness: 400, + damping: 10, + }, + }, }; export default function ConnectorsPage() { - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [expandedCategories, setExpandedCategories] = useState([ - "search-engines", - "knowledge-bases", - "project-management", - "team-chats", - ]); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [expandedCategories, setExpandedCategories] = useState([ + "search-engines", + "knowledge-bases", + "project-management", + "team-chats", + ]); - const toggleCategory = (categoryId: string) => { - setExpandedCategories((prev) => - prev.includes(categoryId) - ? prev.filter((id) => id !== categoryId) - : [...prev, categoryId], - ); - }; + const toggleCategory = (categoryId: string) => { + setExpandedCategories((prev) => + prev.includes(categoryId) ? prev.filter((id) => id !== categoryId) : [...prev, categoryId] + ); + }; - return ( -
- -

- Connect Your Tools -

-

- Integrate with your favorite services to enhance your research - capabilities. -

-
+ return ( +
+ +

+ Connect Your Tools +

+

+ Integrate with your favorite services to enhance your research capabilities. +

+
- - {connectorCategories.map((category) => ( - - toggleCategory(category.id)} - className="w-full" - > -
-

{category.title}

- - - -
+ + {connectorCategories.map((category) => ( + + toggleCategory(category.id)} + className="w-full" + > +
+

{category.title}

+ + + +
- - - - {category.connectors.map((connector) => ( - - - -
- - {connector.icon} - -
-
-
-

- {connector.title} -

- {connector.status === "coming-soon" && ( - - Coming soon - - )} - {connector.status === "connected" && ( - - Connected - - )} -
-
-
+ + + + {category.connectors.map((connector) => ( + + + +
+ + {connector.icon} + +
+
+
+

{connector.title}

+ {connector.status === "coming-soon" && ( + + Coming soon + + )} + {connector.status === "connected" && ( + + Connected + + )} +
+
+
- -

- {connector.description} -

-
+ +

{connector.description}

+
- - {connector.status === "available" && ( - - - - )} - {connector.status === "coming-soon" && ( - - )} - {connector.status === "connected" && ( - - )} - -
-
- ))} -
-
-
-
-
- ))} -
-
- ); + + {connector.status === "available" && ( + + + + )} + {connector.status === "coming-soon" && ( + + )} + {connector.status === "connected" && ( + + )} + + + + ))} + + + + + + ))} + +
+ ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx index 7ace10d6d..c67d8a4e4 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/serper-api/page.tsx @@ -11,197 +11,184 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; // Define the form schema with Zod const serperApiFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - api_key: z.string().min(10, { - message: "API key is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z.string().min(10, { + message: "API key is required and must be valid.", + }), }); // Define the type for the form values type SerperApiFormValues = z.infer; export default function SerperApiPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(serperApiFormSchema), - defaultValues: { - name: "Serper API Connector", - api_key: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(serperApiFormSchema), + defaultValues: { + name: "Serper API Connector", + api_key: "", + }, + }); - // Handle form submission - const onSubmit = async (values: SerperApiFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "SERPER_API", - config: { - SERPER_API_KEY: values.api_key, - }, - is_indexable: false, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: SerperApiFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "SERPER_API", + config: { + SERPER_API_KEY: values.api_key, + }, + is_indexable: false, + last_indexed_at: null, + }); - toast.success("Serper API connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Serper API connector created successfully!"); - return ( -
- + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - - - - Connect Serper API - - Integrate with Serper API to enhance your search capabilities with Google search results. - - - - - - API Key Required - - You'll need a Serper API key to use this connector. You can get one by signing up at{" "} - - serper.dev - - - + return ( +
+ -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect Serper API + + Integrate with Serper API to enhance your search capabilities with Google search + results. + + + + + + API Key Required + + You'll need a Serper API key to use this connector. You can get one by signing up at{" "} + + serper.dev + + + - ( - - Serper API Key - - - - - Your API key will be encrypted and stored securely. - - - - )} - /> + + + ( + + Connector Name + + + + A friendly name to identify this connector. + + + )} + /> -
- -
- - -
- -

What you get with Serper API:

-
    -
  • Access to Google search results directly in your research
  • -
  • Real-time information from the web
  • -
  • Enhanced search capabilities for your projects
  • -
-
-
-
-
- ); -} \ No newline at end of file + ( + + Serper API Key + + + + + Your API key will be encrypted and stored securely. + + + + )} + /> + +
+ +
+ + +
+ +

What you get with Serper API:

+
    +
  • Access to Google search results directly in your research
  • +
  • Real-time information from the web
  • +
  • Enhanced search capabilities for your projects
  • +
+
+
+
+
+ ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx index 3eea9d6e8..b108336ac 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/slack-connector/page.tsx @@ -11,260 +11,276 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // Define the form schema with Zod const slackConnectorFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - bot_token: z.string().min(10, { - message: "Bot User OAuth Token is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + bot_token: z.string().min(10, { + message: "Bot User OAuth Token is required and must be valid.", + }), }); // Define the type for the form values type SlackConnectorFormValues = z.infer; export default function SlackConnectorPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(slackConnectorFormSchema), - defaultValues: { - name: "Slack Connector", - bot_token: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(slackConnectorFormSchema), + defaultValues: { + name: "Slack Connector", + bot_token: "", + }, + }); - // Handle form submission - const onSubmit = async (values: SlackConnectorFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "SLACK_CONNECTOR", - config: { - SLACK_BOT_TOKEN: values.bot_token, - }, - is_indexable: true, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: SlackConnectorFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "SLACK_CONNECTOR", + config: { + SLACK_BOT_TOKEN: values.bot_token, + }, + is_indexable: true, + last_indexed_at: null, + }); - toast.success("Slack connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Slack connector created successfully!"); - return ( -
- + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - - - - Connect - Documentation - - - - - - Connect Slack Workspace - - Integrate with Slack to search and retrieve information from your workspace channels and conversations. This connector can index your Slack messages for search. - - - - - - Bot User OAuth Token Required - - You'll need a Slack Bot User OAuth Token to use this connector. You can create a Slack app and get the token from{" "} - - Slack API Dashboard - - - + return ( +
+ -
- - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect + Documentation + - ( - - Slack Bot User OAuth Token - - - - - Your Bot User OAuth Token will be encrypted and stored securely. It typically starts with "xoxb-". - - - - )} - /> + + + + Connect Slack Workspace + + Integrate with Slack to search and retrieve information from your workspace + channels and conversations. This connector can index your Slack messages for + search. + + + + + + Bot User OAuth Token Required + + You'll need a Slack Bot User OAuth Token to use this connector. You can create a + Slack app and get the token from{" "} + + Slack API Dashboard + + + -
- -
- - -
- -

What you get with Slack integration:

-
    -
  • Search through your Slack channels and conversations
  • -
  • Access historical messages and shared files
  • -
  • Connect your team's knowledge directly to your search space
  • -
  • Keep your search results up-to-date with latest communications
  • -
  • Index your Slack messages for enhanced search capabilities
  • -
-
-
-
- - - - - Slack Connector Documentation - - Learn how to set up and use the Slack connector to index your workspace data. - - - -
-

How it works

-

- The Slack connector indexes all public channels for a given workspace. -

-
    -
  • Upcoming: Support for private channels by tagging/adding the Slack Bot to private channels.
  • -
-
- - - - Authorization - - - - Admin Access Required - - You must be an admin of the Slack workspace to set up the connector. - - - -
    -
  1. Navigate and sign in to https://api.slack.com/apps.
  2. -
  3. - Create a new Slack app: -
      -
    • Click the Create New App button in the top right.
    • -
    • Select From an app manifest option.
    • -
    • Select the relevant workspace from the dropdown and click Next.
    • -
    -
  4. -
  5. - Select the "YAML" tab, paste the following manifest into the text box, and click Next: -
    -
    -{`display_information:
    +								
    + + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + ( + + Slack Bot User OAuth Token + + + + + Your Bot User OAuth Token will be encrypted and stored securely. It + typically starts with "xoxb-". + + + + )} + /> + +
    + +
    + + + + +

    What you get with Slack integration:

    +
      +
    • Search through your Slack channels and conversations
    • +
    • Access historical messages and shared files
    • +
    • Connect your team's knowledge directly to your search space
    • +
    • Keep your search results up-to-date with latest communications
    • +
    • Index your Slack messages for enhanced search capabilities
    • +
    +
    + + + + + + + Slack Connector Documentation + + Learn how to set up and use the Slack connector to index your workspace data. + + + +
    +

    How it works

    +

    + The Slack connector indexes all public channels for a given workspace. +

    +
      +
    • + Upcoming: Support for private channels by tagging/adding the Slack Bot to + private channels. +
    • +
    +
    + + + + + Authorization + + + + + Admin Access Required + + You must be an admin of the Slack workspace to set up the connector. + + + +
      +
    1. + Navigate and sign in to{" "} + + https://api.slack.com/apps + + . +
    2. +
    3. + Create a new Slack app: +
        +
      • + Click the Create New App button in the top right. +
      • +
      • + Select From an app manifest option. +
      • +
      • + Select the relevant workspace from the dropdown and click{" "} + Next. +
      • +
      +
    4. +
    5. + Select the "YAML" tab, paste the following manifest into the text box, and + click Next: +
      +
      +															{`display_information:
         name: SlackConnector
         description: ReadOnly Connector for indexing
       features:
      @@ -287,65 +303,94 @@ settings:
         org_deploy_enabled: false
         socket_mode_enabled: false
         token_rotation_enabled: false`}
      -                            
      -
      -
    6. -
    7. Click the Create button.
    8. -
    9. In the app page, navigate to the OAuth & Permissions tab under the Features header.
    10. -
    11. Copy the Bot User OAuth Token, this will be used to access Slack.
    12. -
    -
    -
    - - - Indexing - -
      -
    1. Navigate to the Connector Dashboard and select the Slack Connector.
    2. -
    3. Place the Bot User OAuth Token under Step 1 Provide Credentials.
    4. -
    5. Click Connect to establish the connection.
    6. -
    - - - - Important: Invite Bot to Channels - - After connecting, you must invite the bot to each channel you want to index. In each Slack channel, type: -
    /invite @YourBotName
    -

    Without this step, you'll get a "not_in_channel" error when the connector tries to access channel messages.

    -
    -
    - - - - First Indexing - - The first indexing pulls all of the public channels and takes longer than future updates. Only channels where the bot has been invited will be fully indexed. - - - -
    -

    Troubleshooting:

    -
      -
    • - not_in_channel error: If you see this error in logs, it means the bot hasn't been invited to a channel it's trying to access. Use the /invite @YourBotName command in that channel. -
    • -
    • - Alternative approach: You can add the chat:write.public scope to your Slack app to allow it to access public channels without an explicit invitation. -
    • -
    • - For private channels: The bot must always be invited using the /invite command. -
    • -
    -
    -
    -
    -
    -
    -
    -
    - - -
    - ); + +
+ +
  • + Click the Create button. +
  • +
  • + In the app page, navigate to the OAuth & Permissions tab + under the Features header. +
  • +
  • + Copy the Bot User OAuth Token, this will be used to + access Slack. +
  • + + + + + + Indexing + +
      +
    1. + Navigate to the Connector Dashboard and select the Slack{" "} + Connector. +
    2. +
    3. + Place the Bot User OAuth Token under{" "} + Step 1 Provide Credentials. +
    4. +
    5. + Click Connect to establish the connection. +
    6. +
    + + + + Important: Invite Bot to Channels + + After connecting, you must invite the bot to each channel you want to + index. In each Slack channel, type: +
    +														/invite @YourBotName
    +													
    +

    + Without this step, you'll get a "not_in_channel" error when the + connector tries to access channel messages. +

    +
    +
    + + + + First Indexing + + The first indexing pulls all of the public channels and takes longer than + future updates. Only channels where the bot has been invited will be fully + indexed. + + + +
    +

    Troubleshooting:

    +
      +
    • + not_in_channel error: If you see this error in logs, it + means the bot hasn't been invited to a channel it's trying to access. + Use the /invite @YourBotName command in that channel. +
    • +
    • + Alternative approach: You can add the{" "} + chat:write.public scope to your Slack app to allow it to + access public channels without an explicit invitation. +
    • +
    • + For private channels: The bot must always be invited + using the /invite command. +
    • +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx index 03ef3d160..61ea0283c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/connectors/add/tavily-api/page.tsx @@ -11,197 +11,184 @@ import { ArrowLeft, Check, Info, Loader2 } from "lucide-react"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; // Define the form schema with Zod const tavilyApiFormSchema = z.object({ - name: z.string().min(3, { - message: "Connector name must be at least 3 characters.", - }), - api_key: z.string().min(10, { - message: "API key is required and must be valid.", - }), + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z.string().min(10, { + message: "API key is required and must be valid.", + }), }); // Define the type for the form values type TavilyApiFormValues = z.infer; export default function TavilyApiPage() { - const router = useRouter(); - const params = useParams(); - const searchSpaceId = params.search_space_id as string; - const [isSubmitting, setIsSubmitting] = useState(false); - const { createConnector } = useSearchSourceConnectors(); + const router = useRouter(); + const params = useParams(); + const searchSpaceId = params.search_space_id as string; + const [isSubmitting, setIsSubmitting] = useState(false); + const { createConnector } = useSearchSourceConnectors(); - // Initialize the form - const form = useForm({ - resolver: zodResolver(tavilyApiFormSchema), - defaultValues: { - name: "Tavily API Connector", - api_key: "", - }, - }); + // Initialize the form + const form = useForm({ + resolver: zodResolver(tavilyApiFormSchema), + defaultValues: { + name: "Tavily API Connector", + api_key: "", + }, + }); - // Handle form submission - const onSubmit = async (values: TavilyApiFormValues) => { - setIsSubmitting(true); - try { - await createConnector({ - name: values.name, - connector_type: "TAVILY_API", - config: { - TAVILY_API_KEY: values.api_key, - }, - is_indexable: false, - last_indexed_at: null, - }); + // Handle form submission + const onSubmit = async (values: TavilyApiFormValues) => { + setIsSubmitting(true); + try { + await createConnector({ + name: values.name, + connector_type: "TAVILY_API", + config: { + TAVILY_API_KEY: values.api_key, + }, + is_indexable: false, + last_indexed_at: null, + }); - toast.success("Tavily API connector created successfully!"); - - // Navigate back to connectors page - router.push(`/dashboard/${searchSpaceId}/connectors`); - } catch (error) { - console.error("Error creating connector:", error); - toast.error(error instanceof Error ? error.message : "Failed to create connector"); - } finally { - setIsSubmitting(false); - } - }; + toast.success("Tavily API connector created successfully!"); - return ( -
    - + // Navigate back to connectors page + router.push(`/dashboard/${searchSpaceId}/connectors`); + } catch (error) { + console.error("Error creating connector:", error); + toast.error(error instanceof Error ? error.message : "Failed to create connector"); + } finally { + setIsSubmitting(false); + } + }; - - - - Connect Tavily API - - Integrate with Tavily API to enhance your search capabilities with AI-powered search results. - - - - - - API Key Required - - You'll need a Tavily API key to use this connector. You can get one by signing up at{" "} - - tavily.com - - - + return ( +
    + -
    - - ( - - Connector Name - - - - - A friendly name to identify this connector. - - - - )} - /> + + + + Connect Tavily API + + Integrate with Tavily API to enhance your search capabilities with AI-powered search + results. + + + + + + API Key Required + + You'll need a Tavily API key to use this connector. You can get one by signing up at{" "} + + tavily.com + + + - ( - - Tavily API Key - - - - - Your API key will be encrypted and stored securely. - - - - )} - /> + + + ( + + Connector Name + + + + A friendly name to identify this connector. + + + )} + /> -
    - -
    - - -
    - -

    What you get with Tavily API:

    -
      -
    • AI-powered search results tailored to your queries
    • -
    • Real-time information from the web
    • -
    • Enhanced search capabilities for your projects
    • -
    -
    -
    -
    -
    - ); -} \ No newline at end of file + ( + + Tavily API Key + + + + + Your API key will be encrypted and stored securely. + + + + )} + /> + +
    + +
    + + +
    + +

    What you get with Tavily API:

    +
      +
    • AI-powered search results tailored to your queries
    • +
    • Real-time information from the web
    • +
    • Enhanced search capabilities for your projects
    • +
    +
    +
    +
    +
    + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx index 1b66684db..9ed6dd265 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/page.tsx @@ -3,111 +3,96 @@ import { DocumentViewer } from "@/components/document-viewer"; import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Pagination, PaginationContent, PaginationItem } from "@/components/ui/pagination"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { - Pagination, - PaginationContent, - PaginationItem, -} from "@/components/ui/pagination"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@/components/ui/table"; import { useDocuments } from "@/hooks/use-documents"; import { cn } from "@/lib/utils"; import { - IconBrandDiscord, - IconBrandGithub, - IconBrandNotion, - IconBrandSlack, - IconBrandYoutube, - IconLayoutKanban, - IconTicket, + IconBrandDiscord, + IconBrandGithub, + IconBrandNotion, + IconBrandSlack, + IconBrandYoutube, + IconLayoutKanban, + IconTicket, } from "@tabler/icons-react"; import { - ColumnDef, - ColumnFiltersState, - FilterFn, - PaginationState, - Row, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, + type ColumnDef, + type ColumnFiltersState, + type FilterFn, + type PaginationState, + type Row, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, } from "@tanstack/react-table"; import { AnimatePresence, motion } from "framer-motion"; import { - AlertCircle, - ChevronDown, - ChevronFirst, - ChevronLast, - ChevronLeft, - ChevronRight, - ChevronUp, - CircleAlert, - CircleX, - Columns3, - File, - FileX, - Filter, - Globe, - ListFilter, - MoreHorizontal, - Trash, - Webhook, + AlertCircle, + ChevronDown, + ChevronFirst, + ChevronLast, + ChevronLeft, + ChevronRight, + ChevronUp, + CircleAlert, + CircleX, + Columns3, + File, + FileX, + Filter, + Globe, + ListFilter, + MoreHorizontal, + Trash, + Webhook, } from "lucide-react"; import { useParams } from "next/navigation"; -import React, { - useContext, - useEffect, - useId, - useMemo, - useRef, - useState, -} from "react"; +import React, { useContext, useEffect, useId, useMemo, useRef, useState } from "react"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; import rehypeSanitize from "rehype-sanitize"; @@ -116,1065 +101,945 @@ import { toast } from "sonner"; // Define animation variants for reuse const fadeInScale = { - hidden: { opacity: 0, scale: 0.95 }, - visible: { - opacity: 1, - scale: 1, - transition: { type: "spring", stiffness: 300, damping: 30 }, - }, - exit: { - opacity: 0, - scale: 0.95, - transition: { duration: 0.15 }, - }, + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { type: "spring", stiffness: 300, damping: 30 }, + }, + exit: { + opacity: 0, + scale: 0.95, + transition: { duration: 0.15 }, + }, }; type Document = { - id: number; - title: string; - document_type: - | "EXTENSION" - | "CRAWLED_URL" - | "SLACK_CONNECTOR" - | "NOTION_CONNECTOR" - | "FILE" - | "YOUTUBE_VIDEO" - | "LINEAR_CONNECTOR" - | "DISCORD_CONNECTOR"; - document_metadata: any; - content: string; - created_at: string; - search_space_id: number; + id: number; + title: string; + document_type: + | "EXTENSION" + | "CRAWLED_URL" + | "SLACK_CONNECTOR" + | "NOTION_CONNECTOR" + | "FILE" + | "YOUTUBE_VIDEO" + | "LINEAR_CONNECTOR" + | "DISCORD_CONNECTOR"; + document_metadata: any; + content: string; + created_at: string; + search_space_id: number; }; // Custom filter function for multi-column searching -const multiColumnFilterFn: FilterFn = ( - row, - columnId, - filterValue, -) => { - const searchableRowContent = `${row.original.title}`.toLowerCase(); - const searchTerm = (filterValue ?? "").toLowerCase(); - return searchableRowContent.includes(searchTerm); +const multiColumnFilterFn: FilterFn = (row, columnId, filterValue) => { + const searchableRowContent = `${row.original.title}`.toLowerCase(); + const searchTerm = (filterValue ?? "").toLowerCase(); + return searchableRowContent.includes(searchTerm); }; -const statusFilterFn: FilterFn = ( - row, - columnId, - filterValue: string[], -) => { - if (!filterValue?.length) return true; - const status = row.getValue(columnId) as string; - return filterValue.includes(status); +const statusFilterFn: FilterFn = (row, columnId, filterValue: string[]) => { + if (!filterValue?.length) return true; + const status = row.getValue(columnId) as string; + return filterValue.includes(status); }; // Add document type icons mapping const documentTypeIcons = { - EXTENSION: Webhook, - CRAWLED_URL: Globe, - SLACK_CONNECTOR: IconBrandSlack, - NOTION_CONNECTOR: IconBrandNotion, - FILE: File, - YOUTUBE_VIDEO: IconBrandYoutube, - GITHUB_CONNECTOR: IconBrandGithub, - LINEAR_CONNECTOR: IconLayoutKanban, - JIRA_CONNECTOR: IconTicket, - DISCORD_CONNECTOR: IconBrandDiscord, + EXTENSION: Webhook, + CRAWLED_URL: Globe, + SLACK_CONNECTOR: IconBrandSlack, + NOTION_CONNECTOR: IconBrandNotion, + FILE: File, + YOUTUBE_VIDEO: IconBrandYoutube, + GITHUB_CONNECTOR: IconBrandGithub, + LINEAR_CONNECTOR: IconLayoutKanban, + JIRA_CONNECTOR: IconTicket, + DISCORD_CONNECTOR: IconBrandDiscord, } as const; const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - size: 28, - enableSorting: false, - enableHiding: false, - }, - { - header: "Title", - accessorKey: "title", - cell: ({ row }) => { - const Icon = documentTypeIcons[row.original.document_type]; - return ( - - - {row.getValue("title")} - - ); - }, - size: 250, - }, - { - header: "Type", - accessorKey: "document_type", - cell: ({ row }) => { - const type = row.getValue( - "document_type", - ) as keyof typeof documentTypeIcons; - const Icon = documentTypeIcons[type]; - return ( -
    -
    - -
    - - {type - .split("_") - .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) - .join(" ")} - -
    - ); - }, - size: 180, - }, - { - header: "Content Summary", - accessorKey: "content", - cell: ({ row }) => { - const content = row.getValue("content") as string; - const title = row.getValue("title") as string; + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 28, + enableSorting: false, + enableHiding: false, + }, + { + header: "Title", + accessorKey: "title", + cell: ({ row }) => { + const Icon = documentTypeIcons[row.original.document_type]; + return ( + + + {row.getValue("title")} + + ); + }, + size: 250, + }, + { + header: "Type", + accessorKey: "document_type", + cell: ({ row }) => { + const type = row.getValue("document_type") as keyof typeof documentTypeIcons; + const Icon = documentTypeIcons[type]; + return ( +
    +
    + +
    + + {type + .split("_") + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(" ")} + +
    + ); + }, + size: 180, + }, + { + header: "Content Summary", + accessorKey: "content", + cell: ({ row }) => { + const content = row.getValue("content") as string; + const title = row.getValue("title") as string; - // Create a truncated preview (first 150 characters) - const previewContent = - content.length > 150 ? content.substring(0, 150) + "..." : content; + // Create a truncated preview (first 150 characters) + const previewContent = content.length > 150 ? content.substring(0, 150) + "..." : content; - return ( - - ); - }, - size: 300, - }, - { - header: "Created At", - accessorKey: "created_at", - cell: ({ row }) => { - const date = new Date(row.getValue("created_at")); - return date.toLocaleDateString(); - }, - size: 120, - }, - { - id: "actions", - header: () => Actions, - cell: ({ row }) => , - size: 60, - enableHiding: false, - }, + return ( + + ); + }, + size: 300, + }, + { + header: "Created At", + accessorKey: "created_at", + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return date.toLocaleDateString(); + }, + size: 120, + }, + { + id: "actions", + header: () => Actions, + cell: ({ row }) => , + size: 60, + enableHiding: false, + }, ]; // Create a context to share the deleteDocument function const DocumentsContext = React.createContext<{ - deleteDocument: (id: number) => Promise; - refreshDocuments: () => Promise; + deleteDocument: (id: number) => Promise; + refreshDocuments: () => Promise; } | null>(null); export default function DocumentsTable() { - const id = useId(); - const params = useParams(); - const searchSpaceId = Number(params.search_space_id); - const { documents, loading, error, refreshDocuments, deleteDocument } = - useDocuments(searchSpaceId); + const id = useId(); + const params = useParams(); + const searchSpaceId = Number(params.search_space_id); + const { documents, loading, error, refreshDocuments, deleteDocument } = + useDocuments(searchSpaceId); - // console.log("Search Space ID:", searchSpaceId); - // console.log("Documents loaded:", documents?.length); + // console.log("Search Space ID:", searchSpaceId); + // console.log("Documents loaded:", documents?.length); - useEffect(() => { - console.log("Delete document function available:", !!deleteDocument); - }, [deleteDocument]); + useEffect(() => { + console.log("Delete document function available:", !!deleteDocument); + }, [deleteDocument]); - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 10, - }); - const inputRef = useRef(null); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + const inputRef = useRef(null); - const [sorting, setSorting] = useState([ - { - id: "title", - desc: false, - }, - ]); + const [sorting, setSorting] = useState([ + { + id: "title", + desc: false, + }, + ]); - const [data, setData] = useState([]); + const [data, setData] = useState([]); - useEffect(() => { - if (documents) { - setData(documents); - } - }, [documents]); + useEffect(() => { + if (documents) { + setData(documents); + } + }, [documents]); - const handleDeleteRows = async () => { - const selectedRows = table.getSelectedRowModel().rows; - // console.log("Deleting selected rows:", selectedRows.length); + const handleDeleteRows = async () => { + const selectedRows = table.getSelectedRowModel().rows; + // console.log("Deleting selected rows:", selectedRows.length); - if (selectedRows.length === 0) { - toast.error("No rows selected"); - return; - } + if (selectedRows.length === 0) { + toast.error("No rows selected"); + return; + } - // Create an array of promises for each delete operation - const deletePromises = selectedRows.map((row) => { - // console.log("Deleting row with ID:", row.original.id); - return deleteDocument(row.original.id); - }); + // Create an array of promises for each delete operation + const deletePromises = selectedRows.map((row) => { + // console.log("Deleting row with ID:", row.original.id); + return deleteDocument(row.original.id); + }); - try { - // Execute all delete operations - const results = await Promise.all(deletePromises); - // console.log("Delete results:", results); + try { + // Execute all delete operations + const results = await Promise.all(deletePromises); + // console.log("Delete results:", results); - // Check if all deletions were successful - const allSuccessful = results.every((result) => result === true); + // Check if all deletions were successful + const allSuccessful = results.every((result) => result === true); - if (allSuccessful) { - toast.success( - `Successfully deleted ${selectedRows.length} document(s)`, - ); - } else { - toast.error("Some documents could not be deleted"); - } + if (allSuccessful) { + toast.success(`Successfully deleted ${selectedRows.length} document(s)`); + } else { + toast.error("Some documents could not be deleted"); + } - // Refresh the documents list after all deletions - await refreshDocuments(); - table.resetRowSelection(); - } catch (error: any) { - console.error("Error deleting documents:", error); - toast.error("Error deleting documents"); - } - }; + // Refresh the documents list after all deletions + await refreshDocuments(); + table.resetRowSelection(); + } catch (error: any) { + console.error("Error deleting documents:", error); + toast.error("Error deleting documents"); + } + }; - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - onSortingChange: setSorting, - enableSortingRemoval: false, - getPaginationRowModel: getPaginationRowModel(), - onPaginationChange: setPagination, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getFilteredRowModel: getFilteredRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - pagination, - columnFilters, - columnVisibility, - }, - }); + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + enableSortingRemoval: false, + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getFilteredRowModel: getFilteredRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + state: { + sorting, + pagination, + columnFilters, + columnVisibility, + }, + }); - // Get unique status values - const uniqueStatusValues = useMemo(() => { - const statusColumn = table.getColumn("document_type"); + // Get unique status values + const uniqueStatusValues = useMemo(() => { + const statusColumn = table.getColumn("document_type"); - if (!statusColumn) return []; + if (!statusColumn) return []; - const values = Array.from(statusColumn.getFacetedUniqueValues().keys()); + const values = Array.from(statusColumn.getFacetedUniqueValues().keys()); - return values.sort(); - }, [table.getColumn("document_type")?.getFacetedUniqueValues()]); + return values.sort(); + }, [table.getColumn("document_type")?.getFacetedUniqueValues()]); - // Get counts for each status - const statusCounts = useMemo(() => { - const statusColumn = table.getColumn("document_type"); - if (!statusColumn) return new Map(); - return statusColumn.getFacetedUniqueValues(); - }, [table.getColumn("document_type")?.getFacetedUniqueValues()]); + // Get counts for each status + const statusCounts = useMemo(() => { + const statusColumn = table.getColumn("document_type"); + if (!statusColumn) return new Map(); + return statusColumn.getFacetedUniqueValues(); + }, [table.getColumn("document_type")?.getFacetedUniqueValues()]); - const selectedStatuses = useMemo(() => { - const filterValue = table - .getColumn("document_type") - ?.getFilterValue() as string[]; - return filterValue ?? []; - }, [table.getColumn("document_type")?.getFilterValue()]); + const selectedStatuses = useMemo(() => { + const filterValue = table.getColumn("document_type")?.getFilterValue() as string[]; + return filterValue ?? []; + }, [table.getColumn("document_type")?.getFilterValue()]); - const handleStatusChange = (checked: boolean, value: string) => { - const filterValue = table - .getColumn("document_type") - ?.getFilterValue() as string[]; - const newFilterValue = filterValue ? [...filterValue] : []; + const handleStatusChange = (checked: boolean, value: string) => { + const filterValue = table.getColumn("document_type")?.getFilterValue() as string[]; + const newFilterValue = filterValue ? [...filterValue] : []; - if (checked) { - newFilterValue.push(value); - } else { - const index = newFilterValue.indexOf(value); - if (index > -1) { - newFilterValue.splice(index, 1); - } - } + if (checked) { + newFilterValue.push(value); + } else { + const index = newFilterValue.indexOf(value); + if (index > -1) { + newFilterValue.splice(index, 1); + } + } - table - .getColumn("document_type") - ?.setFilterValue(newFilterValue.length ? newFilterValue : undefined); - }; + table + .getColumn("document_type") + ?.setFilterValue(newFilterValue.length ? newFilterValue : undefined); + }; - return ( - Promise.resolve(false)), - refreshDocuments: refreshDocuments || (() => Promise.resolve()), - }} - > - - {/* Filters */} - -
    - {/* Filter by name or email */} - - - table.getColumn("title")?.setFilterValue(e.target.value) - } - placeholder="Filter by title..." - type="text" - aria-label="Filter by title" - /> - - - {Boolean(table.getColumn("title")?.getFilterValue()) && ( - { - table.getColumn("title")?.setFilterValue(""); - if (inputRef.current) { - inputRef.current.focus(); - } - }} - initial={{ opacity: 0, rotate: -90 }} - animate={{ opacity: 1, rotate: 0 }} - exit={{ opacity: 0, rotate: 90 }} - whileHover={{ scale: 1.1 }} - whileTap={{ scale: 0.9 }} - > - - )} - - {/* Filter by status */} - - - - - - - - -
    -
    - Filters -
    -
    - - {uniqueStatusValues.map((value, i) => ( - - - handleStatusChange(checked, value) - } - /> - - - ))} - -
    -
    -
    -
    -
    - {/* Toggle columns visibility */} - - - - - - - - - Toggle columns - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - onSelect={(event) => event.preventDefault()} - > - {column.id} - - ); - })} - - - -
    -
    - {/* Delete button */} - {table.getSelectedRowModel().rows.length > 0 && ( - - - - - -
    - - - - Are you absolutely sure? - - - This action cannot be undone. This will permanently - delete {table.getSelectedRowModel().rows.length}{" "} - selected{" "} - {table.getSelectedRowModel().rows.length === 1 - ? "row" - : "rows"} - . - - -
    - - Cancel - - Delete - - -
    -
    - )} - {/* Add user button */} - {/* + + + + +
    +
    Filters
    +
    + + {uniqueStatusValues.map((value, i) => ( + + + handleStatusChange(checked, value) + } + /> + + + ))} + +
    +
    +
    +
    + + {/* Toggle columns visibility */} + + + + + + + + + Toggle columns + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + onSelect={(event) => event.preventDefault()} + > + {column.id} + + ); + })} + + + +
    +
    + {/* Delete button */} + {table.getSelectedRowModel().rows.length > 0 && ( + + + + + +
    + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete{" "} + {table.getSelectedRowModel().rows.length} selected{" "} + {table.getSelectedRowModel().rows.length === 1 ? "row" : "rows"}. + + +
    + + Cancel + Delete + +
    +
    + )} + {/* Add user button */} + {/* */} -
    -
    +
    + - {/* Table */} - - {loading ? ( -
    -
    -
    -

    - Loading documents... -

    -
    -
    - ) : error ? ( -
    -
    - -

    - Error loading documents -

    - -
    -
    - ) : data.length === 0 ? ( -
    -
    - -

    - No documents found -

    -
    -
    - ) : ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : header.column.getCanSort() ? ( -
    { - // Enhanced keyboard handling for sorting - if ( - header.column.getCanSort() && - (e.key === "Enter" || e.key === " ") - ) { - e.preventDefault(); - header.column.getToggleSortingHandler()?.(e); - } - }} - tabIndex={ - header.column.getCanSort() ? 0 : undefined - } - > - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - {{ - asc: ( -
    - ) : ( - flexRender( - header.column.columnDef.header, - header.getContext(), - ) - )} -
    - ); - })} -
    - ))} -
    - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row, index) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No documents found. - - - )} - - -
    - )} -
    + {/* Table */} + + {loading ? ( +
    +
    +
    +

    Loading documents...

    +
    +
    + ) : error ? ( +
    +
    + +

    Error loading documents

    + +
    +
    + ) : data.length === 0 ? ( +
    +
    + +

    No documents found

    +
    +
    + ) : ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : header.column.getCanSort() ? ( +
    { + // Enhanced keyboard handling for sorting + if ( + header.column.getCanSort() && + (e.key === "Enter" || e.key === " ") + ) { + e.preventDefault(); + header.column.getToggleSortingHandler()?.(e); + } + }} + tabIndex={header.column.getCanSort() ? 0 : undefined} + > + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ( +
    + ) : ( + flexRender(header.column.columnDef.header, header.getContext()) + )} +
    + ); + })} +
    + ))} +
    + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row, index) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No documents found. + + + )} + + +
    + )} +
    - {/* Pagination */} -
    - {/* Results per page */} - - - - - {/* Page number information */} - -

    - - {table.getState().pagination.pageIndex * - table.getState().pagination.pageSize + - 1} - - - {Math.min( - Math.max( - table.getState().pagination.pageIndex * - table.getState().pagination.pageSize + - table.getState().pagination.pageSize, - 0, - ), - table.getRowCount(), - )} - {" "} - of{" "} - - {table.getRowCount().toString()} - -

    -
    + {/* Pagination */} +
    + {/* Results per page */} + + + + + {/* Page number information */} + +

    + + {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- + {Math.min( + Math.max( + table.getState().pagination.pageIndex * table.getState().pagination.pageSize + + table.getState().pagination.pageSize, + 0 + ), + table.getRowCount() + )} + {" "} + of {table.getRowCount().toString()} +

    +
    - {/* Pagination buttons */} -
    - - - {/* First page button */} - - - - - - {/* Previous page button */} - - - - - - {/* Next page button */} - - - - - - {/* Last page button */} - - - - - - - -
    -
    - - - ); + {/* Pagination buttons */} +
    + + + {/* First page button */} + + + + + + {/* Previous page button */} + + + + + + {/* Next page button */} + + + + + + {/* Last page button */} + + + + + + + +
    +
    + + + ); } function RowActions({ row }: { row: Row }) { - const [isOpen, setIsOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const { deleteDocument, refreshDocuments } = useContext(DocumentsContext)!; - const document = row.original; + const [isOpen, setIsOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const { deleteDocument, refreshDocuments } = useContext(DocumentsContext)!; + const document = row.original; - const handleDelete = async () => { - setIsDeleting(true); - try { - await deleteDocument(document.id); - toast.success("Document deleted successfully"); - await refreshDocuments(); - } catch (error) { - console.error("Error deleting document:", error); - toast.error("Failed to delete document"); - } finally { - setIsDeleting(false); - setIsOpen(false); - } - }; + const handleDelete = async () => { + setIsDeleting(true); + try { + await deleteDocument(document.id); + toast.success("Document deleted successfully"); + await refreshDocuments(); + } catch (error) { + console.error("Error deleting document:", error); + toast.error("Failed to delete document"); + } finally { + setIsDeleting(false); + setIsOpen(false); + } + }; - return ( -
    - - - - - - e.preventDefault()}> - View Metadata - - } - /> - - - - { - e.preventDefault(); - setIsOpen(true); - }} - > - Delete - - - - - Are you sure? - - This action cannot be undone. This will permanently delete the - document. - - - - Cancel - { - e.preventDefault(); - handleDelete(); - }} - disabled={isDeleting} - > - {isDeleting ? "Deleting..." : "Delete"} - - - - - - -
    - ); + return ( +
    + + + + + + e.preventDefault()}> + View Metadata + + } + /> + + + + { + e.preventDefault(); + setIsOpen(true); + }} + > + Delete + + + + + Are you sure? + + This action cannot be undone. This will permanently delete the document. + + + + Cancel + { + e.preventDefault(); + handleDelete(); + }} + disabled={isDeleting} + > + {isDeleting ? "Deleting..." : "Delete"} + + + + + + +
    + ); } export { DocumentsTable }; diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx index 302c14976..40d71d8a1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx @@ -1,535 +1,580 @@ -"use client" +"use client"; -import { useState, useCallback, useRef } from "react" -import { useDropzone } from "react-dropzone" -import { Button } from "@/components/ui/button" -import { toast } from "sonner" -import { X, Upload, Tag, CheckCircle2, Calendar, FileType } from "lucide-react" -import { useRouter, useParams } from "next/navigation" -import { motion, AnimatePresence } from "framer-motion" +import { useState, useCallback, useRef } from "react"; +import { useDropzone } from "react-dropzone"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { X, Upload, Tag, CheckCircle2, Calendar, FileType } from "lucide-react"; +import { useRouter, useParams } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; // Grid pattern component inspired by Aceternity UI function GridPattern() { - const columns = 41; - const rows = 11; - return ( -
    - {Array.from({ length: rows }).map((_, row) => - Array.from({ length: columns }).map((_, col) => { - const index = row * columns + col; - return ( -
    - ); - }) - )} -
    - ); + const columns = 41; + const rows = 11; + return ( +
    + {Array.from({ length: rows }).map((_, row) => + Array.from({ length: columns }).map((_, col) => { + const index = row * columns + col; + return ( +
    + ); + }) + )} +
    + ); } export default function FileUploader() { - // Use the useParams hook to get the params - const params = useParams(); - const search_space_id = params.search_space_id as string; + // Use the useParams hook to get the params + const params = useParams(); + const search_space_id = params.search_space_id as string; - const [files, setFiles] = useState([]) - const [isUploading, setIsUploading] = useState(false) - const router = useRouter(); - const fileInputRef = useRef(null); + const [files, setFiles] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const router = useRouter(); + const fileInputRef = useRef(null); - // Audio files are always supported (using whisper) - const audioFileTypes = { - 'audio/mpeg': ['.mp3', '.mpeg', '.mpga'], - 'audio/mp4': ['.mp4', '.m4a'], - 'audio/wav': ['.wav'], - 'audio/webm': ['.webm'], - 'text/markdown': ['.md', '.markdown'], - 'text/plain': ['.txt'], - }; + // Audio files are always supported (using whisper) + const audioFileTypes = { + "audio/mpeg": [".mp3", ".mpeg", ".mpga"], + "audio/mp4": [".mp4", ".m4a"], + "audio/wav": [".wav"], + "audio/webm": [".webm"], + "text/markdown": [".md", ".markdown"], + "text/plain": [".txt"], + }; - // Conditionally set accepted file types based on ETL service - const getAcceptedFileTypes = () => { - const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE; - - if (etlService === 'LLAMACLOUD') { - return { - // LlamaCloud supported file types - 'application/pdf': ['.pdf'], - 'application/msword': ['.doc'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'application/vnd.ms-word.document.macroEnabled.12': ['.docm'], - 'application/msword-template': ['.dot'], - 'application/vnd.ms-word.template.macroEnabled.12': ['.dotm'], - 'application/vnd.ms-powerpoint': ['.ppt'], - 'application/vnd.ms-powerpoint.template.macroEnabled.12': ['.pptm'], - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], - 'application/vnd.ms-powerpoint.template': ['.pot'], - 'application/vnd.openxmlformats-officedocument.presentationml.template': ['.potx'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - 'application/vnd.ms-excel': ['.xls'], - 'application/vnd.ms-excel.sheet.macroEnabled.12': ['.xlsm'], - 'application/vnd.ms-excel.sheet.binary.macroEnabled.12': ['.xlsb'], - 'application/vnd.ms-excel.workspace': ['.xlw'], - 'application/rtf': ['.rtf'], - 'application/xml': ['.xml'], - 'application/epub+zip': ['.epub'], - 'application/vnd.apple.keynote': ['.key'], - 'application/vnd.apple.pages': ['.pages'], - 'application/vnd.apple.numbers': ['.numbers'], - 'application/vnd.wordperfect': ['.wpd'], - 'application/vnd.oasis.opendocument.text': ['.odt'], - 'application/vnd.oasis.opendocument.presentation': ['.odp'], - 'application/vnd.oasis.opendocument.graphics': ['.odg'], - 'application/vnd.oasis.opendocument.spreadsheet': ['.ods'], - 'application/vnd.oasis.opendocument.formula': ['.fods'], - 'text/csv': ['.csv'], - 'text/tab-separated-values': ['.tsv'], - 'text/html': ['.html', '.htm', '.web'], - 'image/jpeg': ['.jpg', '.jpeg'], - 'image/png': ['.png'], - 'image/gif': ['.gif'], - 'image/bmp': ['.bmp'], - 'image/svg+xml': ['.svg'], - 'image/tiff': ['.tiff'], - 'image/webp': ['.webp'], - 'application/dbase': ['.dbf'], - 'application/vnd.lotus-1-2-3': ['.123'], - 'text/x-web-markdown': ['.602', '.abw', '.cgm', '.cwk', '.hwp', '.lwp', '.mw', '.mcw', '.pbd', '.sda', '.sdd', '.sdp', '.sdw', '.sgl', '.sti', '.sxi', '.sxw', '.stw', '.sxg', '.uof', '.uop', '.uot', '.vor', '.wps', '.zabw'], - 'text/x-spreadsheet': ['.dif', '.sylk', '.slk', '.prn', '.et', '.uos1', '.uos2', '.wk1', '.wk2', '.wk3', '.wk4', '.wks', '.wq1', '.wq2', '.wb1', '.wb2', '.wb3', '.qpw', '.xlr', '.eth'], - // Audio files (always supported) - ...audioFileTypes, - }; - } else if (etlService === 'DOCLING') { - return { - // Docling supported file types - 'application/pdf': ['.pdf'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], - 'text/asciidoc': ['.adoc', '.asciidoc'], - 'text/html': ['.html', '.htm', '.xhtml'], - 'text/csv': ['.csv'], - 'image/png': ['.png'], - 'image/jpeg': ['.jpg', '.jpeg'], - 'image/tiff': ['.tiff', '.tif'], - 'image/bmp': ['.bmp'], - 'image/webp': ['.webp'], - // Audio files (always supported) - ...audioFileTypes, - }; - } else { - return { - // Unstructured supported file types - 'image/bmp': ['.bmp'], - 'text/csv': ['.csv'], - 'application/msword': ['.doc'], - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], - 'message/rfc822': ['.eml'], - 'application/epub+zip': ['.epub'], - 'image/heic': ['.heic'], - 'text/html': ['.html'], - 'image/jpeg': ['.jpeg', '.jpg'], - 'image/png': ['.png'], - 'application/vnd.ms-outlook': ['.msg'], - 'application/vnd.oasis.opendocument.text': ['.odt'], - 'text/x-org': ['.org'], - 'application/pkcs7-signature': ['.p7s'], - 'application/pdf': ['.pdf'], - 'application/vnd.ms-powerpoint': ['.ppt'], - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], - 'text/x-rst': ['.rst'], - 'application/rtf': ['.rtf'], - 'image/tiff': ['.tiff'], - 'text/tab-separated-values': ['.tsv'], - 'application/vnd.ms-excel': ['.xls'], - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - 'application/xml': ['.xml'], - // Audio files (always supported) - ...audioFileTypes, - }; - } - }; + // Conditionally set accepted file types based on ETL service + const getAcceptedFileTypes = () => { + const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE; - const acceptedFileTypes = getAcceptedFileTypes(); + if (etlService === "LLAMACLOUD") { + return { + // LlamaCloud supported file types + "application/pdf": [".pdf"], + "application/msword": [".doc"], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"], + "application/vnd.ms-word.document.macroEnabled.12": [".docm"], + "application/msword-template": [".dot"], + "application/vnd.ms-word.template.macroEnabled.12": [".dotm"], + "application/vnd.ms-powerpoint": [".ppt"], + "application/vnd.ms-powerpoint.template.macroEnabled.12": [".pptm"], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"], + "application/vnd.ms-powerpoint.template": [".pot"], + "application/vnd.openxmlformats-officedocument.presentationml.template": [".potx"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], + "application/vnd.ms-excel": [".xls"], + "application/vnd.ms-excel.sheet.macroEnabled.12": [".xlsm"], + "application/vnd.ms-excel.sheet.binary.macroEnabled.12": [".xlsb"], + "application/vnd.ms-excel.workspace": [".xlw"], + "application/rtf": [".rtf"], + "application/xml": [".xml"], + "application/epub+zip": [".epub"], + "application/vnd.apple.keynote": [".key"], + "application/vnd.apple.pages": [".pages"], + "application/vnd.apple.numbers": [".numbers"], + "application/vnd.wordperfect": [".wpd"], + "application/vnd.oasis.opendocument.text": [".odt"], + "application/vnd.oasis.opendocument.presentation": [".odp"], + "application/vnd.oasis.opendocument.graphics": [".odg"], + "application/vnd.oasis.opendocument.spreadsheet": [".ods"], + "application/vnd.oasis.opendocument.formula": [".fods"], + "text/csv": [".csv"], + "text/tab-separated-values": [".tsv"], + "text/html": [".html", ".htm", ".web"], + "image/jpeg": [".jpg", ".jpeg"], + "image/png": [".png"], + "image/gif": [".gif"], + "image/bmp": [".bmp"], + "image/svg+xml": [".svg"], + "image/tiff": [".tiff"], + "image/webp": [".webp"], + "application/dbase": [".dbf"], + "application/vnd.lotus-1-2-3": [".123"], + "text/x-web-markdown": [ + ".602", + ".abw", + ".cgm", + ".cwk", + ".hwp", + ".lwp", + ".mw", + ".mcw", + ".pbd", + ".sda", + ".sdd", + ".sdp", + ".sdw", + ".sgl", + ".sti", + ".sxi", + ".sxw", + ".stw", + ".sxg", + ".uof", + ".uop", + ".uot", + ".vor", + ".wps", + ".zabw", + ], + "text/x-spreadsheet": [ + ".dif", + ".sylk", + ".slk", + ".prn", + ".et", + ".uos1", + ".uos2", + ".wk1", + ".wk2", + ".wk3", + ".wk4", + ".wks", + ".wq1", + ".wq2", + ".wb1", + ".wb2", + ".wb3", + ".qpw", + ".xlr", + ".eth", + ], + // Audio files (always supported) + ...audioFileTypes, + }; + } else if (etlService === "DOCLING") { + return { + // Docling supported file types + "application/pdf": [".pdf"], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"], + "text/asciidoc": [".adoc", ".asciidoc"], + "text/html": [".html", ".htm", ".xhtml"], + "text/csv": [".csv"], + "image/png": [".png"], + "image/jpeg": [".jpg", ".jpeg"], + "image/tiff": [".tiff", ".tif"], + "image/bmp": [".bmp"], + "image/webp": [".webp"], + // Audio files (always supported) + ...audioFileTypes, + }; + } else { + return { + // Unstructured supported file types + "image/bmp": [".bmp"], + "text/csv": [".csv"], + "application/msword": [".doc"], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"], + "message/rfc822": [".eml"], + "application/epub+zip": [".epub"], + "image/heic": [".heic"], + "text/html": [".html"], + "image/jpeg": [".jpeg", ".jpg"], + "image/png": [".png"], + "application/vnd.ms-outlook": [".msg"], + "application/vnd.oasis.opendocument.text": [".odt"], + "text/x-org": [".org"], + "application/pkcs7-signature": [".p7s"], + "application/pdf": [".pdf"], + "application/vnd.ms-powerpoint": [".ppt"], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"], + "text/x-rst": [".rst"], + "application/rtf": [".rtf"], + "image/tiff": [".tiff"], + "text/tab-separated-values": [".tsv"], + "application/vnd.ms-excel": [".xls"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], + "application/xml": [".xml"], + // Audio files (always supported) + ...audioFileTypes, + }; + } + }; - const supportedExtensions = Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort() + const acceptedFileTypes = getAcceptedFileTypes(); - const onDrop = useCallback((acceptedFiles: File[]) => { - setFiles((prevFiles) => [...prevFiles, ...acceptedFiles]) - }, []) + const supportedExtensions = Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort(); - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, - accept: acceptedFileTypes, - maxSize: 50 * 1024 * 1024, // 50MB - }) + const onDrop = useCallback((acceptedFiles: File[]) => { + setFiles((prevFiles) => [...prevFiles, ...acceptedFiles]); + }, []); - const handleClick = () => { - fileInputRef.current?.click(); - }; + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: acceptedFileTypes, + maxSize: 50 * 1024 * 1024, // 50MB + }); - const removeFile = (index: number) => { - setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)) - } + const handleClick = () => { + fileInputRef.current?.click(); + }; - const formatFileSize = (bytes: number) => { - if (bytes === 0) return "0 Bytes" - const k = 1024 - const sizes = ["Bytes", "KB", "MB", "GB", "TB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] - } + const removeFile = (index: number) => { + setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); + }; - const handleUpload = async () => { - setIsUploading(true) + const formatFileSize = (bytes: number) => { + if (bytes === 0) return "0 Bytes"; + const k = 1024; + const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / k ** i).toFixed(2)) + " " + sizes[i]; + }; - const formData = new FormData() - files.forEach((file) => { - formData.append("files", file) - }) + const handleUpload = async () => { + setIsUploading(true); - formData.append('search_space_id', search_space_id) + const formData = new FormData(); + files.forEach((file) => { + formData.append("files", file); + }); - try { - // toast("File Upload", { - // description: "Files Uploading Initiated", - // }) + formData.append("search_space_id", search_space_id); - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL!}/api/v1/documents/fileupload`, { - method: "POST", - headers: { - 'Authorization': `Bearer ${window.localStorage.getItem("surfsense_bearer_token")}` - }, - body: formData, - }) + try { + // toast("File Upload", { + // description: "Files Uploading Initiated", + // }) - if (!response.ok) { - throw new Error("Upload failed") - } + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL!}/api/v1/documents/fileupload`, + { + method: "POST", + headers: { + Authorization: `Bearer ${window.localStorage.getItem("surfsense_bearer_token")}`, + }, + body: formData, + } + ); - await response.json() + if (!response.ok) { + throw new Error("Upload failed"); + } - toast("Upload Task Initiated", { - description: "Files Uploading Initiated", - }) + await response.json(); - router.push(`/dashboard/${search_space_id}/documents`); - } catch (error: any) { - setIsUploading(false) - toast("Upload Error", { - description: `Error uploading files: ${error.message}`, - }) - } - } + toast("Upload Task Initiated", { + description: "Files Uploading Initiated", + }); - const mainVariant = { - initial: { - x: 0, - y: 0, - }, - animate: { - x: 20, - y: -20, - opacity: 0.9, - }, - }; + router.push(`/dashboard/${search_space_id}/documents`); + } catch (error: any) { + setIsUploading(false); + toast("Upload Error", { + description: `Error uploading files: ${error.message}`, + }); + } + }; - const secondaryVariant = { - initial: { - opacity: 0, - }, - animate: { - opacity: 1, - }, - }; + const mainVariant = { + initial: { + x: 0, + y: 0, + }, + animate: { + x: 20, + y: -20, + opacity: 0.9, + }, + }; - const containerVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.5, - when: "beforeChildren", - staggerChildren: 0.1 - } - } - }; + const secondaryVariant = { + initial: { + opacity: 0, + }, + animate: { + opacity: 1, + }, + }; - const itemVariants = { - hidden: { opacity: 0, y: 10 }, - visible: { opacity: 1, y: 0, transition: { duration: 0.3 } } - }; + const containerVariants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + when: "beforeChildren", + staggerChildren: 0.1, + }, + }, + }; - const fileItemVariants = { - hidden: { opacity: 0, x: -20 }, - visible: { opacity: 1, x: 0, transition: { duration: 0.3 } }, - exit: { opacity: 0, x: 20, transition: { duration: 0.2 } } - }; + const itemVariants = { + hidden: { opacity: 0, y: 10 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.3 } }, + }; - return ( -
    - + const fileItemVariants = { + hidden: { opacity: 0, x: -20 }, + visible: { opacity: 1, x: 0, transition: { duration: 0.3 } }, + exit: { opacity: 0, x: 20, transition: { duration: 0.2 } }, + }; - - - {/* Grid background pattern */} -
    - -
    + return ( +
    + + + + {/* Grid background pattern */} +
    + +
    -
    - {/* Dropzone area */} -
    - +
    + {/* Dropzone area */} +
    + -

    - Upload files -

    -

    - Drag or drop your files here or click to upload -

    +

    + Upload files +

    +

    + Drag or drop your files here or click to upload +

    -
    - {!files.length && ( - - {isDragActive ? ( - - Drop it - - - ) : ( - - )} - - )} +
    + {!files.length && ( + + {isDragActive ? ( + + Drop it + + + ) : ( + + )} + + )} - {!files.length && ( - - )} -
    -
    -
    - + {!files.length && ( + + )} +
    +
    +
    +
    - {/* File list section */} - - {files.length > 0 && ( - -
    -

    Selected Files ({files.length})

    - -
    + // Force a re-render after animation completes + setTimeout(() => { + const event = new Event("resize"); + window.dispatchEvent(event); + }, 350); + }} + disabled={isUploading} + > + Clear all + +
    -
    - - {files.map((file, index) => ( - -
    - - {file.name} - -
    - - {formatFileSize(file.size)} - - -
    -
    +
    + + {files.map((file, index) => ( + +
    + + {file.name} + +
    + + {formatFileSize(file.size)} + + +
    +
    -
    - - - {file.type || 'Unknown type'} - +
    + + + {file.type || "Unknown type"} + - - - modified {new Date(file.lastModified).toLocaleDateString()} - -
    - - ))} - -
    + + + modified {new Date(file.lastModified).toLocaleDateString()} + +
    +
    + ))} +
    +
    - - - -
    - )} - + + + +
    + )} + - {/* File type information */} - -
    -
    - -

    Supported file types:

    -
    -
    - {supportedExtensions.map((ext) => ( - - {ext} - - ))} -
    -
    -
    -
    - + {/* File type information */} + +
    +
    + +

    Supported file types:

    +
    +
    + {supportedExtensions.map((ext) => ( + + {ext} + + ))} +
    +
    +
    + + - -
    - ) +
    + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/webpage/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/webpage/page.tsx index 60571d7d0..24205410c 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/webpage/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/webpage/page.tsx @@ -1,11 +1,18 @@ "use client"; -import { useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import { Tag, TagInput } from "emblor"; +import { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { type Tag, TagInput } from "emblor"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { toast } from "sonner"; import { Globe, Loader2 } from "lucide-react"; @@ -13,188 +20,182 @@ import { Globe, Loader2 } from "lucide-react"; const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/; export default function WebpageCrawler() { - const params = useParams(); - const router = useRouter(); - const search_space_id = params.search_space_id as string; - - const [urlTags, setUrlTags] = useState([]); - const [activeTagIndex, setActiveTagIndex] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); + const params = useParams(); + const router = useRouter(); + const search_space_id = params.search_space_id as string; - // Function to validate a URL - const isValidUrl = (url: string): boolean => { - return urlRegex.test(url); - }; + const [urlTags, setUrlTags] = useState([]); + const [activeTagIndex, setActiveTagIndex] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); - // Function to handle URL submission - const handleSubmit = async () => { - // Validate that we have at least one URL - if (urlTags.length === 0) { - setError("Please add at least one URL"); - return; - } + // Function to validate a URL + const isValidUrl = (url: string): boolean => { + return urlRegex.test(url); + }; - // Validate all URLs - const invalidUrls = urlTags.filter(tag => !isValidUrl(tag.text)); - if (invalidUrls.length > 0) { - setError(`Invalid URLs detected: ${invalidUrls.map(tag => tag.text).join(', ')}`); - return; - } + // Function to handle URL submission + const handleSubmit = async () => { + // Validate that we have at least one URL + if (urlTags.length === 0) { + setError("Please add at least one URL"); + return; + } - setError(null); - setIsSubmitting(true); + // Validate all URLs + const invalidUrls = urlTags.filter((tag) => !isValidUrl(tag.text)); + if (invalidUrls.length > 0) { + setError(`Invalid URLs detected: ${invalidUrls.map((tag) => tag.text).join(", ")}`); + return; + } - try { - toast("URL Crawling", { - description: "Starting URL crawling process...", - }); + setError(null); + setIsSubmitting(true); - // Extract URLs from tags - const urls = urlTags.map(tag => tag.text); + try { + toast("URL Crawling", { + description: "Starting URL crawling process...", + }); - // Make API call to backend - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/`, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("surfsense_bearer_token")}` - }, - body: JSON.stringify({ - "document_type": "CRAWLED_URL", - "content": urls, - "search_space_id": parseInt(search_space_id) - }), - }); + // Extract URLs from tags + const urls = urlTags.map((tag) => tag.text); - if (!response.ok) { - throw new Error("Failed to crawl URLs"); - } + // Make API call to backend + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + body: JSON.stringify({ + document_type: "CRAWLED_URL", + content: urls, + search_space_id: parseInt(search_space_id), + }), + } + ); - await response.json(); + if (!response.ok) { + throw new Error("Failed to crawl URLs"); + } - toast("Crawling Successful", { - description: "URLs have been submitted for crawling", - }); + await response.json(); - // Redirect to documents page - router.push(`/dashboard/${search_space_id}/documents`); - } catch (error: any) { - setError(error.message || "An error occurred while crawling URLs"); - toast("Crawling Error", { - description: `Error crawling URLs: ${error.message}`, - }); - } finally { - setIsSubmitting(false); - } - }; + toast("Crawling Successful", { + description: "URLs have been submitted for crawling", + }); - // Function to add a new URL tag - const handleAddTag = (text: string) => { - // Basic URL validation - if (!isValidUrl(text)) { - toast("Invalid URL", { - description: "Please enter a valid URL", - }); - return; - } + // Redirect to documents page + router.push(`/dashboard/${search_space_id}/documents`); + } catch (error: any) { + setError(error.message || "An error occurred while crawling URLs"); + toast("Crawling Error", { + description: `Error crawling URLs: ${error.message}`, + }); + } finally { + setIsSubmitting(false); + } + }; - // Check for duplicates - if (urlTags.some(tag => tag.text === text)) { - toast("Duplicate URL", { - description: "This URL has already been added", - }); - return; - } + // Function to add a new URL tag + const handleAddTag = (text: string) => { + // Basic URL validation + if (!isValidUrl(text)) { + toast("Invalid URL", { + description: "Please enter a valid URL", + }); + return; + } - // Add the new tag - const newTag: Tag = { - id: Date.now().toString(), - text: text, - }; + // Check for duplicates + if (urlTags.some((tag) => tag.text === text)) { + toast("Duplicate URL", { + description: "This URL has already been added", + }); + return; + } - setUrlTags([...urlTags, newTag]); - }; + // Add the new tag + const newTag: Tag = { + id: Date.now().toString(), + text: text, + }; - return ( -
    - - - - - Add Webpages for Crawling - - - Enter URLs to crawl and add to your document collection - - - -
    -
    - - -

    - Add multiple URLs by pressing Enter after each one -

    -
    + setUrlTags([...urlTags, newTag]); + }; - {error && ( -
    - {error} -
    - )} + return ( +
    + + + + + Add Webpages for Crawling + + Enter URLs to crawl and add to your document collection + + +
    +
    + + +

    + Add multiple URLs by pressing Enter after each one +

    +
    -
    -

    Tips for URL crawling:

    -
      -
    • Enter complete URLs including http:// or https://
    • -
    • Make sure the websites allow crawling
    • -
    • Public webpages work best
    • -
    • Crawling may take some time depending on the website size
    • -
    -
    -
    -
    - - - - -
    -
    - ); -} \ No newline at end of file + {error &&
    {error}
    } + +
    +

    Tips for URL crawling:

    +
      +
    • Enter complete URLs including http:// or https://
    • +
    • Make sure the websites allow crawling
    • +
    • Public webpages work best
    • +
    • Crawling may take some time depending on the website size
    • +
    +
    +
    +
    + + + + +
    +
    + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx index 540d26182..931de1098 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/youtube/page.tsx @@ -1,302 +1,303 @@ "use client"; -import { useState } from 'react'; -import { useParams, useRouter } from 'next/navigation'; -import { Tag, TagInput } from "emblor"; +import { useState } from "react"; +import { useParams, useRouter } from "next/navigation"; +import { type Tag, TagInput } from "emblor"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { toast } from "sonner"; import { Youtube, Loader2 } from "lucide-react"; import { motion } from "framer-motion"; // YouTube video ID validation regex -const youtubeRegex = /^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/; +const youtubeRegex = + /^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/; export default function YouTubeVideoAdder() { - const params = useParams(); - const router = useRouter(); - const search_space_id = params.search_space_id as string; - - const [videoTags, setVideoTags] = useState([]); - const [activeTagIndex, setActiveTagIndex] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); + const params = useParams(); + const router = useRouter(); + const search_space_id = params.search_space_id as string; - // Function to validate a YouTube URL - const isValidYoutubeUrl = (url: string): boolean => { - return youtubeRegex.test(url); - }; + const [videoTags, setVideoTags] = useState([]); + const [activeTagIndex, setActiveTagIndex] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); - // Function to extract video ID from URL - const extractVideoId = (url: string): string | null => { - const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); - return match ? match[1] : null; - }; + // Function to validate a YouTube URL + const isValidYoutubeUrl = (url: string): boolean => { + return youtubeRegex.test(url); + }; - // Function to handle video URL submission - const handleSubmit = async () => { - // Validate that we have at least one video URL - if (videoTags.length === 0) { - setError("Please add at least one YouTube video URL"); - return; - } + // Function to extract video ID from URL + const extractVideoId = (url: string): string | null => { + const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); + return match ? match[1] : null; + }; - // Validate all URLs - const invalidUrls = videoTags.filter(tag => !isValidYoutubeUrl(tag.text)); - if (invalidUrls.length > 0) { - setError(`Invalid YouTube URLs detected: ${invalidUrls.map(tag => tag.text).join(', ')}`); - return; - } + // Function to handle video URL submission + const handleSubmit = async () => { + // Validate that we have at least one video URL + if (videoTags.length === 0) { + setError("Please add at least one YouTube video URL"); + return; + } - setError(null); - setIsSubmitting(true); + // Validate all URLs + const invalidUrls = videoTags.filter((tag) => !isValidYoutubeUrl(tag.text)); + if (invalidUrls.length > 0) { + setError(`Invalid YouTube URLs detected: ${invalidUrls.map((tag) => tag.text).join(", ")}`); + return; + } - try { - toast("YouTube Video Processing", { - description: "Starting YouTube video processing...", - }); + setError(null); + setIsSubmitting(true); - // Extract URLs from tags - const videoUrls = videoTags.map(tag => tag.text); + try { + toast("YouTube Video Processing", { + description: "Starting YouTube video processing...", + }); - // Make API call to backend - const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/`, { - method: "POST", - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("surfsense_bearer_token")}` - }, - body: JSON.stringify({ - "document_type": "YOUTUBE_VIDEO", - "content": videoUrls, - "search_space_id": parseInt(search_space_id) - }), - }); + // Extract URLs from tags + const videoUrls = videoTags.map((tag) => tag.text); - if (!response.ok) { - throw new Error("Failed to process YouTube videos"); - } + // Make API call to backend + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/documents/`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`, + }, + body: JSON.stringify({ + document_type: "YOUTUBE_VIDEO", + content: videoUrls, + search_space_id: parseInt(search_space_id), + }), + } + ); - await response.json(); + if (!response.ok) { + throw new Error("Failed to process YouTube videos"); + } - toast("Processing Successful", { - description: "YouTube videos have been submitted for processing", - }); + await response.json(); - // Redirect to documents page - router.push(`/dashboard/${search_space_id}/documents`); - } catch (error: any) { - setError(error.message || "An error occurred while processing YouTube videos"); - toast("Processing Error", { - description: `Error processing YouTube videos: ${error.message}`, - }); - } finally { - setIsSubmitting(false); - } - }; + toast("Processing Successful", { + description: "YouTube videos have been submitted for processing", + }); - // Function to add a new video URL tag - const handleAddTag = (text: string) => { - // Basic URL validation - if (!isValidYoutubeUrl(text)) { - toast("Invalid YouTube URL", { - description: "Please enter a valid YouTube video URL", - }); - return; - } + // Redirect to documents page + router.push(`/dashboard/${search_space_id}/documents`); + } catch (error: any) { + setError(error.message || "An error occurred while processing YouTube videos"); + toast("Processing Error", { + description: `Error processing YouTube videos: ${error.message}`, + }); + } finally { + setIsSubmitting(false); + } + }; - // Check for duplicates - if (videoTags.some(tag => tag.text === text)) { - toast("Duplicate URL", { - description: "This YouTube video has already been added", - }); - return; - } + // Function to add a new video URL tag + const handleAddTag = (text: string) => { + // Basic URL validation + if (!isValidYoutubeUrl(text)) { + toast("Invalid YouTube URL", { + description: "Please enter a valid YouTube video URL", + }); + return; + } - // Add the new tag - const newTag: Tag = { - id: Date.now().toString(), - text: text, - }; + // Check for duplicates + if (videoTags.some((tag) => tag.text === text)) { + toast("Duplicate URL", { + description: "This YouTube video has already been added", + }); + return; + } - setVideoTags([...videoTags, newTag]); - }; + // Add the new tag + const newTag: Tag = { + id: Date.now().toString(), + text: text, + }; - // Animation variants - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1 - } - } - }; - - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { - type: "spring", - stiffness: 300, - damping: 24 - } - } - }; + setVideoTags([...videoTags, newTag]); + }; - return ( -
    - - - - - - - Add YouTube Videos - - - Enter YouTube video URLs to add to your document collection - - - - - - -
    -
    - - -

    - Add multiple YouTube URLs by pressing Enter after each one -

    -
    + // Animation variants + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, + }; - {error && ( - - {error} - - )} + const itemVariants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 300, + damping: 24, + }, + }, + }; - -

    Tips for adding YouTube videos:

    -
      -
    • Use standard YouTube URLs (youtube.com/watch?v= or youtu.be/)
    • -
    • Make sure videos are publicly accessible
    • -
    • Supported formats: youtube.com/watch?v=VIDEO_ID or youtu.be/VIDEO_ID
    • -
    • Processing may take some time depending on video length
    • -
    -
    + return ( +
    + + + + + + + Add YouTube Videos + + + Enter YouTube video URLs to add to your document collection + + + - {videoTags.length > 0 && ( - -

    Preview:

    -
    - {videoTags.map((tag, index) => { - const videoId = extractVideoId(tag.text); - return videoId ? ( - - - - ) : null; - })} -
    -
    - )} -
    - - - - - - - - - - - -
    - ); + + +
    +
    + + +

    + Add multiple YouTube URLs by pressing Enter after each one +

    +
    + + {error && ( + + {error} + + )} + + +

    Tips for adding YouTube videos:

    +
      +
    • Use standard YouTube URLs (youtube.com/watch?v= or youtu.be/)
    • +
    • Make sure videos are publicly accessible
    • +
    • Supported formats: youtube.com/watch?v=VIDEO_ID or youtu.be/VIDEO_ID
    • +
    • Processing may take some time depending on video length
    • +
    +
    + + {videoTags.length > 0 && ( + +

    Preview:

    +
    + {videoTags.map((tag, index) => { + const videoId = extractVideoId(tag.text); + return videoId ? ( + + + + ) : null; + })} +
    +
    + )} +
    +
    +
    + + + + + + + +
    +
    +
    + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index ff077f5fc..9dc18621b 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -1,100 +1,99 @@ // Server component -import React, { use } from 'react' -import { DashboardClientLayout } from './client-layout' +import type React from "react"; +import { use } from "react"; +import { DashboardClientLayout } from "./client-layout"; -export default function DashboardLayout({ - params, - children -}: { - params: Promise<{ search_space_id: string }>, - children: React.ReactNode +export default function DashboardLayout({ + params, + children, +}: { + params: Promise<{ search_space_id: string }>; + children: React.ReactNode; }) { - // Use React.use to unwrap the params Promise - const { search_space_id } = use(params); + // Use React.use to unwrap the params Promise + const { search_space_id } = use(params); - const customNavSecondary = [ - { - title: `All Search Spaces`, - url: `#`, - icon: "Info", - }, - { - title: `All Search Spaces`, - url: "/dashboard", - icon: "Undo2", - }, - ] + const customNavSecondary = [ + { + title: `All Search Spaces`, + url: `#`, + icon: "Info", + }, + { + title: `All Search Spaces`, + url: "/dashboard", + icon: "Undo2", + }, + ]; - const customNavMain = [ - { - title: "Researcher", - url: `/dashboard/${search_space_id}/researcher`, - icon: "SquareTerminal", - isActive: true, - items: [], - }, + const customNavMain = [ + { + title: "Researcher", + url: `/dashboard/${search_space_id}/researcher`, + icon: "SquareTerminal", + isActive: true, + items: [], + }, - { - title: "Documents", - url: "#", - icon: "FileStack", - items: [ - { - title: "Upload Documents", - url: `/dashboard/${search_space_id}/documents/upload`, - }, - // { TODO: FIX THIS AND ADD IT BACK - // title: "Add Webpages", - // url: `/dashboard/${search_space_id}/documents/webpage`, - // }, - { - title: "Add Youtube Videos", - url: `/dashboard/${search_space_id}/documents/youtube`, - }, - { - title: "Manage Documents", - url: `/dashboard/${search_space_id}/documents`, - }, - ], - }, - { - title: "Connectors", - url: `#`, - icon: "Cable", - items: [ - { - title: "Add Connector", - url: `/dashboard/${search_space_id}/connectors/add`, - }, - { - title: "Manage Connectors", - url: `/dashboard/${search_space_id}/connectors`, - }, - ], - }, - { - title: "Podcasts", - url: `/dashboard/${search_space_id}/podcasts`, - icon: "Podcast", - items: [ - ], - }, - { - title: "Logs", - url: `/dashboard/${search_space_id}/logs`, - icon: "FileText", - items: [ - ], - } - ] + { + title: "Documents", + url: "#", + icon: "FileStack", + items: [ + { + title: "Upload Documents", + url: `/dashboard/${search_space_id}/documents/upload`, + }, + // { TODO: FIX THIS AND ADD IT BACK + // title: "Add Webpages", + // url: `/dashboard/${search_space_id}/documents/webpage`, + // }, + { + title: "Add Youtube Videos", + url: `/dashboard/${search_space_id}/documents/youtube`, + }, + { + title: "Manage Documents", + url: `/dashboard/${search_space_id}/documents`, + }, + ], + }, + { + title: "Connectors", + url: `#`, + icon: "Cable", + items: [ + { + title: "Add Connector", + url: `/dashboard/${search_space_id}/connectors/add`, + }, + { + title: "Manage Connectors", + url: `/dashboard/${search_space_id}/connectors`, + }, + ], + }, + { + title: "Podcasts", + url: `/dashboard/${search_space_id}/podcasts`, + icon: "Podcast", + items: [], + }, + { + title: "Logs", + url: `/dashboard/${search_space_id}/logs`, + icon: "FileText", + items: [], + }, + ]; - return ( - - {children} - - ) -} \ No newline at end of file + return ( + + {children} + + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx index e43e03bce..56c2de446 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx @@ -1,91 +1,91 @@ "use client"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Pagination, PaginationContent, PaginationItem } from "@/components/ui/pagination"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@/components/ui/table"; import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; -import { useLogs, useLogsSummary, Log, LogLevel, LogStatus } from "@/hooks/use-logs"; +import { useLogs, useLogsSummary, type Log, type LogLevel, type LogStatus } from "@/hooks/use-logs"; import { cn } from "@/lib/utils"; import { - ColumnDef, - ColumnFiltersState, - PaginationState, - Row, - SortingState, - VisibilityState, - flexRender, - getCoreRowModel, - getFacetedUniqueValues, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, + type ColumnDef, + type ColumnFiltersState, + type PaginationState, + type Row, + type SortingState, + type VisibilityState, + flexRender, + getCoreRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, } from "@tanstack/react-table"; import { AnimatePresence, motion } from "framer-motion"; import { - Activity, - AlertCircle, - AlertTriangle, - Bug, - CheckCircle2, - ChevronDown, - ChevronFirst, - ChevronLast, - ChevronLeft, - ChevronRight, - ChevronUp, - CircleAlert, - CircleX, - Clock, - Columns3, - Filter, - Info, - ListFilter, - MoreHorizontal, - RefreshCw, - Terminal, - Trash, - X, - Zap, + Activity, + AlertCircle, + AlertTriangle, + Bug, + CheckCircle2, + ChevronDown, + ChevronFirst, + ChevronLast, + ChevronLeft, + ChevronRight, + ChevronUp, + CircleAlert, + CircleX, + Clock, + Columns3, + Filter, + Info, + ListFilter, + MoreHorizontal, + RefreshCw, + Terminal, + Trash, + X, + Zap, } from "lucide-react"; import { useParams } from "next/navigation"; import React, { useContext, useEffect, useId, useMemo, useRef, useState } from "react"; @@ -93,993 +93,997 @@ import { toast } from "sonner"; // Define animation variants for reuse const fadeInScale = { - hidden: { opacity: 0, scale: 0.95 }, - visible: { - opacity: 1, - scale: 1, - transition: { type: "spring", stiffness: 300, damping: 30 } - }, - exit: { - opacity: 0, - scale: 0.95, - transition: { duration: 0.15 } - } + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { type: "spring", stiffness: 300, damping: 30 }, + }, + exit: { + opacity: 0, + scale: 0.95, + transition: { duration: 0.15 }, + }, }; // Log level icons and colors const logLevelConfig = { - DEBUG: { icon: Bug, color: "text-muted-foreground", bgColor: "bg-muted/50" }, - INFO: { icon: Info, color: "text-blue-600", bgColor: "bg-blue-50" }, - WARNING: { icon: AlertTriangle, color: "text-yellow-600", bgColor: "bg-yellow-50" }, - ERROR: { icon: AlertCircle, color: "text-red-600", bgColor: "bg-red-50" }, - CRITICAL: { icon: Zap, color: "text-purple-600", bgColor: "bg-purple-50" }, + DEBUG: { icon: Bug, color: "text-muted-foreground", bgColor: "bg-muted/50" }, + INFO: { icon: Info, color: "text-blue-600", bgColor: "bg-blue-50" }, + WARNING: { icon: AlertTriangle, color: "text-yellow-600", bgColor: "bg-yellow-50" }, + ERROR: { icon: AlertCircle, color: "text-red-600", bgColor: "bg-red-50" }, + CRITICAL: { icon: Zap, color: "text-purple-600", bgColor: "bg-purple-50" }, } as const; // Log status icons and colors const logStatusConfig = { - IN_PROGRESS: { icon: Clock, color: "text-blue-600", bgColor: "bg-blue-50" }, - SUCCESS: { icon: CheckCircle2, color: "text-green-600", bgColor: "bg-green-50" }, - FAILED: { icon: X, color: "text-red-600", bgColor: "bg-red-50" }, + IN_PROGRESS: { icon: Clock, color: "text-blue-600", bgColor: "bg-blue-50" }, + SUCCESS: { icon: CheckCircle2, color: "text-green-600", bgColor: "bg-green-50" }, + FAILED: { icon: X, color: "text-red-600", bgColor: "bg-red-50" }, } as const; const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - size: 28, - enableSorting: false, - enableHiding: false, - }, - { - header: "Level", - accessorKey: "level", - cell: ({ row }) => { - const level = row.getValue("level") as LogLevel; - const config = logLevelConfig[level]; - const Icon = config.icon; - return ( - -
    - -
    - - {level} - -
    - ); - }, - size: 120, - }, - { - header: "Status", - accessorKey: "status", - cell: ({ row }) => { - const status = row.getValue("status") as LogStatus; - const config = logStatusConfig[status]; - const Icon = config.icon; - return ( - -
    - -
    - - {status.replace('_', ' ')} - -
    - ); - }, - size: 140, - }, - { - header: "Source", - accessorKey: "source", - cell: ({ row }) => { - const source = row.getValue("source") as string; - return ( - - - {source || "System"} - - ); - }, - size: 150, - }, - { - header: "Message", - accessorKey: "message", - cell: ({ row }) => { - const message = row.getValue("message") as string; - const taskName = row.original.log_metadata?.task_name; - - return ( -
    - {taskName && ( -
    - {taskName} -
    - )} -
    - {message.length > 100 ? `${message.substring(0, 100)}...` : message} -
    -
    - ); - }, - size: 400, - }, - { - header: "Created At", - accessorKey: "created_at", - cell: ({ row }) => { - const date = new Date(row.getValue("created_at")); - return ( -
    -
    {date.toLocaleDateString()}
    -
    {date.toLocaleTimeString()}
    -
    - ); - }, - size: 120, - }, - { - id: "actions", - header: () => Actions, - cell: ({ row }) => , - size: 60, - enableHiding: false, - }, + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 28, + enableSorting: false, + enableHiding: false, + }, + { + header: "Level", + accessorKey: "level", + cell: ({ row }) => { + const level = row.getValue("level") as LogLevel; + const config = logLevelConfig[level]; + const Icon = config.icon; + return ( + +
    + +
    + {level} +
    + ); + }, + size: 120, + }, + { + header: "Status", + accessorKey: "status", + cell: ({ row }) => { + const status = row.getValue("status") as LogStatus; + const config = logStatusConfig[status]; + const Icon = config.icon; + return ( + +
    + +
    + + {status.replace("_", " ")} + +
    + ); + }, + size: 140, + }, + { + header: "Source", + accessorKey: "source", + cell: ({ row }) => { + const source = row.getValue("source") as string; + return ( + + + {source || "System"} + + ); + }, + size: 150, + }, + { + header: "Message", + accessorKey: "message", + cell: ({ row }) => { + const message = row.getValue("message") as string; + const taskName = row.original.log_metadata?.task_name; + + return ( +
    + {taskName && ( +
    + {taskName} +
    + )} +
    + {message.length > 100 ? `${message.substring(0, 100)}...` : message} +
    +
    + ); + }, + size: 400, + }, + { + header: "Created At", + accessorKey: "created_at", + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return ( +
    +
    {date.toLocaleDateString()}
    +
    {date.toLocaleTimeString()}
    +
    + ); + }, + size: 120, + }, + { + id: "actions", + header: () => Actions, + cell: ({ row }) => , + size: 60, + enableHiding: false, + }, ]; // Create a context to share functions const LogsContext = React.createContext<{ - deleteLog: (id: number) => Promise; - refreshLogs: () => Promise; + deleteLog: (id: number) => Promise; + refreshLogs: () => Promise; } | null>(null); export default function LogsManagePage() { - const id = useId(); - const params = useParams(); - const searchSpaceId = Number(params.search_space_id); - - const { logs, loading: logsLoading, error: logsError, refreshLogs, deleteLog } = useLogs(searchSpaceId); - const { summary, loading: summaryLoading, error: summaryError, refreshSummary } = useLogsSummary(searchSpaceId, 24); - - const [columnFilters, setColumnFilters] = useState([]); - const [columnVisibility, setColumnVisibility] = useState({}); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: 20, - }); - const [sorting, setSorting] = useState([ - { - id: "created_at", - desc: true, - }, - ]); + const id = useId(); + const params = useParams(); + const searchSpaceId = Number(params.search_space_id); - const inputRef = useRef(null); + const { + logs, + loading: logsLoading, + error: logsError, + refreshLogs, + deleteLog, + } = useLogs(searchSpaceId); + const { + summary, + loading: summaryLoading, + error: summaryError, + refreshSummary, + } = useLogsSummary(searchSpaceId, 24); - const table = useReactTable({ - data: logs, - columns, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - onSortingChange: setSorting, - enableSortingRemoval: false, - getPaginationRowModel: getPaginationRowModel(), - onPaginationChange: setPagination, - onColumnFiltersChange: setColumnFilters, - onColumnVisibilityChange: setColumnVisibility, - getFilteredRowModel: getFilteredRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - state: { - sorting, - pagination, - columnFilters, - columnVisibility, - }, - }); + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 20, + }); + const [sorting, setSorting] = useState([ + { + id: "created_at", + desc: true, + }, + ]); - // Get unique values for filters - const uniqueLevels = useMemo(() => { - const levelColumn = table.getColumn("level"); - if (!levelColumn) return []; - return Array.from(levelColumn.getFacetedUniqueValues().keys()).sort(); - }, [table.getColumn("level")?.getFacetedUniqueValues()]); + const inputRef = useRef(null); - const uniqueStatuses = useMemo(() => { - const statusColumn = table.getColumn("status"); - if (!statusColumn) return []; - return Array.from(statusColumn.getFacetedUniqueValues().keys()).sort(); - }, [table.getColumn("status")?.getFacetedUniqueValues()]); + const table = useReactTable({ + data: logs, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + enableSortingRemoval: false, + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getFilteredRowModel: getFilteredRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + state: { + sorting, + pagination, + columnFilters, + columnVisibility, + }, + }); - const handleDeleteRows = async () => { - const selectedRows = table.getSelectedRowModel().rows; - - if (selectedRows.length === 0) { - toast.error("No rows selected"); - return; - } - - const deletePromises = selectedRows.map(row => deleteLog(row.original.id)); - - try { - const results = await Promise.all(deletePromises); - const allSuccessful = results.every(result => result === true); - - if (allSuccessful) { - toast.success(`Successfully deleted ${selectedRows.length} log(s)`); - } else { - toast.error("Some logs could not be deleted"); - } - - await refreshLogs(); - table.resetRowSelection(); - } catch (error: any) { - console.error("Error deleting logs:", error); - toast.error("Error deleting logs"); - } - }; + // Get unique values for filters + const uniqueLevels = useMemo(() => { + const levelColumn = table.getColumn("level"); + if (!levelColumn) return []; + return Array.from(levelColumn.getFacetedUniqueValues().keys()).sort(); + }, [table.getColumn("level")?.getFacetedUniqueValues()]); - const handleRefresh = async () => { - await Promise.all([refreshLogs(), refreshSummary()]); - toast.success("Logs refreshed"); - }; + const uniqueStatuses = useMemo(() => { + const statusColumn = table.getColumn("status"); + if (!statusColumn) return []; + return Array.from(statusColumn.getFacetedUniqueValues().keys()).sort(); + }, [table.getColumn("status")?.getFacetedUniqueValues()]); - return ( - Promise.resolve(false)), - refreshLogs: refreshLogs || (() => Promise.resolve()) - }}> - - {/* Summary Dashboard */} - + const handleDeleteRows = async () => { + const selectedRows = table.getSelectedRowModel().rows; - {/* Logs Table Header */} - -
    -

    Task Logs

    -

    - Monitor and analyze all task execution logs -

    -
    - -
    + if (selectedRows.length === 0) { + toast.error("No rows selected"); + return; + } - {/* Filters */} - + const deletePromises = selectedRows.map((row) => deleteLog(row.original.id)); - {/* Delete Button */} - {table.getSelectedRowModel().rows.length > 0 && ( - - - - - - -
    -
    - -
    - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete{" "} - {table.getSelectedRowModel().rows.length} selected log(s). - - -
    - - Cancel - Delete - -
    -
    -
    - )} + try { + const results = await Promise.all(deletePromises); + const allSuccessful = results.every((result) => result === true); - {/* Logs Table */} - -
    -
    - ); + if (allSuccessful) { + toast.success(`Successfully deleted ${selectedRows.length} log(s)`); + } else { + toast.error("Some logs could not be deleted"); + } + + await refreshLogs(); + table.resetRowSelection(); + } catch (error: any) { + console.error("Error deleting logs:", error); + toast.error("Error deleting logs"); + } + }; + + const handleRefresh = async () => { + await Promise.all([refreshLogs(), refreshSummary()]); + toast.success("Logs refreshed"); + }; + + return ( + Promise.resolve(false)), + refreshLogs: refreshLogs || (() => Promise.resolve()), + }} + > + + {/* Summary Dashboard */} + + + {/* Logs Table Header */} + +
    +

    Task Logs

    +

    Monitor and analyze all task execution logs

    +
    + +
    + + {/* Filters */} + + + {/* Delete Button */} + {table.getSelectedRowModel().rows.length > 0 && ( + + + + + + +
    +
    + +
    + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete{" "} + {table.getSelectedRowModel().rows.length} selected log(s). + + +
    + + Cancel + Delete + +
    +
    +
    + )} + + {/* Logs Table */} + +
    +
    + ); } // Summary Dashboard Component -function LogsSummaryDashboard({ - summary, - loading, - error, - onRefresh -}: { - summary: any; - loading: boolean; - error: string | null; - onRefresh: () => void; +function LogsSummaryDashboard({ + summary, + loading, + error, + onRefresh, +}: { + summary: any; + loading: boolean; + error: string | null; + onRefresh: () => void; }) { - if (loading) { - return ( - - {[...Array(4)].map((_, i) => ( - - -
    - - -
    - - - ))} - - ); - } + if (loading) { + return ( + + {[...Array(4)].map((_, i) => ( + + +
    + + +
    + + + ))} + + ); + } - if (error || !summary) { - return ( - - -
    - -

    Failed to load summary

    - -
    -
    -
    - ); - } + if (error || !summary) { + return ( + + +
    + +

    Failed to load summary

    + +
    +
    +
    + ); + } - return ( - - {/* Total Logs */} - - - - Total Logs - - - -
    {summary.total_logs}
    -

    - Last {summary.time_window_hours} hours -

    -
    -
    -
    + return ( + + {/* Total Logs */} + + + + Total Logs + + + +
    {summary.total_logs}
    +

    Last {summary.time_window_hours} hours

    +
    +
    +
    - {/* Active Tasks */} - - - - Active Tasks - - - -
    - {summary.active_tasks?.length || 0} -
    -

    - Currently running -

    -
    -
    -
    + {/* Active Tasks */} + + + + Active Tasks + + + +
    + {summary.active_tasks?.length || 0} +
    +

    Currently running

    +
    +
    +
    - {/* Success Rate */} - - - - Success Rate - - - -
    - {summary.total_logs > 0 - ? Math.round(((summary.by_status?.SUCCESS || 0) / summary.total_logs) * 100) - : 0 - }% -
    -

    - {summary.by_status?.SUCCESS || 0} successful -

    -
    -
    -
    + {/* Success Rate */} + + + + Success Rate + + + +
    + {summary.total_logs > 0 + ? Math.round(((summary.by_status?.SUCCESS || 0) / summary.total_logs) * 100) + : 0} + % +
    +

    + {summary.by_status?.SUCCESS || 0} successful +

    +
    +
    +
    - {/* Recent Failures */} - - - - Recent Failures - - - -
    - {summary.recent_failures?.length || 0} -
    -

    - Need attention -

    -
    -
    -
    -
    - ); + {/* Recent Failures */} + + + + Recent Failures + + + +
    + {summary.recent_failures?.length || 0} +
    +

    Need attention

    +
    +
    +
    +
    + ); } // Filters Component -function LogsFilters({ - table, - uniqueLevels, - uniqueStatuses, - inputRef, - id -}: { - table: any; - uniqueLevels: string[]; - uniqueStatuses: string[]; - inputRef: React.RefObject; - id: string; +function LogsFilters({ + table, + uniqueLevels, + uniqueStatuses, + inputRef, + id, +}: { + table: any; + uniqueLevels: string[]; + uniqueStatuses: string[]; + inputRef: React.RefObject; + id: string; }) { - return ( - -
    - {/* Search Input */} - - table.getColumn("message")?.setFilterValue(e.target.value)} - placeholder="Filter by message..." - type="text" - /> -
    - -
    - {Boolean(table.getColumn("message")?.getFilterValue()) && ( - - )} -
    + return ( + +
    + {/* Search Input */} + + table.getColumn("message")?.setFilterValue(e.target.value)} + placeholder="Filter by message..." + type="text" + /> +
    + +
    + {Boolean(table.getColumn("message")?.getFilterValue()) && ( + + )} +
    - {/* Level Filter */} - + {/* Level Filter */} + - {/* Status Filter */} - + {/* Status Filter */} + - {/* Column Visibility */} - - - - - - Toggle columns - {table - .getAllColumns() - .filter((column: any) => column.getCanHide()) - .map((column: any) => ( - column.toggleVisibility(!!value)} - onSelect={(event) => event.preventDefault()} - > - {column.id} - - ))} - - -
    -
    - ); + {/* Column Visibility */} + + + + + + Toggle columns + {table + .getAllColumns() + .filter((column: any) => column.getCanHide()) + .map((column: any) => ( + column.toggleVisibility(!!value)} + onSelect={(event) => event.preventDefault()} + > + {column.id} + + ))} + + +
    +
    + ); } // Filter Dropdown Component -function FilterDropdown({ - title, - column, - options, - id -}: { - title: string; - column: any; - options: string[]; - id: string; +function FilterDropdown({ + title, + column, + options, + id, +}: { + title: string; + column: any; + options: string[]; + id: string; }) { - const selectedValues = useMemo(() => { - const filterValue = column?.getFilterValue() as string[]; - return filterValue ?? []; - }, [column?.getFilterValue()]); + const selectedValues = useMemo(() => { + const filterValue = column?.getFilterValue() as string[]; + return filterValue ?? []; + }, [column?.getFilterValue()]); - const handleValueChange = (checked: boolean, value: string) => { - const filterValue = column?.getFilterValue() as string[]; - const newFilterValue = filterValue ? [...filterValue] : []; + const handleValueChange = (checked: boolean, value: string) => { + const filterValue = column?.getFilterValue() as string[]; + const newFilterValue = filterValue ? [...filterValue] : []; - if (checked) { - newFilterValue.push(value); - } else { - const index = newFilterValue.indexOf(value); - if (index > -1) { - newFilterValue.splice(index, 1); - } - } + if (checked) { + newFilterValue.push(value); + } else { + const index = newFilterValue.indexOf(value); + if (index > -1) { + newFilterValue.splice(index, 1); + } + } - column?.setFilterValue(newFilterValue.length ? newFilterValue : undefined); - }; + column?.setFilterValue(newFilterValue.length ? newFilterValue : undefined); + }; - return ( - - - - - -
    -
    Filter by {title}
    -
    - {options.map((value, i) => ( -
    - handleValueChange(checked, value)} - /> - -
    - ))} -
    -
    -
    -
    - ); + return ( + + + + + +
    +
    Filter by {title}
    +
    + {options.map((value, i) => ( +
    + handleValueChange(checked, value)} + /> + +
    + ))} +
    +
    +
    +
    + ); } // Logs Table Component -function LogsTable({ - table, - logs, - loading, - error, - onRefresh, - id -}: { - table: any; - logs: Log[]; - loading: boolean; - error: string | null; - onRefresh: () => void; - id: string; +function LogsTable({ + table, + logs, + loading, + error, + onRefresh, + id, +}: { + table: any; + logs: Log[]; + loading: boolean; + error: string | null; + onRefresh: () => void; + id: string; }) { - if (loading) { - return ( - -
    -
    -
    -

    Loading logs...

    -
    -
    -
    - ); - } + if (loading) { + return ( + +
    +
    +
    +

    Loading logs...

    +
    +
    +
    + ); + } - if (error) { - return ( - -
    -
    - -

    Error loading logs

    - -
    -
    -
    - ); - } + if (error) { + return ( + +
    +
    + +

    Error loading logs

    + +
    +
    +
    + ); + } - if (logs.length === 0) { - return ( - -
    -
    - -

    No logs found

    -
    -
    -
    - ); - } + if (logs.length === 0) { + return ( + +
    +
    + +

    No logs found

    +
    +
    +
    + ); + } - return ( - <> - - - - {table.getHeaderGroups().map((headerGroup: any) => ( - - {headerGroup.headers.map((header: any) => ( - - {header.isPlaceholder ? null : header.column.getCanSort() ? ( -
    - {flexRender(header.column.columnDef.header, header.getContext())} - {{ - asc: , - desc: , - }[header.column.getIsSorted() as string] ?? null} -
    - ) : ( - flexRender(header.column.columnDef.header, header.getContext()) - )} -
    - ))} -
    - ))} -
    - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row: any, index: number) => ( - - {row.getVisibleCells().map((cell: any) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - No logs found. - - - )} - - -
    -
    + return ( + <> + + + + {table.getHeaderGroups().map((headerGroup: any) => ( + + {headerGroup.headers.map((header: any) => ( + + {header.isPlaceholder ? null : header.column.getCanSort() ? ( +
    + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} +
    + ) : ( + flexRender(header.column.columnDef.header, header.getContext()) + )} +
    + ))} +
    + ))} +
    + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row: any, index: number) => ( + + {row.getVisibleCells().map((cell: any) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No logs found. + + + )} + + +
    +
    - {/* Pagination */} - - - ); + {/* Pagination */} + + + ); } // Pagination Component function LogsPagination({ table, id }: { table: any; id: string }) { - return ( -
    - - - - + return ( +
    + + + + - -

    - - {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- - {Math.min( - table.getState().pagination.pageIndex * table.getState().pagination.pageSize + - table.getState().pagination.pageSize, - table.getRowCount(), - )} - {" "} - of {table.getRowCount()} -

    -
    + +

    + + {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- + {Math.min( + table.getState().pagination.pageIndex * table.getState().pagination.pageSize + + table.getState().pagination.pageSize, + table.getRowCount() + )} + {" "} + of {table.getRowCount()} +

    +
    -
    - - - - - - - - - - - - - - - - -
    -
    - ); +
    + + + + + + + + + + + + + + + + +
    +
    + ); } // Row Actions Component function LogRowActions({ row }: { row: Row }) { - const [isOpen, setIsOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const { deleteLog, refreshLogs } = useContext(LogsContext)!; - const log = row.original; + const [isOpen, setIsOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const { deleteLog, refreshLogs } = useContext(LogsContext)!; + const log = row.original; - const handleDelete = async () => { - setIsDeleting(true); - try { - await deleteLog(log.id); - toast.success("Log deleted successfully"); - await refreshLogs(); - } catch (error) { - console.error("Error deleting log:", error); - toast.error("Failed to delete log"); - } finally { - setIsDeleting(false); - setIsOpen(false); - } - }; + const handleDelete = async () => { + setIsDeleting(true); + try { + await deleteLog(log.id); + toast.success("Log deleted successfully"); + await refreshLogs(); + } catch (error) { + console.error("Error deleting log:", error); + toast.error("Failed to delete log"); + } finally { + setIsDeleting(false); + setIsOpen(false); + } + }; - return ( -
    - - - - - - e.preventDefault()}> - View Metadata - - } - /> - - - - { - e.preventDefault(); - setIsOpen(true); - }} - > - Delete - - - - - Are you sure? - - This action cannot be undone. This will permanently delete the log entry. - - - - Cancel - - {isDeleting ? "Deleting..." : "Delete"} - - - - - - -
    - ); -} \ No newline at end of file + return ( +
    + + + + + + e.preventDefault()}> + View Metadata + + } + /> + + + + { + e.preventDefault(); + setIsOpen(true); + }} + > + Delete + + + + + Are you sure? + + This action cannot be undone. This will permanently delete the log entry. + + + + Cancel + + {isDeleting ? "Deleting..." : "Delete"} + + + + + + +
    + ); +} diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx index 429260724..c8c724ee2 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/page.tsx @@ -1,20 +1,24 @@ -import { Suspense } from 'react'; -import PodcastsPageClient from './podcasts-client'; +import { Suspense } from "react"; +import PodcastsPageClient from "./podcasts-client"; interface PageProps { - params: { - search_space_id: string; - }; + params: { + search_space_id: string; + }; } export default async function PodcastsPage({ params }: PageProps) { - const { search_space_id: searchSpaceId } = await Promise.resolve(params); - - return ( - -
    -
    }> - - - ); + const { search_space_id: searchSpaceId } = await Promise.resolve(params); + + return ( + +
    +
    + } + > + + + ); } diff --git a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx index 7c803822c..5a6d6be55 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/podcasts/podcasts-client.tsx @@ -83,9 +83,7 @@ const podcastCardVariants = { const MotionCard = motion(Card); -export default function PodcastsPageClient({ - searchSpaceId, -}: PodcastsPageClientProps) { +export default function PodcastsPageClient({ searchSpaceId }: PodcastsPageClientProps) { const [podcasts, setPodcasts] = useState([]); const [filteredPodcasts, setFilteredPodcasts] = useState([]); const [isLoading, setIsLoading] = useState(true); @@ -100,9 +98,7 @@ export default function PodcastsPageClient({ const [isDeleting, setIsDeleting] = useState(false); // Audio player state - const [currentPodcast, setCurrentPodcast] = useState( - null, - ); + const [currentPodcast, setCurrentPodcast] = useState(null); const [audioSrc, setAudioSrc] = useState(undefined); const [isAudioLoading, setIsAudioLoading] = useState(false); const [isPlaying, setIsPlaying] = useState(false); @@ -141,13 +137,13 @@ export default function PodcastsPageClient({ "Content-Type": "application/json", }, cache: "no-store", - }, + } ); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error( - `Failed to fetch podcasts: ${response.status} ${errorData?.detail || ""}`, + `Failed to fetch podcasts: ${response.status} ${errorData?.detail || ""}` ); } @@ -157,9 +153,7 @@ export default function PodcastsPageClient({ setError(null); } catch (error) { console.error("Error fetching podcasts:", error); - setError( - error instanceof Error ? error.message : "Unknown error occurred", - ); + setError(error instanceof Error ? error.message : "Unknown error occurred"); setPodcasts([]); setFilteredPodcasts([]); } finally { @@ -177,15 +171,11 @@ export default function PodcastsPageClient({ // Filter by search term if (searchQuery) { const query = searchQuery.toLowerCase(); - result = result.filter((podcast) => - podcast.title.toLowerCase().includes(query), - ); + result = result.filter((podcast) => podcast.title.toLowerCase().includes(query)); } // Filter by search space - result = result.filter( - (podcast) => podcast.search_space_id === parseInt(searchSpaceId), - ); + result = result.filter((podcast) => podcast.search_space_id === parseInt(searchSpaceId)); // Sort podcasts result.sort((a, b) => { @@ -294,7 +284,7 @@ export default function PodcastsPageClient({ if (audioRef.current) { audioRef.current.currentTime = Math.min( audioRef.current.duration, - audioRef.current.currentTime + 10, + audioRef.current.currentTime + 10 ); } }; @@ -302,10 +292,7 @@ export default function PodcastsPageClient({ // Skip backward 10 seconds const skipBackward = () => { if (audioRef.current) { - audioRef.current.currentTime = Math.max( - 0, - audioRef.current.currentTime - 10, - ); + audioRef.current.currentTime = Math.max(0, audioRef.current.currentTime - 10); } }; @@ -361,13 +348,11 @@ export default function PodcastsPageClient({ Authorization: `Bearer ${token}`, }, signal: controller.signal, - }, + } ); if (!response.ok) { - throw new Error( - `Failed to fetch audio stream: ${response.statusText}`, - ); + throw new Error(`Failed to fetch audio stream: ${response.statusText}`); } const blob = await response.blob(); @@ -389,11 +374,7 @@ export default function PodcastsPageClient({ } } catch (error) { console.error("Error fetching or playing podcast:", error); - toast.error( - error instanceof Error - ? error.message - : "Failed to load podcast audio.", - ); + toast.error(error instanceof Error ? error.message : "Failed to load podcast audio."); // Reset state on error setCurrentPodcast(null); setAudioSrc(undefined); @@ -422,7 +403,7 @@ export default function PodcastsPageClient({ Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, - }, + } ); if (!response.ok) { @@ -435,7 +416,7 @@ export default function PodcastsPageClient({ // Update local state by removing the deleted podcast setPodcasts((prevPodcasts) => - prevPodcasts.filter((podcast) => podcast.id !== podcastToDelete.id), + prevPodcasts.filter((podcast) => podcast.id !== podcastToDelete.id) ); // If the current playing podcast is deleted, stop playback @@ -450,9 +431,7 @@ export default function PodcastsPageClient({ toast.success("Podcast deleted successfully"); } catch (error) { console.error("Error deleting podcast:", error); - toast.error( - error instanceof Error ? error.message : "Failed to delete podcast", - ); + toast.error(error instanceof Error ? error.message : "Failed to delete podcast"); } finally { setIsDeleting(false); } @@ -507,9 +486,7 @@ export default function PodcastsPageClient({
    -

    - Loading podcasts... -

    +

    Loading podcasts...

    )} @@ -589,18 +566,13 @@ export default function PodcastsPageClient({ transition={{ type: "spring", damping: 20 }} >
    -

    - Loading podcast... -

    +

    Loading podcast...

    )} {/* Play button with animations */} - {!( - currentPodcast?.id === podcast.id && - (isPlaying || isAudioLoading) - ) && ( + {!(currentPodcast?.id === podcast.id && (isPlaying || isAudioLoading)) && ( - - - )} + + + + + )} {/* Now playing indicator */} {currentPodcast?.id === podcast.id && !isAudioLoading && ( @@ -713,10 +683,7 @@ export default function PodcastsPageClient({ const container = e.currentTarget; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; - const percentage = Math.max( - 0, - Math.min(1, x / rect.width), - ); + const percentage = Math.max(0, Math.min(1, x / rect.width)); const newTime = percentage * duration; handleSeek([newTime]); }} @@ -750,10 +717,7 @@ export default function PodcastsPageClient({ animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} > - + - + - +
    - +
    - + - - -

    {copied ? "Copied!" : "Copy to clipboard"}

    -
    - - -
    - ) : ( - - No API key found. - - )} - - - -
    + + + + Your API Key + Use this key to authenticate your API requests. + + + + {isLoading ? ( + + ) : apiKey ? ( + +
    + + {apiKey} + +
    + + + + + + +

    {copied ? "Copied!" : "Copy to clipboard"}

    +
    +
    +
    +
    + ) : ( + + No API key found. + + )} +
    +
    +
    +
    - -

    - How to use your API key -

    - - - - -

    - Authentication -

    -

    - Include your API key in the Authorization header of your - requests: -

    - - - Authorization: Bearer {apiKey || "YOUR_API_KEY"} - - -
    -
    -
    -
    -
    -
    -
    - -
    -
    - ); + +

    How to use your API key

    + + + + +

    Authentication

    +

    + Include your API key in the Authorization header of your requests: +

    + + + Authorization: Bearer {apiKey || "YOUR_API_KEY"} + + +
    +
    +
    +
    +
    + +
    + +
    +
    + ); }; export default ApiKeyClient; diff --git a/surfsense_web/app/dashboard/api-key/client-wrapper.tsx b/surfsense_web/app/dashboard/api-key/client-wrapper.tsx index bbe1d018c..63bd700a3 100644 --- a/surfsense_web/app/dashboard/api-key/client-wrapper.tsx +++ b/surfsense_web/app/dashboard/api-key/client-wrapper.tsx @@ -1,32 +1,32 @@ -'use client' +"use client"; -import React, { useEffect, useState } from 'react' -import dynamic from 'next/dynamic' +import React, { useEffect, useState } from "react"; +import dynamic from "next/dynamic"; // Loading component with animation const LoadingComponent = () => ( -
    -
    -

    Loading API Key Management...

    -
    -) +
    +
    +

    Loading API Key Management...

    +
    +); // Dynamically import the ApiKeyClient component -const ApiKeyClient = dynamic(() => import('./api-key-client'), { - ssr: false, - loading: () => -}) +const ApiKeyClient = dynamic(() => import("./api-key-client"), { + ssr: false, + loading: () => , +}); export default function ClientWrapper() { - const [isMounted, setIsMounted] = useState(false) - - useEffect(() => { - setIsMounted(true) - }, []) - - if (!isMounted) { - return - } - - return -} \ No newline at end of file + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return ; + } + + return ; +} diff --git a/surfsense_web/app/dashboard/api-key/page.tsx b/surfsense_web/app/dashboard/api-key/page.tsx index 957652432..adfc6c367 100644 --- a/surfsense_web/app/dashboard/api-key/page.tsx +++ b/surfsense_web/app/dashboard/api-key/page.tsx @@ -1,6 +1,6 @@ -import React from 'react' -import ClientWrapper from './client-wrapper' +import React from "react"; +import ClientWrapper from "./client-wrapper"; export default function ApiKeyPage() { - return -} \ No newline at end of file + return ; +} diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx index 0a434748d..18cdf4da9 100644 --- a/surfsense_web/app/dashboard/layout.tsx +++ b/surfsense_web/app/dashboard/layout.tsx @@ -1,90 +1,92 @@ "use client"; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { useLLMPreferences } from '@/hooks/use-llm-configs'; -import { Loader2 } from 'lucide-react'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useLLMPreferences } from "@/hooks/use-llm-configs"; +import { Loader2 } from "lucide-react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; interface DashboardLayoutProps { - children: React.ReactNode; + children: React.ReactNode; } export default function DashboardLayout({ children }: DashboardLayoutProps) { - const router = useRouter(); - const { loading, error, isOnboardingComplete } = useLLMPreferences(); - const [isCheckingAuth, setIsCheckingAuth] = useState(true); + const router = useRouter(); + const { loading, error, isOnboardingComplete } = useLLMPreferences(); + const [isCheckingAuth, setIsCheckingAuth] = useState(true); - useEffect(() => { - // Check if user is authenticated - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - router.push('/login'); - return; - } - setIsCheckingAuth(false); - }, [router]); + useEffect(() => { + // Check if user is authenticated + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + router.push("/login"); + return; + } + setIsCheckingAuth(false); + }, [router]); - useEffect(() => { - // Wait for preferences to load, then check if onboarding is complete - if (!loading && !error && !isCheckingAuth) { - if (!isOnboardingComplete()) { - router.push('/onboard'); - } - } - }, [loading, error, isCheckingAuth, isOnboardingComplete, router]); + useEffect(() => { + // Wait for preferences to load, then check if onboarding is complete + if (!loading && !error && !isCheckingAuth) { + if (!isOnboardingComplete()) { + router.push("/onboard"); + } + } + }, [loading, error, isCheckingAuth, isOnboardingComplete, router]); - // Show loading screen while checking authentication or loading preferences - if (isCheckingAuth || loading) { - return ( -
    - - - Loading Dashboard - Checking your configuration... - - - - - -
    - ); - } + // Show loading screen while checking authentication or loading preferences + if (isCheckingAuth || loading) { + return ( +
    + + + Loading Dashboard + Checking your configuration... + + + + + +
    + ); + } - // Show error screen if there's an error loading preferences - if (error) { - return ( -
    - - - Configuration Error - Failed to load your LLM configuration - - -

    {error}

    -
    -
    -
    - ); - } + // Show error screen if there's an error loading preferences + if (error) { + return ( +
    + + + + Configuration Error + + Failed to load your LLM configuration + + +

    {error}

    +
    +
    +
    + ); + } - // Only render children if onboarding is complete - if (isOnboardingComplete()) { - return <>{children}; - } + // Only render children if onboarding is complete + if (isOnboardingComplete()) { + return <>{children}; + } - // This should not be reached due to redirect, but just in case - return ( -
    - - - Redirecting... - Taking you to complete your setup - - - - - -
    - ); -} \ No newline at end of file + // This should not be reached due to redirect, but just in case + return ( +
    + + + Redirecting... + Taking you to complete your setup + + + + + +
    + ); +} diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index c0ca5623e..3cab679f6 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -1,43 +1,46 @@ "use client"; -import React, { useEffect, useState } from 'react' -import Link from 'next/link' -import { motion } from 'framer-motion' -import { Button } from '@/components/ui/button' -import { Plus, Search, Trash2, AlertCircle, Loader2 } from 'lucide-react' -import { Tilt } from '@/components/ui/tilt' -import { Spotlight } from '@/components/ui/spotlight' -import { Logo } from '@/components/Logo'; -import { ThemeTogglerComponent } from '@/components/theme/theme-toggle'; -import { UserDropdown } from '@/components/UserDropdown'; -import { toast } from 'sonner'; +import React, { useEffect, useState } from "react"; +import Link from "next/link"; +import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Plus, Search, Trash2, AlertCircle, Loader2 } from "lucide-react"; +import { Tilt } from "@/components/ui/tilt"; +import { Spotlight } from "@/components/ui/spotlight"; +import { Logo } from "@/components/Logo"; +import { ThemeTogglerComponent } from "@/components/theme/theme-toggle"; +import { UserDropdown } from "@/components/UserDropdown"; +import { toast } from "sonner"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog'; + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { useSearchSpaces } from '@/hooks/use-search-spaces'; -import { apiClient } from '@/lib/api'; -import { useRouter } from 'next/navigation'; + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useSearchSpaces } from "@/hooks/use-search-spaces"; +import { apiClient } from "@/lib/api"; +import { useRouter } from "next/navigation"; interface User { - id: string; - email: string; - is_active: boolean; - is_superuser: boolean; - is_verified: boolean; + id: string; + email: string; + is_active: boolean; + is_superuser: boolean; + is_verified: boolean; } /** @@ -46,356 +49,352 @@ interface User { * @returns Formatted date string (e.g., "Jan 1, 2023") */ const formatDate = (dateString: string): string => { - return new Date(dateString).toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }); + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); }; /** * Loading screen component with animation */ const LoadingScreen = () => { - return ( -
    - - - - Loading - Fetching your search spaces... - - - - - - - - This may take a moment - - - -
    - ); + return ( +
    + + + + Loading + Fetching your search spaces... + + + + + + + + This may take a moment + + + +
    + ); }; /** * Error screen component with animation */ const ErrorScreen = ({ message }: { message: string }) => { - const router = useRouter(); + const router = useRouter(); - return ( -
    - - - -
    - - Error -
    - Something went wrong -
    - - - - Error Details - - {message} - - - - - - - -
    -
    -
    - ); + return ( +
    + + + +
    + + Error +
    + Something went wrong +
    + + + + Error Details + {message} + + + + + + +
    +
    +
    + ); }; const DashboardPage = () => { - // Animation variants - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - }, - }, - }; + // Animation variants + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, + }; - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { - type: "spring", - stiffness: 300, - damping: 24, - }, - }, - }; + const itemVariants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 300, + damping: 24, + }, + }, + }; - const router = useRouter(); - const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces(); - - // User state management - const [user, setUser] = useState(null); - const [isLoadingUser, setIsLoadingUser] = useState(true); - const [userError, setUserError] = useState(null); + const router = useRouter(); + const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces(); - // Fetch user details - useEffect(() => { - const fetchUser = async () => { - try { - if (typeof window === 'undefined') return; + // User state management + const [user, setUser] = useState(null); + const [isLoadingUser, setIsLoadingUser] = useState(true); + const [userError, setUserError] = useState(null); - try { - const userData = await apiClient.get('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); - } - }; + // Fetch user details + useEffect(() => { + const fetchUser = async () => { + try { + if (typeof window === "undefined") return; - fetchUser(); - }, []); + try { + const userData = await apiClient.get("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); + } + }; - // Create user object for UserDropdown - const customUser = { - name: user?.email ? user.email.split('@')[0] : 'User', - email: user?.email || (isLoadingUser ? 'Loading...' : userError ? 'Error loading user' : 'Unknown User'), - avatar: '/icon-128.png', // Default avatar - }; + fetchUser(); + }, []); - if (loading) return ; - if (error) return ; + // Create user object for UserDropdown + const customUser = { + name: user?.email ? user.email.split("@")[0] : "User", + email: + user?.email || + (isLoadingUser ? "Loading..." : userError ? "Error loading user" : "Unknown User"), + avatar: "/icon-128.png", // Default avatar + }; - const handleDeleteSearchSpace = async (id: number) => { - // Send DELETE request to the API - try { - 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) { - toast.error("Failed to delete search space"); - throw new Error("Failed to delete search space"); - } - - // Refresh the search spaces list after successful deletion - refreshSearchSpaces(); - } catch (error) { - console.error('Error deleting search space:', error); - toast.error("An error occurred while deleting the search space"); - return; - } - toast.success("Search space deleted successfully"); - }; + if (loading) return ; + if (error) return ; - return ( - - -
    -
    - -
    -

    SurfSense Dashboard

    -

    - Welcome to your SurfSense dashboard. -

    -
    -
    -
    - - -
    -
    + const handleDeleteSearchSpace = async (id: number) => { + // Send DELETE request to the API + try { + 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")}`, + }, + } + ); -
    -
    -

    Your Search Spaces

    - - - - - -
    + if (!response.ok) { + toast.error("Failed to delete search space"); + throw new Error("Failed to delete search space"); + } -
    - {searchSpaces && searchSpaces.map((space) => ( - - + // Refresh the search spaces list after successful deletion + refreshSearchSpaces(); + } catch (error) { + console.error("Error deleting search space:", error); + toast.error("An error occurred while deleting the search space"); + return; + } + toast.success("Search space deleted successfully"); + }; - - -
    -
    - {space.name} -
    -
    -
    e.preventDefault()}> - - - - - - - Delete Search Space - - Are you sure you want to delete "{space.name}"? This action cannot be undone. - All documents, chats, and podcasts in this search space will be permanently deleted. - - - - Cancel - handleDeleteSearchSpace(space.id)} - className="bg-destructive hover:bg-destructive/90" - > - Delete - - - - -
    -
    -
    - -
    -
    -

    {space.name}

    -

    {space.description}

    -
    -
    - {/* {space.title} */} - Created {formatDate(space.created_at)} -
    -
    -
    - + return ( + + +
    +
    + +
    +

    SurfSense Dashboard

    +

    Welcome to your SurfSense dashboard.

    +
    +
    +
    + + +
    +
    -
    - - ))} +
    +
    +

    Your Search Spaces

    + + + + + +
    - {searchSpaces.length === 0 && ( - -
    - -
    -

    No search spaces found

    -

    Create your first search space to get started

    - - - -
    - )} +
    + {searchSpaces && + searchSpaces.map((space) => ( + + + + +
    +
    + {space.name} +
    +
    +
    e.preventDefault()}> + + + + + + + Delete Search Space + + Are you sure you want to delete "{space.name}"? This + action cannot be undone. All documents, chats, and podcasts in + this search space will be permanently deleted. + + + + Cancel + handleDeleteSearchSpace(space.id)} + className="bg-destructive hover:bg-destructive/90" + > + Delete + + + + +
    +
    +
    - {searchSpaces.length > 0 && ( - - - -
    - - Add New Search Space -
    - -
    -
    - )} -
    -
    -
    - - ) -} +
    +
    +

    {space.name}

    +

    + {space.description} +

    +
    +
    + {/* {space.title} */} + Created {formatDate(space.created_at)} +
    +
    +
    + + + + ))} -export default DashboardPage + {searchSpaces.length === 0 && ( + +
    + +
    +

    No search spaces found

    +

    + Create your first search space to get started +

    + + + +
    + )} + + {searchSpaces.length > 0 && ( + + + +
    + + Add New Search Space +
    + +
    +
    + )} +
    +
    +
    + + ); +}; + +export default DashboardPage; diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx index 9bade8239..059779d31 100644 --- a/surfsense_web/app/dashboard/searchspaces/page.tsx +++ b/surfsense_web/app/dashboard/searchspaces/page.tsx @@ -5,48 +5,51 @@ import { SearchSpaceForm } from "@/components/search-space-form"; import { motion } from "framer-motion"; import { useRouter } from "next/navigation"; export default function SearchSpacesPage() { - const router = useRouter(); - const handleCreateSearchSpace = async (data: { name: string; description: string }) => { - try { - 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) { - toast.error("Failed to create search space"); - throw new Error("Failed to create search space"); - } - - const result = await response.json(); - - toast.success("Search space created successfully", { - description: `"${data.name}" has been created.`, - }); + const router = useRouter(); + const handleCreateSearchSpace = async (data: { name: string; description: string }) => { + try { + 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), + } + ); - router.push(`/dashboard`); - - return result; - } catch (error: any) { - console.error('Error creating search space:', error); - throw error; - } - }; + if (!response.ok) { + toast.error("Failed to create search space"); + throw new Error("Failed to create search space"); + } - return ( - -
    - -
    -
    - ); -} \ No newline at end of file + const result = await response.json(); + + toast.success("Search space created successfully", { + description: `"${data.name}" has been created.`, + }); + + router.push(`/dashboard`); + + return result; + } catch (error: any) { + console.error("Error creating search space:", error); + throw error; + } + }; + + return ( + +
    + +
    +
    + ); +} diff --git a/surfsense_web/app/docs/[[...slug]]/page.tsx b/surfsense_web/app/docs/[[...slug]]/page.tsx index 6c8574d87..1280dfb25 100644 --- a/surfsense_web/app/docs/[[...slug]]/page.tsx +++ b/surfsense_web/app/docs/[[...slug]]/page.tsx @@ -1,46 +1,37 @@ -import { source } from '@/lib/source'; -import { - DocsBody, - DocsDescription, - DocsPage, - DocsTitle, -} from 'fumadocs-ui/page'; -import { notFound } from 'next/navigation'; -import { getMDXComponents } from '@/mdx-components'; - -export default async function Page(props: { - params: Promise<{ slug?: string[] }>; -}) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); - - const MDX = page.data.body; - - return ( - - {page.data.title} - {page.data.description} - - - - - ); +import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; +import { notFound } from "next/navigation"; +import { source } from "@/lib/source"; +import { getMDXComponents } from "@/mdx-components"; + +export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + const MDX = page.data.body; + + return ( + + {page.data.title} + {page.data.description} + + + + + ); } - + export async function generateStaticParams() { - return source.generateParams(); + return source.generateParams(); +} + +export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { + const params = await props.params; + const page = source.getPage(params.slug); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + }; } - -export async function generateMetadata(props: { - params: Promise<{ slug?: string[] }>; -}) { - const params = await props.params; - const page = source.getPage(params.slug); - if (!page) notFound(); - - return { - title: page.data.title, - description: page.data.description, - }; -} \ No newline at end of file diff --git a/surfsense_web/app/docs/layout.tsx b/surfsense_web/app/docs/layout.tsx index e818c1f68..27dd5de7a 100644 --- a/surfsense_web/app/docs/layout.tsx +++ b/surfsense_web/app/docs/layout.tsx @@ -1,12 +1,12 @@ -import { source } from '@/lib/source'; -import { DocsLayout } from 'fumadocs-ui/layouts/docs'; -import type { ReactNode } from 'react'; -import { baseOptions } from '@/app/layout.config'; - +import { DocsLayout } from "fumadocs-ui/layouts/docs"; +import type { ReactNode } from "react"; +import { baseOptions } from "@/app/layout.config"; +import { source } from "@/lib/source"; + export default function Layout({ children }: { children: ReactNode }) { - return ( - - {children} - - ); -} \ No newline at end of file + return ( + + {children} + + ); +} diff --git a/surfsense_web/app/globals.css b/surfsense_web/app/globals.css index 88bd7ced7..a1ee277c6 100644 --- a/surfsense_web/app/globals.css +++ b/surfsense_web/app/globals.css @@ -1,160 +1,160 @@ -@import 'tailwindcss'; -@import 'fumadocs-ui/css/neutral.css'; -@import 'fumadocs-ui/css/preset.css'; +@import "tailwindcss"; +@import "fumadocs-ui/css/neutral.css"; +@import "fumadocs-ui/css/preset.css"; @plugin "tailwindcss-animate"; @custom-variant dark (&:is(.dark *)); @theme { - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); } :root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); - --syntax-bg: #f5f5f5; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --syntax-bg: #f5f5f5; } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); - --syntax-bg: #1e1e1e; + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); + --syntax-bg: #1e1e1e; } @theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); } @layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } - :root { - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + :root { + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } - .dark { - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } + .dark { + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } } button { - cursor: pointer; + cursor: pointer; } -@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}' \ No newline at end of file +@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}'; diff --git a/surfsense_web/app/layout.config.tsx b/surfsense_web/app/layout.config.tsx index ef0500157..b1b07fd02 100644 --- a/surfsense_web/app/layout.config.tsx +++ b/surfsense_web/app/layout.config.tsx @@ -1,7 +1,7 @@ -import { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; - +import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; + export const baseOptions: BaseLayoutProps = { - nav: { - title: 'SurfSense Documentation', - }, -}; \ No newline at end of file + nav: { + title: "SurfSense Documentation", + }, +}; diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 33bf10bb4..7e5ef3f91 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -1,108 +1,102 @@ import type { Metadata } from "next"; import "./globals.css"; -import { cn } from "@/lib/utils"; +import { RootProvider } from "fumadocs-ui/provider"; import { Roboto } from "next/font/google"; - -import { Toaster } from "@/components/ui/sonner"; import { ThemeProvider } from "@/components/theme/theme-provider"; -import { RootProvider } from 'fumadocs-ui/provider'; +import { Toaster } from "@/components/ui/sonner"; +import { cn } from "@/lib/utils"; -const roboto = Roboto({ - subsets: ["latin"], - weight: ["400", "500", "700"], - display: 'swap', - variable: '--font-roboto', +const roboto = Roboto({ + subsets: ["latin"], + weight: ["400", "500", "700"], + display: "swap", + variable: "--font-roboto", }); export const metadata: Metadata = { - title: "SurfSense – Customizable AI Research & Knowledge Management Assistant", - description: - "SurfSense is an AI-powered research assistant that integrates with tools like Notion, GitHub, Slack, and more to help you efficiently manage, search, and chat with your documents. Generate podcasts, perform hybrid search, and unlock insights from your knowledge base.", - keywords: [ - "SurfSense", - "AI research assistant", - "AI knowledge management", - "AI document assistant", - "customizable AI assistant", - "notion integration", - "slack integration", - "github integration", - "hybrid search", - "vector search", - "RAG", - "LangChain", - "FastAPI", - "LLM apps", - "AI document chat", - "knowledge management AI", - "AI-powered document search", - "personal AI assistant", - "AI research tools", - "AI podcast generator", - "AI knowledge base", - "AI document assistant tools", - "AI-powered search assistant", - ], - openGraph: { - title: "SurfSense – AI Research & Knowledge Management Assistant", - description: - "Connect your documents and tools like Notion, Slack, GitHub, and more to your private AI assistant. SurfSense offers powerful search, document chat, podcast generation, and RAG APIs to enhance your workflow.", - url: "https://surfsense.net", - siteName: "SurfSense", - type: "website", - images: [ - { - url: "https://surfsense.net/og-image.png", - width: 1200, - height: 630, - alt: "SurfSense AI Research Assistant", - }, - ], - locale: "en_US", - }, - twitter: { - card: "summary_large_image", - title: "SurfSense – AI Assistant for Research & Knowledge Management", - description: - "Have your own NotebookLM or Perplexity, but better. SurfSense connects external tools, allows chat with your documents, and generates fast, high-quality podcasts.", - creator: "https://surfsense.net", - site: "https://surfsense.net", - images: [ - { - url: "https://surfsense.net/og-image-twitter.png", - width: 1200, - height: 630, - alt: "SurfSense AI Assistant Preview", - }, - ], - } + title: "SurfSense – Customizable AI Research & Knowledge Management Assistant", + description: + "SurfSense is an AI-powered research assistant that integrates with tools like Notion, GitHub, Slack, and more to help you efficiently manage, search, and chat with your documents. Generate podcasts, perform hybrid search, and unlock insights from your knowledge base.", + keywords: [ + "SurfSense", + "AI research assistant", + "AI knowledge management", + "AI document assistant", + "customizable AI assistant", + "notion integration", + "slack integration", + "github integration", + "hybrid search", + "vector search", + "RAG", + "LangChain", + "FastAPI", + "LLM apps", + "AI document chat", + "knowledge management AI", + "AI-powered document search", + "personal AI assistant", + "AI research tools", + "AI podcast generator", + "AI knowledge base", + "AI document assistant tools", + "AI-powered search assistant", + ], + openGraph: { + title: "SurfSense – AI Research & Knowledge Management Assistant", + description: + "Connect your documents and tools like Notion, Slack, GitHub, and more to your private AI assistant. SurfSense offers powerful search, document chat, podcast generation, and RAG APIs to enhance your workflow.", + url: "https://surfsense.net", + siteName: "SurfSense", + type: "website", + images: [ + { + url: "https://surfsense.net/og-image.png", + width: 1200, + height: 630, + alt: "SurfSense AI Research Assistant", + }, + ], + locale: "en_US", + }, + twitter: { + card: "summary_large_image", + title: "SurfSense – AI Assistant for Research & Knowledge Management", + description: + "Have your own NotebookLM or Perplexity, but better. SurfSense connects external tools, allows chat with your documents, and generates fast, high-quality podcasts.", + creator: "https://surfsense.net", + site: "https://surfsense.net", + images: [ + { + url: "https://surfsense.net/og-image-twitter.png", + width: 1200, + height: 630, + alt: "SurfSense AI Assistant Preview", + }, + ], + }, }; export default async function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - - {children} - - - - - - ); + return ( + + + + + {children} + + + + + + ); } diff --git a/surfsense_web/app/login/AmbientBackground.tsx b/surfsense_web/app/login/AmbientBackground.tsx index 6b61d517d..b71135ac0 100644 --- a/surfsense_web/app/login/AmbientBackground.tsx +++ b/surfsense_web/app/login/AmbientBackground.tsx @@ -2,42 +2,42 @@ import React from "react"; export const AmbientBackground = () => { - return ( -
    -
    -
    -
    -
    - ); -}; \ No newline at end of file + return ( +
    +
    +
    +
    +
    + ); +}; diff --git a/surfsense_web/app/login/GoogleLoginButton.tsx b/surfsense_web/app/login/GoogleLoginButton.tsx index ee5deb3a9..00bb669d4 100644 --- a/surfsense_web/app/login/GoogleLoginButton.tsx +++ b/surfsense_web/app/login/GoogleLoginButton.tsx @@ -6,87 +6,94 @@ import { Logo } from "@/components/Logo"; import { AmbientBackground } from "./AmbientBackground"; export function GoogleLoginButton() { - const handleGoogleLogin = () => { - // Redirect to Google OAuth authorization URL - fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`) - .then((response) => { - if (!response.ok) { - throw new Error('Failed to get authorization URL'); - } - return response.json(); - }) - .then((data) => { - if (data.authorization_url) { - window.location.href = data.authorization_url; - } else { - console.error('No authorization URL received'); - } - }) - .catch((error) => { - console.error('Error during Google login:', error); - }); - } - return ( -
    - -
    - -

    - Welcome Back -

    - - - - - - - - -
    -

    - SurfSense Cloud is currently in development. Check Docs for more information on Self-Hosted version. -

    -
    -
    -
    - - -
    -
    -
    -
    -
    -
    - - Continue with Google -
    -
    -
    - ); -} \ No newline at end of file + const handleGoogleLogin = () => { + // Redirect to Google OAuth authorization URL + fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/google/authorize`) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to get authorization URL"); + } + return response.json(); + }) + .then((data) => { + if (data.authorization_url) { + window.location.href = data.authorization_url; + } else { + console.error("No authorization URL received"); + } + }) + .catch((error) => { + console.error("Error during Google login:", error); + }); + }; + return ( +
    + +
    + +

    + Welcome Back +

    + + + + + + + + +
    +

    + SurfSense Cloud is currently in development. Check{" "} + + Docs + {" "} + for more information on Self-Hosted version. +

    +
    + + + + +
    +
    +
    +
    +
    +
    + + Continue with Google +
    +
    +
    + ); +} diff --git a/surfsense_web/app/login/LocalLoginForm.tsx b/surfsense_web/app/login/LocalLoginForm.tsx index 345941802..d35f9080f 100644 --- a/surfsense_web/app/login/LocalLoginForm.tsx +++ b/surfsense_web/app/login/LocalLoginForm.tsx @@ -1,114 +1,124 @@ "use client"; -import React, { useState, useEffect } from "react"; +import type React from "react"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; export function LocalLoginForm() { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [error, setError] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [authType, setAuthType] = useState(null); - const router = useRouter(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [authType, setAuthType] = useState(null); + const router = useRouter(); - useEffect(() => { - // Get the auth type from environment variables - setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"); - }, []); + useEffect(() => { + // Get the auth type from environment variables + setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"); + }, []); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(""); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(""); - try { - // Create form data for the API request - const formData = new URLSearchParams(); - formData.append("username", username); - formData.append("password", password); - formData.append("grant_type", "password"); + try { + // Create form data for the API request + const formData = new URLSearchParams(); + formData.append("username", username); + formData.append("password", password); + formData.append("grant_type", "password"); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/jwt/login`, - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: formData.toString(), - } - ); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/jwt/login`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formData.toString(), + } + ); - const data = await response.json(); + const data = await response.json(); - if (!response.ok) { - throw new Error(data.detail || "Failed to login"); - } + if (!response.ok) { + throw new Error(data.detail || "Failed to login"); + } - router.push("/auth/callback?token=" + data.access_token); - } catch (err: any) { - setError(err.message || "An error occurred during login"); - } finally { - setIsLoading(false); - } - }; + router.push("/auth/callback?token=" + data.access_token); + } catch (err: any) { + setError(err.message || "An error occurred during login"); + } finally { + setIsLoading(false); + } + }; - return ( -
    -
    - {error && ( -
    - {error} -
    - )} - -
    - - setUsername(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" - /> -
    + return ( +
    + + {error && ( +
    + {error} +
    + )} -
    - - setPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" - /> -
    +
    + + setUsername(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" + /> +
    - - +
    + + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" + /> +
    - {authType === "LOCAL" && ( -
    -

    - Don't have an account?{" "} - - Register here - -

    -
    - )} -
    - ); -} \ No newline at end of file + + + + {authType === "LOCAL" && ( +
    +

    + Don't have an account?{" "} + + Register here + +

    +
    + )} +
    + ); +} diff --git a/surfsense_web/app/login/page.tsx b/surfsense_web/app/login/page.tsx index 65fa0b873..fb9deb028 100644 --- a/surfsense_web/app/login/page.tsx +++ b/surfsense_web/app/login/page.tsx @@ -9,81 +9,81 @@ import { useSearchParams } from "next/navigation"; import { Loader2 } from "lucide-react"; function LoginContent() { - const [authType, setAuthType] = useState(null); - const [registrationSuccess, setRegistrationSuccess] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const searchParams = useSearchParams(); + const [authType, setAuthType] = useState(null); + const [registrationSuccess, setRegistrationSuccess] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const searchParams = useSearchParams(); - useEffect(() => { - // Check if the user was redirected from registration - if (searchParams.get("registered") === "true") { - setRegistrationSuccess(true); - } + useEffect(() => { + // Check if the user was redirected from registration + if (searchParams.get("registered") === "true") { + setRegistrationSuccess(true); + } - // Get the auth type from environment variables - setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"); - setIsLoading(false); - }, [searchParams]); + // Get the auth type from environment variables + setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"); + setIsLoading(false); + }, [searchParams]); - // Show loading state while determining auth type - if (isLoading) { - return ( -
    - -
    - -
    - - Loading... -
    -
    -
    - ); - } + // Show loading state while determining auth type + if (isLoading) { + return ( +
    + +
    + +
    + + Loading... +
    +
    +
    + ); + } - if (authType === "GOOGLE") { - return ; - } + if (authType === "GOOGLE") { + return ; + } - return ( -
    - -
    - -

    - Sign In -

    + return ( +
    + +
    + +

    + Sign In +

    - {registrationSuccess && ( -
    - Registration successful! You can now sign in with your credentials. -
    - )} + {registrationSuccess && ( +
    + Registration successful! You can now sign in with your credentials. +
    + )} - -
    -
    - ); + +
    +
    + ); } // Loading fallback for Suspense const LoadingFallback = () => ( -
    - -
    - -
    - - Loading... -
    -
    -
    +
    + +
    + +
    + + Loading... +
    +
    +
    ); export default function LoginPage() { - return ( - }> - - - ); -} \ No newline at end of file + return ( + }> + + + ); +} diff --git a/surfsense_web/app/onboard/page.tsx b/surfsense_web/app/onboard/page.tsx index 416aa8ac3..6b9d06341 100644 --- a/surfsense_web/app/onboard/page.tsx +++ b/surfsense_web/app/onboard/page.tsx @@ -1,227 +1,238 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Progress } from '@/components/ui/progress'; -import { CheckCircle, ArrowRight, ArrowLeft, Bot, Sparkles, Zap, Brain } from 'lucide-react'; -import { Logo } from '@/components/Logo'; -import { useLLMConfigs, useLLMPreferences } from '@/hooks/use-llm-configs'; -import { AddProviderStep } from '@/components/onboard/add-provider-step'; -import { AssignRolesStep } from '@/components/onboard/assign-roles-step'; -import { CompletionStep } from '@/components/onboard/completion-step'; +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { motion, AnimatePresence } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { CheckCircle, ArrowRight, ArrowLeft, Bot, Sparkles, Zap, Brain } from "lucide-react"; +import { Logo } from "@/components/Logo"; +import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; +import { AddProviderStep } from "@/components/onboard/add-provider-step"; +import { AssignRolesStep } from "@/components/onboard/assign-roles-step"; +import { CompletionStep } from "@/components/onboard/completion-step"; const TOTAL_STEPS = 3; const OnboardPage = () => { - const router = useRouter(); - const { llmConfigs, loading: configsLoading, refreshConfigs } = useLLMConfigs(); - const { preferences, loading: preferencesLoading, isOnboardingComplete, refreshPreferences } = useLLMPreferences(); - const [currentStep, setCurrentStep] = useState(1); - const [hasUserProgressed, setHasUserProgressed] = useState(false); + const router = useRouter(); + const { llmConfigs, loading: configsLoading, refreshConfigs } = useLLMConfigs(); + const { + preferences, + loading: preferencesLoading, + isOnboardingComplete, + refreshPreferences, + } = useLLMPreferences(); + const [currentStep, setCurrentStep] = useState(1); + const [hasUserProgressed, setHasUserProgressed] = useState(false); - // Check if user is authenticated - useEffect(() => { - const token = localStorage.getItem('surfsense_bearer_token'); - if (!token) { - router.push('/login'); - return; - } - }, [router]); + // Check if user is authenticated + useEffect(() => { + const token = localStorage.getItem("surfsense_bearer_token"); + if (!token) { + router.push("/login"); + return; + } + }, [router]); - // Track if user has progressed beyond step 1 - useEffect(() => { - if (currentStep > 1) { - setHasUserProgressed(true); - } - }, [currentStep]); + // Track if user has progressed beyond step 1 + useEffect(() => { + if (currentStep > 1) { + setHasUserProgressed(true); + } + }, [currentStep]); - // Redirect to dashboard if onboarding is already complete and user hasn't progressed (fresh page load) - useEffect(() => { - if (!preferencesLoading && isOnboardingComplete() && !hasUserProgressed) { - router.push('/dashboard'); - } - }, [preferencesLoading, isOnboardingComplete, hasUserProgressed, router]); + // Redirect to dashboard if onboarding is already complete and user hasn't progressed (fresh page load) + useEffect(() => { + if (!preferencesLoading && isOnboardingComplete() && !hasUserProgressed) { + router.push("/dashboard"); + } + }, [preferencesLoading, isOnboardingComplete, hasUserProgressed, router]); + const progress = (currentStep / TOTAL_STEPS) * 100; + const stepTitles = ["Add LLM Provider", "Assign LLM Roles", "Setup Complete"]; - const progress = (currentStep / TOTAL_STEPS) * 100; + const stepDescriptions = [ + "Configure your first model provider", + "Assign specific roles to your LLM configurations", + "You're all set to start using SurfSense!", + ]; - const stepTitles = [ - "Add LLM Provider", - "Assign LLM Roles", - "Setup Complete" - ]; + const canProceedToStep2 = !configsLoading && llmConfigs.length > 0; + const canProceedToStep3 = + !preferencesLoading && + preferences.long_context_llm_id && + preferences.fast_llm_id && + preferences.strategic_llm_id; - const stepDescriptions = [ - "Configure your first model provider", - "Assign specific roles to your LLM configurations", - "You're all set to start using SurfSense!" - ]; + const handleNext = () => { + if (currentStep < TOTAL_STEPS) { + setCurrentStep(currentStep + 1); + } + }; - const canProceedToStep2 = !configsLoading && llmConfigs.length > 0; - const canProceedToStep3 = !preferencesLoading && preferences.long_context_llm_id && preferences.fast_llm_id && preferences.strategic_llm_id; - + const handlePrevious = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } + }; + const handleComplete = () => { + router.push("/dashboard"); + }; - const handleNext = () => { - if (currentStep < TOTAL_STEPS) { - setCurrentStep(currentStep + 1); - } - }; + if (configsLoading || preferencesLoading) { + return ( +
    + + + +

    Loading your configuration...

    +
    +
    +
    + ); + } - const handlePrevious = () => { - if (currentStep > 1) { - setCurrentStep(currentStep - 1); - } - }; + return ( +
    + + {/* Header */} +
    +
    + +

    Welcome to SurfSense

    +
    +

    + Let's configure your SurfSense to get started +

    +
    - const handleComplete = () => { - router.push('/dashboard'); - }; + {/* Progress */} + + +
    +
    + Step {currentStep} of {TOTAL_STEPS} +
    +
    {Math.round(progress)}% Complete
    +
    + +
    + {Array.from({ length: TOTAL_STEPS }, (_, i) => { + const stepNum = i + 1; + const isCompleted = stepNum < currentStep; + const isCurrent = stepNum === currentStep; - if (configsLoading || preferencesLoading) { - return ( -
    - - - -

    Loading your configuration...

    -
    -
    -
    - ); - } + return ( +
    +
    + {isCompleted ? : stepNum} +
    +
    +

    + {stepTitles[i]} +

    +
    +
    + ); + })} +
    +
    +
    - return ( -
    - - {/* Header */} -
    -
    - -

    Welcome to SurfSense

    -
    -

    Let's configure your SurfSense to get started

    -
    + {/* Step Content */} + + + + {currentStep === 1 && } + {currentStep === 2 && } + {currentStep === 3 && } + {stepTitles[currentStep - 1]} + + + {stepDescriptions[currentStep - 1]} + + + + + + {currentStep === 1 && ( + + )} + {currentStep === 2 && } + {currentStep === 3 && } + + + + - {/* Progress */} - - -
    -
    Step {currentStep} of {TOTAL_STEPS}
    -
    {Math.round(progress)}% Complete
    -
    - -
    - {Array.from({ length: TOTAL_STEPS }, (_, i) => { - const stepNum = i + 1; - const isCompleted = stepNum < currentStep; - const isCurrent = stepNum === currentStep; - - return ( -
    -
    - {isCompleted ? : stepNum} -
    -
    -

    - {stepTitles[i]} -

    -
    -
    - ); - })} -
    -
    -
    + {/* Navigation */} +
    + - {/* Step Content */} - - - - {currentStep === 1 && } - {currentStep === 2 && } - {currentStep === 3 && } - {stepTitles[currentStep - 1]} - - - {stepDescriptions[currentStep - 1]} - - - - - - {currentStep === 1 && } - {currentStep === 2 && } - {currentStep === 3 && } - - - - +
    + {currentStep < TOTAL_STEPS && ( + + )} - {/* Navigation */} -
    - - -
    - {currentStep < TOTAL_STEPS && ( - - )} - - {currentStep === TOTAL_STEPS && ( - - )} -
    -
    - -
    - ); + {currentStep === TOTAL_STEPS && ( + + )} +
    +
    +
    +
    + ); }; -export default OnboardPage; \ No newline at end of file +export default OnboardPage; diff --git a/surfsense_web/app/page.tsx b/surfsense_web/app/page.tsx index 6e5f6166d..e5501d35f 100644 --- a/surfsense_web/app/page.tsx +++ b/surfsense_web/app/page.tsx @@ -1,16 +1,15 @@ "use client"; -import React from "react"; -import { Navbar } from "@/components/Navbar"; -import { motion } from "framer-motion"; -import { ModernHeroWithGradients } from "@/components/ModernHeroWithGradients"; + import { Footer } from "@/components/Footer"; +import { ModernHeroWithGradients } from "@/components/ModernHeroWithGradients"; +import { Navbar } from "@/components/Navbar"; export default function HomePage() { - return ( -
    - - -
    -
    - ); -} \ No newline at end of file + return ( +
    + + +
    +
    + ); +} diff --git a/surfsense_web/app/privacy/page.tsx b/surfsense_web/app/privacy/page.tsx index 309c6c315..ffe1ba9e3 100644 --- a/surfsense_web/app/privacy/page.tsx +++ b/surfsense_web/app/privacy/page.tsx @@ -1,146 +1,190 @@ -import { Metadata } from "next"; +import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Privacy Policy | SurfSense", - description: "Privacy Policy for SurfSense application", + title: "Privacy Policy | SurfSense", + description: "Privacy Policy for SurfSense application", }; export default function PrivacyPolicy() { - return ( -
    -

    Privacy Policy

    - -
    -

    Last updated: {new Date().toLocaleDateString()}

    + return ( +
    +

    Privacy Policy

    -
    -

    1. Introduction

    -

    - Welcome to SurfSense. We respect your privacy and are committed to protecting your personal data. - This privacy policy will inform you about how we look after your personal data when you visit our - website and tell you about your privacy rights and how the law protects you. -

    -

    - By using our services, you acknowledge that you have read and understood this Privacy Policy. We reserve - the right to modify this policy at any time, and such modifications shall be effective immediately upon - posting the modified policy on this website. -

    -
    +
    +

    Last updated: {new Date().toLocaleDateString()}

    -
    -

    2. Data We Collect

    -

    - We may collect, use, store and transfer different kinds of personal data about you which we have - grouped together as follows: -

    -
      -
    • Identity Data includes first name, last name, username or similar identifier.
    • -
    • Contact Data includes email address and telephone numbers.
    • -
    • Technical Data includes internet protocol (IP) address, your login data, browser type and version, - time zone setting and location, browser plug-in types and versions, operating system and platform, - and other technology on the devices you use to access this website.
    • -
    • Usage Data includes information about how you use our website and services.
    • -
    • Surf Data includes information about surf sessions, preferences, and equipment settings.
    • -
    • Marketing and Communications Data includes your preferences in receiving marketing from us and your communication preferences.
    • -
    • Aggregated Data which may be derived from your personal data but is not considered personal data as it does not directly or indirectly reveal your identity.
    • -
    -

    - We may also collect, use and share Aggregated Data such as statistical or demographic data for any purpose. - Aggregated Data may be derived from your personal data but is not considered personal data in law as this data - does not directly or indirectly reveal your identity. -

    -
    +
    +

    1. Introduction

    +

    + Welcome to SurfSense. We respect your privacy and are committed to protecting your + personal data. This privacy policy will inform you about how we look after your personal + data when you visit our website and tell you about your privacy rights and how the law + protects you. +

    +

    + By using our services, you acknowledge that you have read and understood this Privacy + Policy. We reserve the right to modify this policy at any time, and such modifications + shall be effective immediately upon posting the modified policy on this website. +

    +
    -
    -

    3. How We Use Your Data

    -

    - We will only use your personal data when the law allows us to. Most commonly, we will use your - personal data in the following circumstances: -

    -
      -
    • Where we need to perform the contract we are about to enter into or have entered into with you.
    • -
    • Where it is necessary for our legitimate interests (or those of a third party) and your interests - and fundamental rights do not override those interests.
    • -
    • Where we need to comply with a legal obligation.
    • -
    • To provide and maintain our services, including to monitor the usage of our service.
    • -
    • To improve our services, products, marketing, and customer relationships and experiences.
    • -
    • To communicate with you about updates, security alerts, and support messages.
    • -
    • To provide customer support and respond to your requests or inquiries.
    • -
    • For business transfers, such as in connection with a merger, sale of company assets, financing, or acquisition.
    • -
    -

    - We may use your information for marketing purposes, such as sending you information about our products, services, - promotions, and events. You can opt-out of receiving these communications at any time. -

    -
    +
    +

    2. Data We Collect

    +

    + We may collect, use, store and transfer different kinds of personal data about you which + we have grouped together as follows: +

    +
      +
    • + Identity Data includes first name, last name, username or similar + identifier. +
    • +
    • + Contact Data includes email address and telephone numbers. +
    • +
    • + Technical Data includes internet protocol (IP) address, your login + data, browser type and version, time zone setting and location, browser plug-in types + and versions, operating system and platform, and other technology on the devices you + use to access this website. +
    • +
    • + Usage Data includes information about how you use our website and + services. +
    • +
    • + Surf Data includes information about surf sessions, preferences, and + equipment settings. +
    • +
    • + Marketing and Communications Data includes your preferences in + receiving marketing from us and your communication preferences. +
    • +
    • + Aggregated Data which may be derived from your personal data but is + not considered personal data as it does not directly or indirectly reveal your + identity. +
    • +
    +

    + We may also collect, use and share Aggregated Data such as statistical or demographic + data for any purpose. Aggregated Data may be derived from your personal data but is not + considered personal data in law as this data does not directly or indirectly reveal your + identity. +

    +
    -
    -

    4. Data Security

    -

    - We have put in place appropriate security measures to prevent your personal data from being - accidentally lost, used or accessed in an unauthorized way, altered or disclosed. In addition, - we limit access to your personal data to those employees, agents, contractors and other third - parties who have a business need to know. -

    -

    - While we implement safeguards designed to protect your information, no security system is impenetrable - and due to the inherent nature of the Internet, we cannot guarantee that information, during transmission - through the Internet or while stored on our systems, is absolutely safe from intrusion by others. -

    -
    +
    +

    3. How We Use Your Data

    +

    + We will only use your personal data when the law allows us to. Most commonly, we will + use your personal data in the following circumstances: +

    +
      +
    • + Where we need to perform the contract we are about to enter into or have entered into + with you. +
    • +
    • + Where it is necessary for our legitimate interests (or those of a third party) and + your interests and fundamental rights do not override those interests. +
    • +
    • Where we need to comply with a legal obligation.
    • +
    • + To provide and maintain our services, including to monitor the usage of our service. +
    • +
    • + To improve our services, products, marketing, and customer relationships and + experiences. +
    • +
    • To communicate with you about updates, security alerts, and support messages.
    • +
    • To provide customer support and respond to your requests or inquiries.
    • +
    • + For business transfers, such as in connection with a merger, sale of company assets, + financing, or acquisition. +
    • +
    +

    + We may use your information for marketing purposes, such as sending you information + about our products, services, promotions, and events. You can opt-out of receiving these + communications at any time. +

    +
    -
    -

    5. Data Retention

    -

    - We will only retain your personal data for as long as necessary to fulfill the purposes we collected it for, - including for the purposes of satisfying any legal, accounting, or reporting requirements. To determine the appropriate - retention period for personal data, we consider the amount, nature, and sensitivity of the personal data, the - potential risk of harm from unauthorized use or disclosure of your personal data, the purposes for which we process - your personal data and whether we can achieve those purposes through other means, and the applicable legal requirements. -

    -
    +
    +

    4. Data Security

    +

    + We have put in place appropriate security measures to prevent your personal data from + being accidentally lost, used or accessed in an unauthorized way, altered or disclosed. + In addition, we limit access to your personal data to those employees, agents, + contractors and other third parties who have a business need to know. +

    +

    + While we implement safeguards designed to protect your information, no security system + is impenetrable and due to the inherent nature of the Internet, we cannot guarantee that + information, during transmission through the Internet or while stored on our systems, is + absolutely safe from intrusion by others. +

    +
    -
    -

    6. Your Legal Rights

    -

    - Under certain circumstances, you have rights under data protection laws in relation to your personal data, including: -

    -
      -
    • The right to request access to your personal data.
    • -
    • The right to request correction of your personal data.
    • -
    • The right to request erasure of your personal data.
    • -
    • The right to object to processing of your personal data.
    • -
    • The right to request restriction of processing your personal data.
    • -
    • The right to request transfer of your personal data.
    • -
    • The right to withdraw consent.
    • -
    -

    - Please note that these rights are not absolute, and we may be entitled to refuse requests where exceptions apply. - If you wish to exercise any of the rights set out above, please contact us. We may need to request specific - information from you to help us confirm your identity and ensure your right to access your personal data. -

    -
    +
    +

    5. Data Retention

    +

    + We will only retain your personal data for as long as necessary to fulfill the purposes + we collected it for, including for the purposes of satisfying any legal, accounting, or + reporting requirements. To determine the appropriate retention period for personal data, + we consider the amount, nature, and sensitivity of the personal data, the potential risk + of harm from unauthorized use or disclosure of your personal data, the purposes for + which we process your personal data and whether we can achieve those purposes through + other means, and the applicable legal requirements. +

    +
    -
    -

    7. Third-Party Services

    -

    - Our service may contain links to other websites that are not operated by us. If you click on a third-party link, - you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. - We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party - sites or services. -

    -
    +
    +

    6. Your Legal Rights

    +

    + Under certain circumstances, you have rights under data protection laws in relation to + your personal data, including: +

    +
      +
    • The right to request access to your personal data.
    • +
    • The right to request correction of your personal data.
    • +
    • The right to request erasure of your personal data.
    • +
    • The right to object to processing of your personal data.
    • +
    • The right to request restriction of processing your personal data.
    • +
    • The right to request transfer of your personal data.
    • +
    • The right to withdraw consent.
    • +
    +

    + Please note that these rights are not absolute, and we may be entitled to refuse + requests where exceptions apply. If you wish to exercise any of the rights set out + above, please contact us. We may need to request specific information from you to help + us confirm your identity and ensure your right to access your personal data. +

    +
    -
    -

    8. Contact Us

    -

    - If you have any questions about this privacy policy or our privacy practices, please contact us at: -

    -

    - Email: vermarohanfinal@gmail.com -

    -
    -
    -
    - ); -} \ No newline at end of file +
    +

    7. Third-Party Services

    +

    + Our service may contain links to other websites that are not operated by us. If you + click on a third-party link, you will be directed to that third party's site. We + strongly advise you to review the Privacy Policy of every site you visit. We have no + control over and assume no responsibility for the content, privacy policies, or + practices of any third-party sites or services. +

    +
    + +
    +

    8. Contact Us

    +

    + If you have any questions about this privacy policy or our privacy practices, please + contact us at: +

    +

    + Email: vermarohanfinal@gmail.com +

    +
    +
    +
    + ); +} diff --git a/surfsense_web/app/register/page.tsx b/surfsense_web/app/register/page.tsx index 33e012608..2327480c6 100644 --- a/surfsense_web/app/register/page.tsx +++ b/surfsense_web/app/register/page.tsx @@ -1,149 +1,159 @@ "use client"; -import React, { useState, useEffect } from "react"; +import type React from "react"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Logo } from "@/components/Logo"; import { AmbientBackground } from "../login/AmbientBackground"; export default function RegisterPage() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [error, setError] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); - // Check authentication type and redirect if not LOCAL - useEffect(() => { - const authType = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"; - if (authType !== "LOCAL") { - router.push("/login"); - } - }, [router]); + // Check authentication type and redirect if not LOCAL + useEffect(() => { + const authType = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE"; + if (authType !== "LOCAL") { + router.push("/login"); + } + }, [router]); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - // Form validation - if (password !== confirmPassword) { - setError("Passwords do not match"); - return; - } + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - setIsLoading(true); - setError(""); + // Form validation + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } - try { - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email, - password, - is_active: true, - is_superuser: false, - is_verified: false, - }), - } - ); + setIsLoading(true); + setError(""); - const data = await response.json(); + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + password, + is_active: true, + is_superuser: false, + is_verified: false, + }), + }); - if (!response.ok) { - throw new Error(data.detail || "Registration failed"); - } + const data = await response.json(); - // Redirect to login page after successful registration - router.push("/login?registered=true"); - } catch (err: any) { - setError(err.message || "An error occurred during registration"); - } finally { - setIsLoading(false); - } - }; + if (!response.ok) { + throw new Error(data.detail || "Registration failed"); + } - return ( -
    - -
    - -

    - Create an Account -

    + // Redirect to login page after successful registration + router.push("/login?registered=true"); + } catch (err: any) { + setError(err.message || "An error occurred during registration"); + } finally { + setIsLoading(false); + } + }; -
    -
    - {error && ( -
    - {error} -
    - )} - -
    - - setEmail(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" - /> -
    + return ( +
    + +
    + +

    + Create an Account +

    -
    - - setPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" - /> -
    +
    + + {error && ( +
    + {error} +
    + )} -
    - - setConfirmPassword(e.target.value)} - className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" - /> -
    +
    + + setEmail(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" + /> +
    - - +
    + + setPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" + /> +
    -
    -

    - Already have an account?{" "} - - Sign in - -

    -
    -
    -
    -
    - ); -} \ No newline at end of file +
    + + setConfirmPassword(e.target.value)} + className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" + /> +
    + + + + +
    +

    + Already have an account?{" "} + + Sign in + +

    +
    +
    +
    +
    + ); +} diff --git a/surfsense_web/app/settings/page.tsx b/surfsense_web/app/settings/page.tsx index b9e36e751..3d797741b 100644 --- a/surfsense_web/app/settings/page.tsx +++ b/surfsense_web/app/settings/page.tsx @@ -1,72 +1,72 @@ "use client"; -import React from 'react'; -import { useRouter } from 'next/navigation'; // Add this import -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Separator } from '@/components/ui/separator'; -import { Bot, Settings, Brain, ArrowLeft } from 'lucide-react'; // Import ArrowLeft icon -import { ModelConfigManager } from '@/components/settings/model-config-manager'; -import { LLMRoleManager } from '@/components/settings/llm-role-manager'; +import React from "react"; +import { useRouter } from "next/navigation"; // Add this import +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Separator } from "@/components/ui/separator"; +import { Bot, Settings, Brain, ArrowLeft } from "lucide-react"; // Import ArrowLeft icon +import { ModelConfigManager } from "@/components/settings/model-config-manager"; +import { LLMRoleManager } from "@/components/settings/llm-role-manager"; export default function SettingsPage() { - const router = useRouter(); // Initialize router + const router = useRouter(); // Initialize router - return ( -
    -
    -
    - {/* Header Section */} -
    -
    - {/* Back Button */} - -
    - -
    -
    -

    Settings

    -

    - Manage your LLM configurations and role assignments. -

    -
    -
    - -
    + return ( +
    +
    +
    + {/* Header Section */} +
    +
    + {/* Back Button */} + +
    + +
    +
    +

    Settings

    +

    + Manage your LLM configurations and role assignments. +

    +
    +
    + +
    - {/* Settings Content */} - -
    - - - - Model Configs - Models - - - - LLM Roles - Roles - - -
    + {/* Settings Content */} + +
    + + + + Model Configs + Models + + + + LLM Roles + Roles + + +
    - - - + + + - - - -
    -
    -
    -
    - ); -} \ No newline at end of file + + + + +
    +
    +
    + ); +} diff --git a/surfsense_web/app/sitemap.ts b/surfsense_web/app/sitemap.ts index cbd35fba1..1380fc561 100644 --- a/surfsense_web/app/sitemap.ts +++ b/surfsense_web/app/sitemap.ts @@ -1,48 +1,48 @@ -import type { MetadataRoute } from 'next' - +import type { MetadataRoute } from "next"; + export default function sitemap(): MetadataRoute.Sitemap { - return [ - { - url: 'https://www.surfsense.net/', - lastModified: new Date(), - changeFrequency: 'yearly', - priority: 1, - }, - { - url: 'https://www.surfsense.net/privacy', - lastModified: new Date(), - changeFrequency: 'monthly', - priority: 0.9, - }, - { - url: 'https://www.surfsense.net/terms', - lastModified: new Date(), - changeFrequency: 'monthly', - priority: 0.9, - }, - { - url: 'https://www.surfsense.net/docs', - lastModified: new Date(), - changeFrequency: 'weekly', - priority: 0.9, - }, - { - url: 'https://www.surfsense.net/docs/installation', - lastModified: new Date(), - changeFrequency: 'weekly', - priority: 0.9, - }, - { - url: 'https://www.surfsense.net/docs/docker-installation', - lastModified: new Date(), - changeFrequency: 'weekly', - priority: 0.9, - }, - { - url: 'https://www.surfsense.net/docs/manual-installation', - lastModified: new Date(), - changeFrequency: 'weekly', - priority: 0.9, - }, - ] + return [ + { + url: "https://www.surfsense.net/", + lastModified: new Date(), + changeFrequency: "yearly", + priority: 1, + }, + { + url: "https://www.surfsense.net/privacy", + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.9, + }, + { + url: "https://www.surfsense.net/terms", + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.9, + }, + { + url: "https://www.surfsense.net/docs", + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.9, + }, + { + url: "https://www.surfsense.net/docs/installation", + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.9, + }, + { + url: "https://www.surfsense.net/docs/docker-installation", + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.9, + }, + { + url: "https://www.surfsense.net/docs/manual-installation", + lastModified: new Date(), + changeFrequency: "weekly", + priority: 0.9, + }, + ]; } diff --git a/surfsense_web/app/terms/page.tsx b/surfsense_web/app/terms/page.tsx index 5d8bdb395..e1f317a4c 100644 --- a/surfsense_web/app/terms/page.tsx +++ b/surfsense_web/app/terms/page.tsx @@ -1,204 +1,225 @@ -import { Metadata } from "next"; +import type { Metadata } from "next"; export const metadata: Metadata = { - title: "Terms of Service | SurfSense", - description: "Terms of Service for SurfSense application", + title: "Terms of Service | SurfSense", + description: "Terms of Service for SurfSense application", }; export default function TermsOfService() { - return ( -
    -

    Terms of Service

    - -
    -

    Last updated: {new Date().toLocaleDateString()}

    + return ( +
    +

    Terms of Service

    -
    -

    1. Introduction

    -

    - Welcome to SurfSense. These Terms of Service govern your access to and use of the SurfSense website and services. - By accessing or using our services, you agree to be bound by these Terms. -

    -

    - Please read these Terms carefully before using our Services. By using our Services, you agree that these Terms - will govern your relationship with us. If you do not agree to these Terms, please refrain from using our Services. -

    -
    +
    +

    Last updated: {new Date().toLocaleDateString()}

    -
    -

    2. Using Our Services

    -

    - You must follow any policies made available to you within the Services. You may use our Services only as - permitted by law. We may suspend or stop providing our Services to you if you do not comply with our terms or - policies or if we are investigating suspected misconduct. -

    -

    - Using our Services does not give you ownership of any intellectual property rights in our Services or the - content you access. You may not use content from our Services unless you obtain permission from its owner or - are otherwise permitted by law. -

    -

    - We reserve the right to remove any content that we reasonably believe violates these Terms, infringes any - intellectual property right, is abusive, illegal, or otherwise objectionable. -

    -
    +
    +

    1. Introduction

    +

    + Welcome to SurfSense. These Terms of Service govern your access to and use of the + SurfSense website and services. By accessing or using our services, you agree to be + bound by these Terms. +

    +

    + Please read these Terms carefully before using our Services. By using our Services, you + agree that these Terms will govern your relationship with us. If you do not agree to + these Terms, please refrain from using our Services. +

    +
    -
    -

    3. Your Account

    -

    - To use some of our services, you may need to create an account. You are responsible for safeguarding the - password that you use to access the services and for any activities or actions under your password. -

    -

    - You must provide accurate and complete information when creating your account. You agree to update your - information to keep it accurate and complete. You are responsible for maintaining the confidentiality of your - account and password, including restricting access to your computer and/or account. -

    -

    - We reserve the right to refuse service, terminate accounts, remove or edit content, or cancel orders at - our sole discretion. -

    -
    +
    +

    2. Using Our Services

    +

    + You must follow any policies made available to you within the Services. You may use our + Services only as permitted by law. We may suspend or stop providing our Services to you + if you do not comply with our terms or policies or if we are investigating suspected + misconduct. +

    +

    + Using our Services does not give you ownership of any intellectual property rights in + our Services or the content you access. You may not use content from our Services unless + you obtain permission from its owner or are otherwise permitted by law. +

    +

    + We reserve the right to remove any content that we reasonably believe violates these + Terms, infringes any intellectual property right, is abusive, illegal, or otherwise + objectionable. +

    +
    -
    -

    4. Privacy and Copyright Protection

    -

    - Our privacy policies explain how we treat your personal data and protect your privacy when you use our - Services. By using our Services, you agree that SurfSense can use such data in accordance with our privacy policies. -

    -

    - We respond to notices of alleged copyright infringement and terminate accounts of repeat infringers according - to the process set out in applicable copyright laws. -

    -
    +
    +

    3. Your Account

    +

    + To use some of our services, you may need to create an account. You are responsible for + safeguarding the password that you use to access the services and for any activities or + actions under your password. +

    +

    + You must provide accurate and complete information when creating your account. You agree + to update your information to keep it accurate and complete. You are responsible for + maintaining the confidentiality of your account and password, including restricting + access to your computer and/or account. +

    +

    + We reserve the right to refuse service, terminate accounts, remove or edit content, or + cancel orders at our sole discretion. +

    +
    -
    -

    5. License and Intellectual Property

    -

    - SurfSense gives you a personal, worldwide, royalty-free, non-assignable and non-exclusive license to use the - software provided to you as part of the Services. This license is for the sole purpose of enabling you to use - and enjoy the benefit of the Services as provided by SurfSense, in the manner permitted by these terms. -

    -

    - All content included in or made available through our Services—such as text, graphics, logos, button icons, - images, audio clips, digital downloads, data compilations, and software—is the property of SurfSense or its - content suppliers and is protected by international copyright, trademark, and other intellectual property laws. -

    -

    - By submitting, posting, or displaying content on or through our Services, you grant us a worldwide, non-exclusive, - royalty-free license to use, reproduce, modify, adapt, publish, translate, create derivative works from, distribute, - and display such content in any media for the purpose of providing and improving our Services. -

    -
    +
    +

    4. Privacy and Copyright Protection

    +

    + Our privacy policies explain how we treat your personal data and protect your privacy + when you use our Services. By using our Services, you agree that SurfSense can use such + data in accordance with our privacy policies. +

    +

    + We respond to notices of alleged copyright infringement and terminate accounts of repeat + infringers according to the process set out in applicable copyright laws. +

    +
    -
    -

    6. Modifying and Terminating our Services

    -

    - We are constantly changing and improving our Services. We may add or remove functionalities or features, and - we may suspend or stop a Service altogether. You can stop using our Services at any time. SurfSense may also - stop providing Services to you, or add or create new limits on our Services at any time. -

    -

    - We believe that you own your data and preserving your access to such data is important. If we discontinue a Service, - where reasonably possible, we will give you reasonable advance notice and a chance to get information out of that Service. -

    -

    - We reserve the right to modify these Terms at any time. If we make material changes to these Terms, we will notify - you by email or by posting a notice on our website before the changes become effective. Your continued use of our - Services after the effective date of such changes constitutes your acceptance of the modified Terms. -

    -
    +
    +

    5. License and Intellectual Property

    +

    + SurfSense gives you a personal, worldwide, royalty-free, non-assignable and + non-exclusive license to use the software provided to you as part of the Services. This + license is for the sole purpose of enabling you to use and enjoy the benefit of the + Services as provided by SurfSense, in the manner permitted by these terms. +

    +

    + All content included in or made available through our Services—such as text, graphics, + logos, button icons, images, audio clips, digital downloads, data compilations, and + software—is the property of SurfSense or its content suppliers and is protected by + international copyright, trademark, and other intellectual property laws. +

    +

    + By submitting, posting, or displaying content on or through our Services, you grant us a + worldwide, non-exclusive, royalty-free license to use, reproduce, modify, adapt, + publish, translate, create derivative works from, distribute, and display such content + in any media for the purpose of providing and improving our Services. +

    +
    -
    -

    7. Warranties and Disclaimers

    -

    - We provide our Services using a commercially reasonable level of skill and care and we hope that you will - enjoy using them. But there are certain things that we don't promise about our Services. -

    -

    - OTHER THAN AS EXPRESSLY SET OUT IN THESE TERMS OR ADDITIONAL TERMS, NEITHER SURFSENSE NOR ITS SUPPLIERS OR DISTRIBUTORS - MAKE ANY SPECIFIC PROMISES ABOUT THE SERVICES. FOR EXAMPLE, WE DON'T MAKE ANY COMMITMENTS ABOUT THE CONTENT WITHIN THE - SERVICES, THE SPECIFIC FUNCTIONS OF THE SERVICES, OR THEIR RELIABILITY, AVAILABILITY, OR ABILITY TO MEET YOUR NEEDS. - WE PROVIDE THE SERVICES "AS IS". -

    -

    - SOME JURISDICTIONS PROVIDE FOR CERTAIN WARRANTIES, LIKE THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A - PARTICULAR PURPOSE, AND NON-INFRINGEMENT. TO THE EXTENT PERMITTED BY LAW, WE EXCLUDE ALL WARRANTIES. -

    -
    +
    +

    6. Modifying and Terminating our Services

    +

    + We are constantly changing and improving our Services. We may add or remove + functionalities or features, and we may suspend or stop a Service altogether. You can + stop using our Services at any time. SurfSense may also stop providing Services to you, + or add or create new limits on our Services at any time. +

    +

    + We believe that you own your data and preserving your access to such data is important. + If we discontinue a Service, where reasonably possible, we will give you reasonable + advance notice and a chance to get information out of that Service. +

    +

    + We reserve the right to modify these Terms at any time. If we make material changes to + these Terms, we will notify you by email or by posting a notice on our website before + the changes become effective. Your continued use of our Services after the effective + date of such changes constitutes your acceptance of the modified Terms. +

    +
    -
    -

    8. Liability for our Services

    -

    - WHEN PERMITTED BY LAW, SURFSENSE, AND SURFSENSE'S SUPPLIERS AND DISTRIBUTORS, WILL NOT BE RESPONSIBLE FOR - LOST PROFITS, REVENUES, OR DATA, FINANCIAL LOSSES OR INDIRECT, SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR - PUNITIVE DAMAGES. -

    -

    - TO THE EXTENT PERMITTED BY LAW, THE TOTAL LIABILITY OF SURFSENSE, AND ITS SUPPLIERS AND DISTRIBUTORS, FOR ANY - CLAIMS UNDER THESE TERMS, INCLUDING FOR ANY IMPLIED WARRANTIES, IS LIMITED TO THE AMOUNT YOU PAID US TO USE THE - SERVICES (OR, IF WE CHOOSE, TO SUPPLYING YOU THE SERVICES AGAIN). -

    -

    - IN ALL CASES, SURFSENSE, AND ITS SUPPLIERS AND DISTRIBUTORS, WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE THAT IS - NOT REASONABLY FORESEEABLE. -

    -
    +
    +

    7. Warranties and Disclaimers

    +

    + We provide our Services using a commercially reasonable level of skill and care and we + hope that you will enjoy using them. But there are certain things that we don't promise + about our Services. +

    +

    + OTHER THAN AS EXPRESSLY SET OUT IN THESE TERMS OR ADDITIONAL TERMS, NEITHER SURFSENSE + NOR ITS SUPPLIERS OR DISTRIBUTORS MAKE ANY SPECIFIC PROMISES ABOUT THE SERVICES. FOR + EXAMPLE, WE DON'T MAKE ANY COMMITMENTS ABOUT THE CONTENT WITHIN THE SERVICES, THE + SPECIFIC FUNCTIONS OF THE SERVICES, OR THEIR RELIABILITY, AVAILABILITY, OR ABILITY TO + MEET YOUR NEEDS. WE PROVIDE THE SERVICES "AS IS". +

    +

    + SOME JURISDICTIONS PROVIDE FOR CERTAIN WARRANTIES, LIKE THE IMPLIED WARRANTY OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. TO THE EXTENT + PERMITTED BY LAW, WE EXCLUDE ALL WARRANTIES. +

    +
    -
    -

    9. Indemnification

    -

    - You agree to defend, indemnify, and hold harmless SurfSense, its affiliates, and their respective officers, directors, - employees, and agents from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses, or - fees (including reasonable attorneys' fees) arising out of or relating to your violation of these Terms or your use of - the Services, including, but not limited to, any use of the Services' content, services, and products other than as - expressly authorized in these Terms. -

    -
    +
    +

    8. Liability for our Services

    +

    + WHEN PERMITTED BY LAW, SURFSENSE, AND SURFSENSE'S SUPPLIERS AND DISTRIBUTORS, WILL NOT + BE RESPONSIBLE FOR LOST PROFITS, REVENUES, OR DATA, FINANCIAL LOSSES OR INDIRECT, + SPECIAL, CONSEQUENTIAL, EXEMPLARY, OR PUNITIVE DAMAGES. +

    +

    + TO THE EXTENT PERMITTED BY LAW, THE TOTAL LIABILITY OF SURFSENSE, AND ITS SUPPLIERS AND + DISTRIBUTORS, FOR ANY CLAIMS UNDER THESE TERMS, INCLUDING FOR ANY IMPLIED WARRANTIES, IS + LIMITED TO THE AMOUNT YOU PAID US TO USE THE SERVICES (OR, IF WE CHOOSE, TO SUPPLYING + YOU THE SERVICES AGAIN). +

    +

    + IN ALL CASES, SURFSENSE, AND ITS SUPPLIERS AND DISTRIBUTORS, WILL NOT BE LIABLE FOR ANY + LOSS OR DAMAGE THAT IS NOT REASONABLY FORESEEABLE. +

    +
    -
    -

    10. Dispute Resolution

    -

    - Any dispute arising out of or relating to these Terms, including the validity, interpretation, breach, or termination - thereof, shall be resolved by arbitration in accordance with the rules of the arbitration authority in the jurisdiction - where SurfSense operates. The arbitration shall be conducted by one arbitrator, in the English language, and the - decision of the arbitrator shall be final and binding on the parties. -

    -

    - You agree that any dispute resolution proceedings will be conducted only on an individual basis and not in a class, - consolidated, or representative action. If for any reason a claim proceeds in court rather than in arbitration, you - waive any right to a jury trial. -

    -
    +
    +

    9. Indemnification

    +

    + You agree to defend, indemnify, and hold harmless SurfSense, its affiliates, and their + respective officers, directors, employees, and agents from and against any claims, + liabilities, damages, judgments, awards, losses, costs, expenses, or fees (including + reasonable attorneys' fees) arising out of or relating to your violation of these Terms + or your use of the Services, including, but not limited to, any use of the Services' + content, services, and products other than as expressly authorized in these Terms. +

    +
    -
    -

    11. About these Terms

    -

    - We may modify these terms or any additional terms that apply to a Service to, for example, reflect changes to - the law or changes to our Services. You should look at the terms regularly. If you do not agree to the modified - terms for a Service, you should discontinue your use of that Service. -

    -

    - If there is a conflict between these terms and the additional terms, the additional terms will control for that conflict. - These terms control the relationship between SurfSense and you. They do not create any third-party beneficiary rights. -

    -

    - If you do not comply with these terms, and we don't take action right away, this doesn't mean that we are giving up - any rights that we may have (such as taking action in the future). If it turns out that a particular term is not - enforceable, this will not affect any other terms. -

    -
    +
    +

    10. Dispute Resolution

    +

    + Any dispute arising out of or relating to these Terms, including the validity, + interpretation, breach, or termination thereof, shall be resolved by arbitration in + accordance with the rules of the arbitration authority in the jurisdiction where + SurfSense operates. The arbitration shall be conducted by one arbitrator, in the English + language, and the decision of the arbitrator shall be final and binding on the parties. +

    +

    + You agree that any dispute resolution proceedings will be conducted only on an + individual basis and not in a class, consolidated, or representative action. If for any + reason a claim proceeds in court rather than in arbitration, you waive any right to a + jury trial. +

    +
    -
    -

    12. Contact Us

    -

    - If you have any questions about these Terms, please contact us at: -

    -

    - Email: vermarohanfinal@gmail.com -

    -
    -
    -
    - ); -} \ No newline at end of file +
    +

    11. About these Terms

    +

    + We may modify these terms or any additional terms that apply to a Service to, for + example, reflect changes to the law or changes to our Services. You should look at the + terms regularly. If you do not agree to the modified terms for a Service, you should + discontinue your use of that Service. +

    +

    + If there is a conflict between these terms and the additional terms, the additional + terms will control for that conflict. These terms control the relationship between + SurfSense and you. They do not create any third-party beneficiary rights. +

    +

    + If you do not comply with these terms, and we don't take action right away, this doesn't + mean that we are giving up any rights that we may have (such as taking action in the + future). If it turns out that a particular term is not enforceable, this will not affect + any other terms. +

    +
    + +
    +

    12. Contact Us

    +

    If you have any questions about these Terms, please contact us at:

    +

    + Email: vermarohanfinal@gmail.com +

    +
    +
    +
    + ); +} diff --git a/surfsense_web/components.json b/surfsense_web/components.json index 335484f94..6e57ca9e3 100644 --- a/surfsense_web/components.json +++ b/surfsense_web/components.json @@ -1,21 +1,21 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "app/globals.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "iconLibrary": "lucide" -} \ No newline at end of file + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/surfsense_web/components/Footer.tsx b/surfsense_web/components/Footer.tsx index 437d91104..6c56f9cbf 100644 --- a/surfsense_web/components/Footer.tsx +++ b/surfsense_web/components/Footer.tsx @@ -1,106 +1,97 @@ "use client"; import { cn } from "@/lib/utils"; import { - IconBrandDiscord, - IconBrandGithub, - IconBrandLinkedin, - IconBrandTwitter, + IconBrandDiscord, + IconBrandGithub, + IconBrandLinkedin, + IconBrandTwitter, } from "@tabler/icons-react"; import Link from "next/link"; -import React from "react"; +import type React from "react"; export function Footer() { - const pages = [ - { - title: "Privacy", - href: "/privacy", - }, - { - title: "Terms", - href: "/terms", - }, - ]; + const pages = [ + { + title: "Privacy", + href: "/privacy", + }, + { + title: "Terms", + href: "/terms", + }, + ]; - return ( -
    -
    -
    -
    -
    - SurfSense -
    -
    + return ( +
    +
    +
    +
    +
    + SurfSense +
    +
    -
      - {pages.map((page, idx) => ( -
    • - - {page.title} - -
    • - ))} -
    +
      + {pages.map((page, idx) => ( +
    • + + {page.title} + +
    • + ))} +
    - -
    -
    -

    - © SurfSense 2025 -

    -
    - - - - - - - - - - - - -
    -
    -
    -
    - ); + +
    +
    +

    + © SurfSense 2025 +

    +
    + + + + + + + + + + + + +
    +
    +
    +
    + ); } -const GridLineHorizontal = ({ - className, - offset, -}: { - className?: string; - offset?: string; -}) => { - return ( -
    - ); -}; \ No newline at end of file +const GridLineHorizontal = ({ className, offset }: { className?: string; offset?: string }) => { + return ( +
    + ); +}; diff --git a/surfsense_web/components/Logo.tsx b/surfsense_web/components/Logo.tsx index cd2e5aa50..5ebe6760b 100644 --- a/surfsense_web/components/Logo.tsx +++ b/surfsense_web/components/Logo.tsx @@ -5,18 +5,9 @@ import Image from "next/image"; import { cn } from "@/lib/utils"; export const Logo = ({ className }: { className?: string }) => { - return ( - - logo - - ); + return ( + + logo + + ); }; - diff --git a/surfsense_web/components/ModernHeroWithGradients.tsx b/surfsense_web/components/ModernHeroWithGradients.tsx index 40539fb74..f85d53c1b 100644 --- a/surfsense_web/components/ModernHeroWithGradients.tsx +++ b/surfsense_web/components/ModernHeroWithGradients.tsx @@ -7,532 +7,532 @@ import { motion } from "framer-motion"; import { Logo } from "./Logo"; export function ModernHeroWithGradients() { - return ( -
    -
    -
    - - - - - - + return ( +
    +
    +
    + + + + + + -
    -
    - - MODSetter%2FSurfSense | Trendshift - -
    - - - Documentation - - {/* Import the Logo component or define it in this file */} -
    -
    - -
    -

    - SurfSense -

    -
    -

    - A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, YouTube, GitHub, Discord, and more. -

    -
    - - - Discord - - - - GitHub - -
    -
    -
    -
    -
    - ); +
    +
    + + MODSetter%2FSurfSense | Trendshift + +
    + + + Documentation + + {/* Import the Logo component or define it in this file */} +
    +
    + +
    +

    + SurfSense +

    +
    +

    + A Customizable AI Research Agent just like NotebookLM or Perplexity, but connected to + external sources such as search engines (Tavily, LinkUp), Slack, Linear, Notion, + YouTube, GitHub, Discord, and more. +

    +
    + + + Discord + + + + GitHub + +
    +
    +
    +
    +
    + ); } const TopLines = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; const BottomLines = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; const SideLines = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); }; const BottomGradient = ({ className }: { className?: string }) => { - return ( - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + ); }; const TopGradient = ({ className }: { className?: string }) => { - return ( - - - - - - - - - - - - - - - - - - - ); + return ( + + + + + + + + + + + + + + + + + + + ); }; const DarkModeGradient = ({ className }: { className?: string } = {}) => { - return ( -
    -
    -
    -
    -
    - ); -}; \ No newline at end of file + return ( +
    +
    +
    +
    +
    + ); +}; diff --git a/surfsense_web/components/Navbar.tsx b/surfsense_web/components/Navbar.tsx index d61e0083a..67d3dd2e0 100644 --- a/surfsense_web/components/Navbar.tsx +++ b/surfsense_web/components/Navbar.tsx @@ -1,290 +1,289 @@ "use client"; import { cn } from "@/lib/utils"; import { IconMenu2, IconX, IconBrandGoogleFilled, IconUser } from "@tabler/icons-react"; -import { - motion, - AnimatePresence, - useScroll, - useMotionValueEvent, -} from "framer-motion"; +import { motion, AnimatePresence, useScroll, useMotionValueEvent } from "framer-motion"; import Link from "next/link"; -import React, { useRef, useState } from "react"; +import type React from "react"; +import { useRef, useState } from "react"; import { Button } from "./ui/button"; import { Logo } from "./Logo"; import { ThemeTogglerComponent } from "./theme/theme-toggle"; interface NavbarProps { - navItems: { - name: string; - link: string; - }[]; - visible: boolean; + navItems: { + name: string; + link: string; + }[]; + visible: boolean; } export const Navbar = () => { - const navItems = [ - { - name: "Docs", - link: "/docs", - }, - // { - // name: "Product", - // link: "/#product", - // }, - // { - // name: "Pricing", - // link: "/#pricing", - // }, - ]; + const navItems = [ + { + name: "Docs", + link: "/docs", + }, + // { + // name: "Product", + // link: "/#product", + // }, + // { + // name: "Pricing", + // link: "/#pricing", + // }, + ]; - const ref = useRef(null); - const { scrollY } = useScroll({ - target: ref, - offset: ["start start", "end start"], - }); - const [visible, setVisible] = useState(false); + const ref = useRef(null); + const { scrollY } = useScroll({ + target: ref, + offset: ["start start", "end start"], + }); + const [visible, setVisible] = useState(false); - useMotionValueEvent(scrollY, "change", (latest) => { - if (latest > 100) { - setVisible(true); - } else { - setVisible(false); - } - }); + useMotionValueEvent(scrollY, "change", (latest) => { + if (latest > 100) { + setVisible(true); + } else { + setVisible(false); + } + }); - return ( - - - - - ); + return ( + + + + + ); }; const DesktopNav = ({ navItems, visible }: NavbarProps) => { - const [hoveredIndex, setHoveredIndex] = useState(null); + const [hoveredIndex, setHoveredIndex] = useState(null); - const handleGoogleLogin = () => { - // Redirect to the login page - window.location.href = '/login'; - }; + const handleGoogleLogin = () => { + // Redirect to the login page + window.location.href = "/login"; + }; - return ( - setHoveredIndex(null)} - animate={{ - backdropFilter: "blur(16px)", - background: visible - ? "rgba(var(--background-rgb), 0.8)" - : "rgba(var(--background-rgb), 0.6)", - width: visible ? "38%" : "80%", - height: visible ? "48px" : "64px", - y: visible ? 8 : 0, - }} - initial={{ - width: "80%", - height: "64px", - background: "rgba(var(--background-rgb), 0.6)", - }} - transition={{ - type: "spring", - stiffness: 400, - damping: 30, - }} - className={cn( - "hidden lg:flex flex-row self-center items-center justify-between py-2 mx-auto px-6 rounded-full relative z-[60] backdrop-saturate-[1.8]", - visible ? "border dark:border-white/10 border-gray-300/30" : "border-0" - )} - style={{ - "--background-rgb": "var(--tw-dark) ? '0, 0, 0' : '255, 255, 255'", - } as React.CSSProperties} - > -
    - - SurfSense -
    -
    - - {navItems.map((navItem, idx) => ( - setHoveredIndex(idx)} - className="relative" - > - - {navItem.name} - {hoveredIndex === idx && ( - - )} - - - ))} - - - - {!visible && ( - - - - )} - -
    -
    - ); + return ( + setHoveredIndex(null)} + animate={{ + backdropFilter: "blur(16px)", + background: visible + ? "rgba(var(--background-rgb), 0.8)" + : "rgba(var(--background-rgb), 0.6)", + width: visible ? "38%" : "80%", + height: visible ? "48px" : "64px", + y: visible ? 8 : 0, + }} + initial={{ + width: "80%", + height: "64px", + background: "rgba(var(--background-rgb), 0.6)", + }} + transition={{ + type: "spring", + stiffness: 400, + damping: 30, + }} + className={cn( + "hidden lg:flex flex-row self-center items-center justify-between py-2 mx-auto px-6 rounded-full relative z-[60] backdrop-saturate-[1.8]", + visible ? "border dark:border-white/10 border-gray-300/30" : "border-0" + )} + style={ + { + "--background-rgb": "var(--tw-dark) ? '0, 0, 0' : '255, 255, 255'", + } as React.CSSProperties + } + > +
    + + SurfSense +
    +
    + + {navItems.map((navItem, idx) => ( + setHoveredIndex(idx)} + className="relative" + > + + {navItem.name} + {hoveredIndex === idx && ( + + )} + + + ))} + + + + {!visible && ( + + + + )} + +
    +
    + ); }; const MobileNav = ({ navItems, visible }: NavbarProps) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); - const handleGoogleLogin = () => { - // Redirect to the login page - window.location.href = "./login"; - }; + const handleGoogleLogin = () => { + // Redirect to the login page + window.location.href = "./login"; + }; - return ( - <> - -
    - -
    - - {open ? ( - setOpen(!open)} /> - ) : ( - setOpen(!open)} - /> - )} -
    -
    + return ( + <> + +
    + +
    + + {open ? ( + setOpen(!open)} /> + ) : ( + setOpen(!open)} + /> + )} +
    +
    - - {open && ( - - {navItems.map( - (navItem: { link: string; name: string }, idx: number) => ( - setOpen(false)} - className="relative dark:text-white/90 text-gray-800 hover:text-gray-900 dark:hover:text-white transition-colors" - > - {navItem.name} - - ) - )} - - - )} - -
    - - ); -}; \ No newline at end of file + + {open && ( + + {navItems.map((navItem: { link: string; name: string }, idx: number) => ( + setOpen(false)} + className="relative dark:text-white/90 text-gray-800 hover:text-gray-900 dark:hover:text-white transition-colors" + > + {navItem.name} + + ))} + + + )} + +
    + + ); +}; diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx index 2c42c2bf9..770407feb 100644 --- a/surfsense_web/components/TokenHandler.tsx +++ b/surfsense_web/components/TokenHandler.tsx @@ -1,55 +1,55 @@ -'use client'; +"use client"; -import { useEffect } from 'react'; -import { useRouter, useSearchParams } from 'next/navigation'; +import { useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; interface TokenHandlerProps { - redirectPath?: string; // Path to redirect after storing token - tokenParamName?: string; // Name of the URL parameter containing the token - storageKey?: string; // Key to use when storing in localStorage + redirectPath?: string; // Path to redirect after storing token + tokenParamName?: string; // Name of the URL parameter containing the token + storageKey?: string; // Key to use when storing in localStorage } /** * Client component that extracts a token from URL parameters and stores it in localStorage - * + * * @param redirectPath - Path to redirect after storing token (default: '/') * @param tokenParamName - Name of the URL parameter containing the token (default: 'token') * @param storageKey - Key to use when storing in localStorage (default: 'auth_token') */ const TokenHandler = ({ - redirectPath = '/', - tokenParamName = 'token', - storageKey = 'surfsense_bearer_token' + redirectPath = "/", + tokenParamName = "token", + storageKey = "surfsense_bearer_token", }: TokenHandlerProps) => { - const router = useRouter(); - const searchParams = useSearchParams(); + const router = useRouter(); + const searchParams = useSearchParams(); - useEffect(() => { - // Only run on client-side - if (typeof window === 'undefined') return; + useEffect(() => { + // Only run on client-side + if (typeof window === "undefined") return; - // Get token from URL parameters - const token = searchParams.get(tokenParamName); + // Get token from URL parameters + const token = searchParams.get(tokenParamName); - if (token) { - try { - // Store token in localStorage - localStorage.setItem(storageKey, token); - // console.log(`Token stored in localStorage with key: ${storageKey}`); - - // Redirect to specified path - router.push(redirectPath); - } catch (error) { - console.error('Error storing token in localStorage:', error); - } - } - }, [searchParams, tokenParamName, storageKey, redirectPath, router]); + if (token) { + try { + // Store token in localStorage + localStorage.setItem(storageKey, token); + // console.log(`Token stored in localStorage with key: ${storageKey}`); - return ( -
    -

    Processing authentication...

    -
    - ); + // Redirect to specified path + router.push(redirectPath); + } catch (error) { + console.error("Error storing token in localStorage:", error); + } + } + }, [searchParams, tokenParamName, storageKey, redirectPath, router]); + + return ( +
    +

    Processing authentication...

    +
    + ); }; -export default TokenHandler; \ No newline at end of file +export default TokenHandler; diff --git a/surfsense_web/components/UserDropdown.tsx b/surfsense_web/components/UserDropdown.tsx index 68e4619fd..80c5fe1ad 100644 --- a/surfsense_web/components/UserDropdown.tsx +++ b/surfsense_web/components/UserDropdown.tsx @@ -1,101 +1,81 @@ -"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" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Button } from "@/components/ui/button" -import { useRouter, useParams } from "next/navigation" + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { useRouter, useParams } from "next/navigation"; export function UserDropdown({ - user, + user, }: { - user: { - name: string - email: string - avatar: string - } + user: { + name: string; + email: string; + avatar: string; + }; }) { - const router = useRouter() + const router = useRouter(); - const handleLogout = () => { - try { - if (typeof window !== 'undefined') { - localStorage.removeItem('surfsense_bearer_token'); - router.push('/'); - } - } catch (error) { - console.error('Error during logout:', error); - // Optionally, provide user feedback - if (typeof window !== 'undefined') { - alert('Logout failed. Please try again.'); - router.push('/'); - } - } - }; + const handleLogout = () => { + try { + if (typeof window !== "undefined") { + localStorage.removeItem("surfsense_bearer_token"); + router.push("/"); + } + } catch (error) { + console.error("Error during logout:", error); + // Optionally, provide user feedback + if (typeof window !== "undefined") { + alert("Logout failed. Please try again."); + router.push("/"); + } + } + }; - return ( - - - - - - -
    -

    {user.name}

    -

    - {user.email} -

    -
    -
    - - - - router.push(`/dashboard/api-key`)}> - - API Key - - - - - router.push(`/settings`)}> - - Settings - - - - Log out - -
    -
    - ) -} \ No newline at end of file + return ( + + + + + + +
    +

    {user.name}

    +

    {user.email}

    +
    +
    + + + router.push(`/dashboard/api-key`)}> + + API Key + + + + router.push(`/settings`)}> + + Settings + + + + Log out + +
    +
    + ); +} diff --git a/surfsense_web/components/chat/AnimatedEmptyState.tsx b/surfsense_web/components/chat/AnimatedEmptyState.tsx index 7b23ddb38..7b71f801d 100644 --- a/surfsense_web/components/chat/AnimatedEmptyState.tsx +++ b/surfsense_web/components/chat/AnimatedEmptyState.tsx @@ -2,159 +2,151 @@ import { cn } from "@/lib/utils"; import { Manrope } from "next/font/google"; -import React, { - useRef, - useEffect, - useReducer, - useMemo -} from "react"; +import React, { useRef, useEffect, useReducer, useMemo } from "react"; import { RoughNotation, RoughNotationGroup } from "react-rough-notation"; import { useInView } from "framer-motion"; import { useSidebar } from "@/components/ui/sidebar"; // Font configuration - could be moved to a global font config file -const manrope = Manrope({ - subsets: ["latin"], - weight: ["400", "700"], - display: "swap", // Optimize font loading - variable: "--font-manrope" +const manrope = Manrope({ + subsets: ["latin"], + weight: ["400", "700"], + display: "swap", // Optimize font loading + variable: "--font-manrope", }); // Constants for timing - makes it easier to adjust and more maintainable const TIMING = { - SIDEBAR_TRANSITION: 300, // Wait for sidebar transition + buffer - LAYOUT_SETTLE: 100, // Small delay to ensure layout is fully settled + SIDEBAR_TRANSITION: 300, // Wait for sidebar transition + buffer + LAYOUT_SETTLE: 100, // Small delay to ensure layout is fully settled } as const; // Animation configuration const ANIMATION_CONFIG = { - HIGHLIGHT: { - type: "highlight" as const, - animationDuration: 2000, - iterations: 3, - color: "#3b82f680", - multiline: true, - }, - UNDERLINE: { - type: "underline" as const, - animationDuration: 2000, - iterations: 3, - color: "#10b981", - }, + HIGHLIGHT: { + type: "highlight" as const, + animationDuration: 2000, + iterations: 3, + color: "#3b82f680", + multiline: true, + }, + UNDERLINE: { + type: "underline" as const, + animationDuration: 2000, + iterations: 3, + color: "#10b981", + }, } as const; // State management with useReducer for better organization interface HighlightState { - shouldShowHighlight: boolean; - layoutStable: boolean; + shouldShowHighlight: boolean; + layoutStable: boolean; } -type HighlightAction = - | { type: "SIDEBAR_CHANGED" } - | { type: "LAYOUT_STABILIZED" } - | { type: "SHOW_HIGHLIGHT" } - | { type: "HIDE_HIGHLIGHT" }; +type HighlightAction = + | { type: "SIDEBAR_CHANGED" } + | { type: "LAYOUT_STABILIZED" } + | { type: "SHOW_HIGHLIGHT" } + | { type: "HIDE_HIGHLIGHT" }; -const highlightReducer = ( - state: HighlightState, - action: HighlightAction -): HighlightState => { - switch (action.type) { - case "SIDEBAR_CHANGED": - return { - shouldShowHighlight: false, - layoutStable: false, - }; - case "LAYOUT_STABILIZED": - return { - ...state, - layoutStable: true, - }; - case "SHOW_HIGHLIGHT": - return { - ...state, - shouldShowHighlight: true, - }; - case "HIDE_HIGHLIGHT": - return { - ...state, - shouldShowHighlight: false, - }; - default: - return state; - } +const highlightReducer = (state: HighlightState, action: HighlightAction): HighlightState => { + switch (action.type) { + case "SIDEBAR_CHANGED": + return { + shouldShowHighlight: false, + layoutStable: false, + }; + case "LAYOUT_STABILIZED": + return { + ...state, + layoutStable: true, + }; + case "SHOW_HIGHLIGHT": + return { + ...state, + shouldShowHighlight: true, + }; + case "HIDE_HIGHLIGHT": + return { + ...state, + shouldShowHighlight: false, + }; + default: + return state; + } }; const initialState: HighlightState = { - shouldShowHighlight: false, - layoutStable: true, + shouldShowHighlight: false, + layoutStable: true, }; export function AnimatedEmptyState() { - const ref = useRef(null); - const isInView = useInView(ref); - const { state: sidebarState } = useSidebar(); - const [{ shouldShowHighlight, layoutStable }, dispatch] = useReducer( - highlightReducer, - initialState - ); + const ref = useRef(null); + const isInView = useInView(ref); + const { state: sidebarState } = useSidebar(); + const [{ shouldShowHighlight, layoutStable }, dispatch] = useReducer( + highlightReducer, + initialState + ); - // Memoize class names to prevent unnecessary recalculations - const headingClassName = useMemo(() => cn( - "text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 mb-6", - manrope.className, - ), []); + // Memoize class names to prevent unnecessary recalculations + const headingClassName = useMemo( + () => + cn( + "text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 mb-6", + manrope.className + ), + [] + ); - const paragraphClassName = useMemo(() => - "text-lg sm:text-xl text-neutral-600 dark:text-neutral-300 mb-8 max-w-2xl mx-auto", - []); + const paragraphClassName = useMemo( + () => "text-lg sm:text-xl text-neutral-600 dark:text-neutral-300 mb-8 max-w-2xl mx-auto", + [] + ); - // Handle sidebar state changes - useEffect(() => { - dispatch({ type: "SIDEBAR_CHANGED" }); + // Handle sidebar state changes + useEffect(() => { + dispatch({ type: "SIDEBAR_CHANGED" }); - const stabilizeTimer = setTimeout(() => { - dispatch({ type: "LAYOUT_STABILIZED" }); - }, TIMING.SIDEBAR_TRANSITION); + const stabilizeTimer = setTimeout(() => { + dispatch({ type: "LAYOUT_STABILIZED" }); + }, TIMING.SIDEBAR_TRANSITION); - return () => clearTimeout(stabilizeTimer); - }, [sidebarState]); + return () => clearTimeout(stabilizeTimer); + }, [sidebarState]); - // Handle highlight visibility based on layout stability and viewport visibility - useEffect(() => { - if (!layoutStable || !isInView) { - dispatch({ type: "HIDE_HIGHLIGHT" }); - return; - } + // Handle highlight visibility based on layout stability and viewport visibility + useEffect(() => { + if (!layoutStable || !isInView) { + dispatch({ type: "HIDE_HIGHLIGHT" }); + return; + } - const showTimer = setTimeout(() => { - dispatch({ type: "SHOW_HIGHLIGHT" }); - }, TIMING.LAYOUT_SETTLE); + const showTimer = setTimeout(() => { + dispatch({ type: "SHOW_HIGHLIGHT" }); + }, TIMING.LAYOUT_SETTLE); - return () => clearTimeout(showTimer); - }, [layoutStable, isInView]); + return () => clearTimeout(showTimer); + }, [layoutStable, isInView]); - return ( -
    -
    - -

    - - SurfSense - -

    + return ( +
    +
    + +

    + + SurfSense + +

    -

    - - Let's Start Surfing - {" "} - through your knowledge base. -

    -
    -
    -
    - ); +

    + Let's Start Surfing{" "} + through your knowledge base. +

    +
    +
    +
    + ); } diff --git a/surfsense_web/components/chat/ChatCitation.tsx b/surfsense_web/components/chat/ChatCitation.tsx index 14b61c675..b3095f967 100644 --- a/surfsense_web/components/chat/ChatCitation.tsx +++ b/surfsense_web/components/chat/ChatCitation.tsx @@ -1,17 +1,10 @@ "use client"; -import React from "react"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import type React from "react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { ExternalLink } from "lucide-react"; -export const CitationDisplay: React.FC<{ index: number; node: any }> = ({ - index, - node, -}) => { +export const CitationDisplay: React.FC<{ index: number; node: any }> = ({ index, node }) => { const truncateText = (text: string, maxLength: number = 200) => { if (text.length <= maxLength) return text; return text.substring(0, maxLength) + "..."; diff --git a/surfsense_web/components/chat/ChatFurtherQuestions.tsx b/surfsense_web/components/chat/ChatFurtherQuestions.tsx index 51285eda2..5dbbd246f 100644 --- a/surfsense_web/components/chat/ChatFurtherQuestions.tsx +++ b/surfsense_web/components/chat/ChatFurtherQuestions.tsx @@ -1,45 +1,36 @@ "use client"; import { SuggestedQuestions } from "@llamaindex/chat-ui/widgets"; -import { getAnnotationData, Message, useChatUI } from "@llamaindex/chat-ui"; +import { getAnnotationData, type Message, useChatUI } from "@llamaindex/chat-ui"; import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; -export const ChatFurtherQuestions: React.FC<{ message: Message }> = ({ - message, -}) => { - const annotations: string[][] = getAnnotationData( - message, - "FURTHER_QUESTIONS", - ); - const { append, requestData } = useChatUI(); +export const ChatFurtherQuestions: React.FC<{ message: Message }> = ({ message }) => { + const annotations: string[][] = getAnnotationData(message, "FURTHER_QUESTIONS"); + const { append, requestData } = useChatUI(); - if (annotations.length !== 1 || annotations[0].length === 0) { - return <>; - } + if (annotations.length !== 1 || annotations[0].length === 0) { + return <>; + } - return ( - - - - Further Suggested Questions - - - - - - - ); + return ( + + + + Further Suggested Questions + + + + + + + ); }; diff --git a/surfsense_web/components/chat/ChatInputGroup.tsx b/surfsense_web/components/chat/ChatInputGroup.tsx index 142f0bc30..5aca0f69c 100644 --- a/surfsense_web/components/chat/ChatInputGroup.tsx +++ b/surfsense_web/components/chat/ChatInputGroup.tsx @@ -21,14 +21,14 @@ import { import { Badge } from "@/components/ui/badge"; import { Suspense, useState, useCallback } from "react"; import { useParams } from "next/navigation"; -import { useDocuments, Document } from "@/hooks/use-documents"; +import { useDocuments, type Document } from "@/hooks/use-documents"; import { DocumentsDataTable } from "@/components/chat/DocumentsDataTable"; import { useSearchSourceConnectors } from "@/hooks/useSearchSourceConnectors"; import { getConnectorIcon, ConnectorButton as ConnectorButtonComponent, } from "@/components/chat/ConnectorComponents"; -import { ResearchMode } from "@/components/chat"; +import type { ResearchMode } from "@/components/chat"; import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; import React from "react"; @@ -45,7 +45,7 @@ const DocumentSelector = React.memo( const { documents, loading, isLoaded, fetchDocuments } = useDocuments( Number(search_space_id), - true, + true ); const handleOpenChange = useCallback( @@ -55,24 +55,21 @@ const DocumentSelector = React.memo( fetchDocuments(); } }, - [fetchDocuments, isLoaded], + [fetchDocuments, isLoaded] ); const handleSelectionChange = useCallback( (documents: Document[]) => { onSelectionChange?.(documents); }, - [onSelectionChange], + [onSelectionChange] ); const handleDone = useCallback(() => { setIsOpen(false); }, []); - const selectedCount = React.useMemo( - () => selectedDocuments.length, - [selectedDocuments.length], - ); + const selectedCount = React.useMemo(() => selectedDocuments.length, [selectedDocuments.length]); return ( @@ -90,9 +87,7 @@ const DocumentSelector = React.memo(
    - - Select Documents - + Select Documents Choose documents to include in your research context @@ -103,9 +98,7 @@ const DocumentSelector = React.memo(
    -

    - Loading documents... -

    +

    Loading documents...

    ) : isLoaded ? ( @@ -121,7 +114,7 @@ const DocumentSelector = React.memo(
    ); - }, + } ); DocumentSelector.displayName = "DocumentSelector"; @@ -146,7 +139,7 @@ const ConnectorSelector = React.memo( fetchConnectors(); } }, - [fetchConnectors, isLoaded], + [fetchConnectors, isLoaded] ); const handleConnectorToggle = useCallback( @@ -157,7 +150,7 @@ const ConnectorSelector = React.memo( : [...selectedConnectors, connectorType]; onSelectionChange?.(newSelection); }, - [selectedConnectors, onSelectionChange], + [selectedConnectors, onSelectionChange] ); const handleSelectAll = useCallback(() => { @@ -210,9 +203,7 @@ const ConnectorSelector = React.memo(
    {getConnectorIcon(connector.type)}
    - - {connector.name} - + {connector.name} {isSelected && }
    ); @@ -231,7 +222,7 @@ const ConnectorSelector = React.memo( ); - }, + } ); ConnectorSelector.displayName = "ConnectorSelector"; @@ -254,9 +245,7 @@ const SearchModeSelector = React.memo( return (
    - - Scope: - + Scope:
    ); - }, + } ); SearchModeSelector.displayName = "SearchModeSelector"; @@ -295,7 +284,7 @@ const ResearchModeSelector = React.memo( (value: string) => { onResearchModeChange?.(value as ResearchMode); }, - [onResearchModeChange], + [onResearchModeChange] ); // Memoize mode options to prevent recreation @@ -318,14 +307,12 @@ const ResearchModeSelector = React.memo( shortLabel: "Deeper", }, ], - [], + [] ); return (
    - - Mode: - + Mode:
    ); - }, + } ); ResearchModeSelector.displayName = "ResearchModeSelector"; const LLMSelector = React.memo(() => { const { llmConfigs, loading: llmLoading, error } = useLLMConfigs(); - const { - preferences, - updatePreferences, - loading: preferencesLoading, - } = useLLMPreferences(); + const { preferences, updatePreferences, loading: preferencesLoading } = useLLMPreferences(); const isLoading = llmLoading || preferencesLoading; // Memoize the selected config to avoid repeated lookups const selectedConfig = React.useMemo(() => { if (!preferences.fast_llm_id || !llmConfigs.length) return null; - return ( - llmConfigs.find((config) => config.id === preferences.fast_llm_id) || null - ); + return llmConfigs.find((config) => config.id === preferences.fast_llm_id) || null; }, [preferences.fast_llm_id, llmConfigs]); // Memoize the display value for the trigger @@ -390,7 +371,7 @@ const LLMSelector = React.memo(() => { const llmId = value ? parseInt(value, 10) : undefined; updatePreferences({ fast_llm_id: llmId }); }, - [updatePreferences], + [updatePreferences] ); // Loading skeleton @@ -432,9 +413,7 @@ const LLMSelector = React.memo(() => {
    - {displayValue || ( - Select LLM - )} + {displayValue || Select LLM}
    @@ -452,9 +431,7 @@ const LLMSelector = React.memo(() => {
    -

    - No LLM configurations -

    +

    No LLM configurations

    Configure AI models to get started

    @@ -482,13 +459,8 @@ const LLMSelector = React.memo(() => {
    - - {config.name} - - + {config.name} + {config.provider}
    @@ -537,10 +509,8 @@ const CustomChatInputOptions = React.memo( }) => { // Memoize the loading fallback to prevent recreation const loadingFallback = React.useMemo( - () => ( -
    - ), - [], + () =>
    , + [] ); return ( @@ -557,10 +527,7 @@ const CustomChatInputOptions = React.memo( selectedConnectors={selectedConnectors} /> - +
    ); - }, + } ); CustomChatInputOptions.displayName = "CustomChatInputOptions"; @@ -611,7 +578,7 @@ export const ChatInputUI = React.memo( /> ); - }, + } ); ChatInputUI.displayName = "ChatInputUI"; diff --git a/surfsense_web/components/chat/ChatInterface.tsx b/surfsense_web/components/chat/ChatInterface.tsx index 684b3196f..4a6897134 100644 --- a/surfsense_web/components/chat/ChatInterface.tsx +++ b/surfsense_web/components/chat/ChatInterface.tsx @@ -1,13 +1,10 @@ "use client"; import React from "react"; -import { - ChatSection as LlamaIndexChatSection, - ChatHandler, -} from "@llamaindex/chat-ui"; -import { Document } from "@/hooks/use-documents"; +import { ChatSection as LlamaIndexChatSection, type ChatHandler } from "@llamaindex/chat-ui"; +import type { Document } from "@/hooks/use-documents"; import { ChatInputUI } from "@/components/chat/ChatInputGroup"; -import { ResearchMode } from "@/components/chat"; +import type { ResearchMode } from "@/components/chat"; import { ChatMessagesUI } from "@/components/chat/ChatMessages"; interface ChatInterfaceProps { diff --git a/surfsense_web/components/chat/ChatMessages.tsx b/surfsense_web/components/chat/ChatMessages.tsx index 20aa07815..a7d767c31 100644 --- a/surfsense_web/components/chat/ChatMessages.tsx +++ b/surfsense_web/components/chat/ChatMessages.tsx @@ -2,10 +2,10 @@ import React from "react"; import { - ChatMessage as LlamaIndexChatMessage, - ChatMessages as LlamaIndexChatMessages, - Message, - useChatUI, + ChatMessage as LlamaIndexChatMessage, + ChatMessages as LlamaIndexChatMessages, + type Message, + useChatUI, } from "@llamaindex/chat-ui"; import TerminalDisplay from "@/components/chat/ChatTerminal"; import ChatSourcesDisplay from "@/components/chat/ChatSources"; @@ -14,74 +14,60 @@ import { ChatFurtherQuestions } from "@/components/chat/ChatFurtherQuestions"; import { AnimatedEmptyState } from "@/components/chat/AnimatedEmptyState"; import { languageRenderers } from "@/components/chat/CodeBlock"; - - export function ChatMessagesUI() { - const { messages } = useChatUI(); + const { messages } = useChatUI(); - return ( - - - - - - {messages.map((message, index) => ( - - ))} - - - - ); + return ( + + + + + + {messages.map((message, index) => ( + + ))} + + + + ); } -function ChatMessageUI({ - message, - isLast, -}: { - message: Message; - isLast: boolean; -}) { - const bottomRef = React.useRef(null); +function ChatMessageUI({ message, isLast }: { message: Message; isLast: boolean }) { + const bottomRef = React.useRef(null); - React.useEffect(() => { - if (isLast && bottomRef.current) { - bottomRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [message]); + React.useEffect(() => { + if (isLast && bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: "smooth" }); + } + }, [message]); - return ( - - {message.role === "assistant" ? ( -
    - - - - - -
    -
    - {isLast && } - -
    -
    - ) : ( - - - - )} - - ); + return ( + + {message.role === "assistant" ? ( +
    + + + + + +
    +
    + {isLast && } + +
    +
    + ) : ( + + + + )} + + ); } diff --git a/surfsense_web/components/chat/ChatSources.tsx b/surfsense_web/components/chat/ChatSources.tsx index c334babdb..f990e0ecb 100644 --- a/surfsense_web/components/chat/ChatSources.tsx +++ b/surfsense_web/components/chat/ChatSources.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { getAnnotationData, Message } from "@llamaindex/chat-ui"; +import { getAnnotationData, type Message } from "@llamaindex/chat-ui"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -11,13 +11,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ExternalLink, FileText, Globe } from "lucide-react"; import { IconBrandGithub } from "@tabler/icons-react"; @@ -113,12 +107,7 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) { const allNodes: SourceNode[] = []; annotations.forEach((item) => { - if ( - item && - typeof item === "object" && - "nodes" in item && - Array.isArray(item.nodes) - ) { + if (item && typeof item === "object" && "nodes" in item && Array.isArray(item.nodes)) { allNodes.push(...item.nodes); } }); @@ -133,7 +122,7 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) { acc[sourceType].push(node); return acc; }, - {} as Record, + {} as Record ); // Convert grouped nodes to SourceGroup format @@ -159,10 +148,7 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) { return null; } - const totalSources = sourceGroups.reduce( - (acc, group) => acc + group.sources.length, - 0, - ); + const totalSources = sourceGroups.reduce((acc, group) => acc + group.sources.length, 0); return ( @@ -176,10 +162,7 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) { Sources - +
    {sourceGroups.map((group) => ( @@ -189,13 +172,8 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) { className="flex items-center gap-2 whitespace-nowrap px-3 md:px-4" > {getSourceIcon(group.type)} - - {group.name} - - + {group.name} + {group.sources.length} @@ -203,11 +181,7 @@ export default function ChatSourcesDisplay({ message }: { message: Message }) {
    {sourceGroups.map((group) => ( - +
    {group.sources.map((source) => ( diff --git a/surfsense_web/components/chat/ChatTerminal.tsx b/surfsense_web/components/chat/ChatTerminal.tsx index 131d58a6d..b29015118 100644 --- a/surfsense_web/components/chat/ChatTerminal.tsx +++ b/surfsense_web/components/chat/ChatTerminal.tsx @@ -1,15 +1,9 @@ "use client"; import React from "react"; -import { getAnnotationData, Message } from "@llamaindex/chat-ui"; +import { getAnnotationData, type Message } from "@llamaindex/chat-ui"; -export default function TerminalDisplay({ - message, - open, -}: { - message: Message; - open: boolean; -}) { +export default function TerminalDisplay({ message, open }: { message: Message; open: boolean }) { const [isCollapsed, setIsCollapsed] = React.useState(!open); const bottomRef = React.useRef(null); @@ -57,12 +51,7 @@ export default function TerminalDisplay({
    {isCollapsed ? ( - + ) : ( - + +
    {events.map((event, index) => (
    $ @@ -104,9 +85,7 @@ export default function TerminalDisplay({
    ))} {events.length === 0 && ( -
    - No agent events to display... -
    +
    No agent events to display...
    )}
    )} diff --git a/surfsense_web/components/chat/Citation.tsx b/surfsense_web/components/chat/Citation.tsx index bc07e9b7f..09f13b88a 100644 --- a/surfsense_web/components/chat/Citation.tsx +++ b/surfsense_web/components/chat/Citation.tsx @@ -1,116 +1,119 @@ -import React, { useState } from 'react'; -import { ExternalLink } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; +import React, { useState } from "react"; +import { ExternalLink } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { getConnectorIcon } from './ConnectorComponents'; -import { Source } from './types'; + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { getConnectorIcon } from "./ConnectorComponents"; +import type { Source } from "./types"; type CitationProps = { - citationId: number; - citationText: string; - position: number; - source: Source | null; + citationId: number; + citationText: string; + position: number; + source: Source | null; }; /** * Citation component to handle individual citations */ -export const Citation = React.memo(({ citationId, citationText, position, source }: CitationProps) => { - const [open, setOpen] = useState(false); - const citationKey = `citation-${citationId}-${position}`; - - if (!source) return <>{citationText}; - - return ( - - - - - - {citationId} - - - - {open && ( - - -
    -
    - {getConnectorIcon(source.connectorType || '')} -
    -
    -
    -

    {source.title}

    -
    -

    {source.description}

    -
    - {source.url} -
    -
    - -
    -
    -
    - )} -
    -
    - ); -}); +export const Citation = React.memo( + ({ citationId, citationText, position, source }: CitationProps) => { + const [open, setOpen] = useState(false); + const citationKey = `citation-${citationId}-${position}`; -Citation.displayName = 'Citation'; + if (!source) return <>{citationText}; + + return ( + + + + + + {citationId} + + + + {open && ( + + +
    +
    + {getConnectorIcon(source.connectorType || "")} +
    +
    +
    +

    {source.title}

    +
    +

    {source.description}

    +
    + {source.url} +
    +
    + +
    +
    +
    + )} +
    +
    + ); + } +); + +Citation.displayName = "Citation"; /** * Function to render text with citations */ -export const renderTextWithCitations = (text: string, getCitationSource: (id: number) => Source | null) => { - // Regular expression to find citation patterns like [1], [2], etc. - const citationRegex = /\[(\d+)\]/g; - const parts = []; - let lastIndex = 0; - let match; - let position = 0; +export const renderTextWithCitations = ( + text: string, + getCitationSource: (id: number) => Source | null +) => { + // Regular expression to find citation patterns like [1], [2], etc. + const citationRegex = /\[(\d+)\]/g; + const parts = []; + let lastIndex = 0; + let match; + let position = 0; - while ((match = citationRegex.exec(text)) !== null) { - // Add text before the citation - if (match.index > lastIndex) { - parts.push(text.substring(lastIndex, match.index)); - } - - // Add the citation component - const citationId = parseInt(match[1], 10); - parts.push( - - ); - - lastIndex = match.index + match[0].length; - position++; - } - - // Add any remaining text after the last citation - if (lastIndex < text.length) { - parts.push(text.substring(lastIndex)); - } - - return parts; -}; \ No newline at end of file + while ((match = citationRegex.exec(text)) !== null) { + // Add text before the citation + if (match.index > lastIndex) { + parts.push(text.substring(lastIndex, match.index)); + } + + // Add the citation component + const citationId = parseInt(match[1], 10); + parts.push( + + ); + + lastIndex = match.index + match[0].length; + position++; + } + + // Add any remaining text after the last citation + if (lastIndex < text.length) { + parts.push(text.substring(lastIndex)); + } + + return parts; +}; diff --git a/surfsense_web/components/chat/CodeBlock.tsx b/surfsense_web/components/chat/CodeBlock.tsx index 965b4b001..5abcfa52f 100644 --- a/surfsense_web/components/chat/CodeBlock.tsx +++ b/surfsense_web/components/chat/CodeBlock.tsx @@ -2,10 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { - oneLight, - oneDark, -} from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { oneLight, oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { Check, Copy } from "lucide-react"; import { useTheme } from "next-themes"; @@ -13,182 +10,202 @@ import { useTheme } from "next-themes"; const COPY_TIMEOUT = 2000; const BASE_CUSTOM_STYLE = { - margin: 0, - borderRadius: "0.375rem", - fontSize: "0.75rem", - lineHeight: "1.5rem", - border: "none", + margin: 0, + borderRadius: "0.375rem", + fontSize: "0.75rem", + lineHeight: "1.5rem", + border: "none", } as const; const LINE_PROPS_STYLE = { - wordBreak: "break-all" as const, - whiteSpace: "pre-wrap" as const, - border: "none", - borderBottom: "none", - paddingLeft: 0, - paddingRight: 0, - margin: "0.25rem 0", + wordBreak: "break-all" as const, + whiteSpace: "pre-wrap" as const, + border: "none", + borderBottom: "none", + paddingLeft: 0, + paddingRight: 0, + margin: "0.25rem 0", } as const; const CODE_TAG_PROPS = { - className: "font-mono", - style: { - border: "none", - background: "var(--syntax-bg)", - }, + className: "font-mono", + style: { + border: "none", + background: "var(--syntax-bg)", + }, } as const; // TypeScript interfaces interface CodeBlockProps { - children: string; - language: string; + children: string; + language: string; } -interface LanguageRenderer { - (props: { code: string }): React.JSX.Element; -} +type LanguageRenderer = (props: { code: string }) => React.JSX.Element interface SyntaxStyle { - [key: string]: React.CSSProperties; + [key: string]: React.CSSProperties; } // Memoized fallback component for SSR/hydration const FallbackCodeBlock = React.memo(({ children }: { children: string }) => ( -
    -
    -      
    -        {children}
    -      
    -    
    -
    +
    +
    +			{children}
    +		
    +
    )); FallbackCodeBlock.displayName = "FallbackCodeBlock"; // Code block component with syntax highlighting and copy functionality -export const CodeBlock = React.memo(({ - children, - language, -}) => { - const [copied, setCopied] = useState(false); - const { resolvedTheme, theme } = useTheme(); - const [mounted, setMounted] = useState(false); +export const CodeBlock = React.memo(({ children, language }) => { + const [copied, setCopied] = useState(false); + const { resolvedTheme, theme } = useTheme(); + const [mounted, setMounted] = useState(false); - // Prevent hydration issues - useEffect(() => { - setMounted(true); - }, []); + // Prevent hydration issues + useEffect(() => { + setMounted(true); + }, []); - // Memoize theme detection - const isDarkTheme = useMemo(() => - mounted && (resolvedTheme === "dark" || theme === "dark"), - [mounted, resolvedTheme, theme] - ); + // Memoize theme detection + const isDarkTheme = useMemo( + () => mounted && (resolvedTheme === "dark" || theme === "dark"), + [mounted, resolvedTheme, theme] + ); - // Memoize syntax theme selection - const syntaxTheme = useMemo(() => - isDarkTheme ? oneDark : oneLight, - [isDarkTheme] - ); + // Memoize syntax theme selection + const syntaxTheme = useMemo(() => (isDarkTheme ? oneDark : oneLight), [isDarkTheme]); - // Memoize enhanced style with theme-specific modifications - const enhancedStyle = useMemo(() => ({ - ...syntaxTheme, - 'pre[class*="language-"]': { - ...syntaxTheme['pre[class*="language-"]'], - margin: 0, - border: "none", - borderRadius: "0.375rem", - background: "var(--syntax-bg)", - }, - 'code[class*="language-"]': { - ...syntaxTheme['code[class*="language-"]'], - border: "none", - background: "var(--syntax-bg)", - }, - }), [syntaxTheme]); + // Memoize enhanced style with theme-specific modifications + const enhancedStyle = useMemo( + () => ({ + ...syntaxTheme, + 'pre[class*="language-"]': { + ...syntaxTheme['pre[class*="language-"]'], + margin: 0, + border: "none", + borderRadius: "0.375rem", + background: "var(--syntax-bg)", + }, + 'code[class*="language-"]': { + ...syntaxTheme['code[class*="language-"]'], + border: "none", + background: "var(--syntax-bg)", + }, + }), + [syntaxTheme] + ); - // Memoize custom style with background - const customStyle = useMemo(() => ({ - ...BASE_CUSTOM_STYLE, - backgroundColor: "var(--syntax-bg)", - }), []); + // Memoize custom style with background + const customStyle = useMemo( + () => ({ + ...BASE_CUSTOM_STYLE, + backgroundColor: "var(--syntax-bg)", + }), + [] + ); - // Memoized copy handler - const handleCopy = useCallback(async () => { - try { - await navigator.clipboard.writeText(children); - setCopied(true); - const timeoutId = setTimeout(() => setCopied(false), COPY_TIMEOUT); - return () => clearTimeout(timeoutId); - } catch (error) { - console.warn("Failed to copy code to clipboard:", error); - } - }, [children]); + // Memoized copy handler + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(children); + setCopied(true); + const timeoutId = setTimeout(() => setCopied(false), COPY_TIMEOUT); + return () => clearTimeout(timeoutId); + } catch (error) { + console.warn("Failed to copy code to clipboard:", error); + } + }, [children]); - // Memoized line props with style - const lineProps = useMemo(() => ({ - style: LINE_PROPS_STYLE, - }), []); + // Memoized line props with style + const lineProps = useMemo( + () => ({ + style: LINE_PROPS_STYLE, + }), + [] + ); - // Early return for non-mounted state - if (!mounted) { - return {children}; - } + // Early return for non-mounted state + if (!mounted) { + return {children}; + } - return ( -
    -
    - -
    - - {children} - -
    - ); + return ( +
    +
    + +
    + + {children} + +
    + ); }); CodeBlock.displayName = "CodeBlock"; // Optimized language renderer factory with memoization const createLanguageRenderer = (lang: string): LanguageRenderer => { - const renderer = ({ code }: { code: string }) => ( - {code} - ); - renderer.displayName = `LanguageRenderer(${lang})`; - return renderer; + const renderer = ({ code }: { code: string }) => {code}; + renderer.displayName = `LanguageRenderer(${lang})`; + return renderer; }; // Pre-defined supported languages for better maintainability const SUPPORTED_LANGUAGES = [ - "javascript", "typescript", "python", "java", "csharp", "cpp", "c", - "php", "ruby", "go", "rust", "swift", "kotlin", "scala", "sql", - "json", "xml", "yaml", "bash", "shell", "powershell", "dockerfile", - "html", "css", "scss", "less", "markdown", "text" + "javascript", + "typescript", + "python", + "java", + "csharp", + "cpp", + "c", + "php", + "ruby", + "go", + "rust", + "swift", + "kotlin", + "scala", + "sql", + "json", + "xml", + "yaml", + "bash", + "shell", + "powershell", + "dockerfile", + "html", + "css", + "scss", + "less", + "markdown", + "text", ] as const; // Generate language renderers efficiently -export const languageRenderers: Record = - Object.fromEntries( - SUPPORTED_LANGUAGES.map(lang => [lang, createLanguageRenderer(lang)]) - ); \ No newline at end of file +export const languageRenderers: Record = Object.fromEntries( + SUPPORTED_LANGUAGES.map((lang) => [lang, createLanguageRenderer(lang)]) +); diff --git a/surfsense_web/components/chat/ConnectorComponents.tsx b/surfsense_web/components/chat/ConnectorComponents.tsx index d7c977b98..5a373c717 100644 --- a/surfsense_web/components/chat/ConnectorComponents.tsx +++ b/surfsense_web/components/chat/ConnectorComponents.tsx @@ -1,302 +1,289 @@ -import React from "react"; +import type React from "react"; import { - ChevronDown, - Plus, - Search, - Globe, - Sparkles, - Microscope, - Telescope, - File, - Link, - Webhook, - MessageCircle, - FileText, + ChevronDown, + Plus, + Search, + Globe, + Sparkles, + Microscope, + Telescope, + File, + Link, + Webhook, + MessageCircle, + FileText, } from "lucide-react"; import { - IconBrandNotion, - IconBrandSlack, - IconBrandYoutube, - IconBrandGithub, - IconLayoutKanban, - IconLinkPlus, - IconBrandDiscord, - IconTicket, + IconBrandNotion, + IconBrandSlack, + IconBrandYoutube, + IconBrandGithub, + IconLayoutKanban, + IconLinkPlus, + IconBrandDiscord, + IconTicket, } from "@tabler/icons-react"; import { Button } from "@/components/ui/button"; -import { Connector, ResearchMode } from "./types"; +import type { Connector, ResearchMode } from "./types"; // Helper function to get connector icon export const getConnectorIcon = (connectorType: string) => { - const iconProps = { className: "h-4 w-4" }; + const iconProps = { className: "h-4 w-4" }; - switch (connectorType) { - case "LINKUP_API": - return ; - case "LINEAR_CONNECTOR": - return ; - case "GITHUB_CONNECTOR": - return ; - case "YOUTUBE_VIDEO": - return ; - case "CRAWLED_URL": - return ; - case "FILE": - return ; - case "EXTENSION": - return ; - case "SERPER_API": - case "TAVILY_API": - return ; - case "SLACK_CONNECTOR": - return ; - case "NOTION_CONNECTOR": - return ; - case "DISCORD_CONNECTOR": - return ; - case "JIRA_CONNECTOR": - return ; - case "DEEP": - return ; - case "DEEPER": - return ; - case "DEEPEST": - return ; - default: - return ; - } + switch (connectorType) { + case "LINKUP_API": + return ; + case "LINEAR_CONNECTOR": + return ; + case "GITHUB_CONNECTOR": + return ; + case "YOUTUBE_VIDEO": + return ; + case "CRAWLED_URL": + return ; + case "FILE": + return ; + case "EXTENSION": + return ; + case "SERPER_API": + case "TAVILY_API": + return ; + case "SLACK_CONNECTOR": + return ; + case "NOTION_CONNECTOR": + return ; + case "DISCORD_CONNECTOR": + return ; + case "JIRA_CONNECTOR": + return ; + case "DEEP": + return ; + case "DEEPER": + return ; + case "DEEPEST": + return ; + default: + return ; + } }; export const researcherOptions: { - value: ResearchMode; - label: string; - icon: React.ReactNode; + value: ResearchMode; + label: string; + icon: React.ReactNode; }[] = [ - { - value: "QNA", - label: "Q/A", - icon: getConnectorIcon("GENERAL"), - }, - { - value: "REPORT_GENERAL", - label: "General", - icon: getConnectorIcon("GENERAL"), - }, - { - value: "REPORT_DEEP", - label: "Deep", - icon: getConnectorIcon("DEEP"), - }, - { - value: "REPORT_DEEPER", - label: "Deeper", - icon: getConnectorIcon("DEEPER"), - }, + { + value: "QNA", + label: "Q/A", + icon: getConnectorIcon("GENERAL"), + }, + { + value: "REPORT_GENERAL", + label: "General", + icon: getConnectorIcon("GENERAL"), + }, + { + value: "REPORT_DEEP", + label: "Deep", + icon: getConnectorIcon("DEEP"), + }, + { + value: "REPORT_DEEPER", + label: "Deeper", + icon: getConnectorIcon("DEEPER"), + }, ]; /** * Displays a small icon for a connector type */ -export const ConnectorIcon = ({ - type, - index = 0, -}: { - type: string; - index?: number; -}) => ( -
    - {getConnectorIcon(type)} -
    +export const ConnectorIcon = ({ type, index = 0 }: { type: string; index?: number }) => ( +
    + {getConnectorIcon(type)} +
    ); /** * Displays a count indicator for additional connectors */ export const ConnectorCountBadge = ({ count }: { count: number }) => ( -
    - +{count} -
    +
    + +{count} +
    ); type ConnectorButtonProps = { - selectedConnectors: string[]; - onClick: () => void; - connectorSources: Connector[]; + selectedConnectors: string[]; + onClick: () => void; + connectorSources: Connector[]; }; /** * Button that displays selected connectors and opens connector selection dialog */ export const ConnectorButton = ({ - selectedConnectors, - onClick, - connectorSources, + selectedConnectors, + onClick, + connectorSources, }: ConnectorButtonProps) => { - const totalConnectors = connectorSources.length; - const selectedCount = selectedConnectors.length; - const progressPercentage = (selectedCount / totalConnectors) * 100; + const totalConnectors = connectorSources.length; + const selectedCount = selectedConnectors.length; + const progressPercentage = (selectedCount / totalConnectors) * 100; - // Get the name of a single selected connector - const getSingleConnectorName = () => { - const connector = connectorSources.find( - (c) => c.type === selectedConnectors[0], - ); - return connector?.name || ""; - }; + // Get the name of a single selected connector + const getSingleConnectorName = () => { + const connector = connectorSources.find((c) => c.type === selectedConnectors[0]); + return connector?.name || ""; + }; - // Get display text based on selection count - const getDisplayText = () => { - if (selectedCount === totalConnectors) return "All Connectors"; - if (selectedCount === 1) return getSingleConnectorName(); - return `${selectedCount} Connectors`; - }; + // Get display text based on selection count + const getDisplayText = () => { + if (selectedCount === totalConnectors) return "All Connectors"; + if (selectedCount === 1) return getSingleConnectorName(); + return `${selectedCount} Connectors`; + }; - // Render the empty state (no connectors selected) - const renderEmptyState = () => ( - <> - - Select Connectors - - ); + // Render the empty state (no connectors selected) + const renderEmptyState = () => ( + <> + + Select Connectors + + ); - // Render the selected connectors preview - const renderSelectedConnectors = () => ( - <> -
    - {/* Show up to 3 connector icons */} - {selectedConnectors.slice(0, 3).map((type, index) => ( - - ))} + // Render the selected connectors preview + const renderSelectedConnectors = () => ( + <> +
    + {/* Show up to 3 connector icons */} + {selectedConnectors.slice(0, 3).map((type, index) => ( + + ))} - {/* Show count indicator if more than 3 connectors are selected */} - {selectedCount > 3 && } -
    + {/* Show count indicator if more than 3 connectors are selected */} + {selectedCount > 3 && } +
    - {/* Display text */} - {getDisplayText()} - - ); + {/* Display text */} + {getDisplayText()} + + ); - return ( - - ); +
    + {selectedCount === 0 ? renderEmptyState() : renderSelectedConnectors()} + +
    + + ); }; // New component for Research Mode Control with Q/A and Report toggle type ResearchModeControlProps = { - value: ResearchMode; - onChange: (value: ResearchMode) => void; + value: ResearchMode; + onChange: (value: ResearchMode) => void; }; -export const ResearchModeControl = ({ - value, - onChange, -}: ResearchModeControlProps) => { - // Determine if we're in Q/A mode or Report mode - const isQnaMode = value === "QNA"; - const isReportMode = value.startsWith("REPORT_"); +export const ResearchModeControl = ({ value, onChange }: ResearchModeControlProps) => { + // Determine if we're in Q/A mode or Report mode + const isQnaMode = value === "QNA"; + const isReportMode = value.startsWith("REPORT_"); - // Get the current report sub-mode - const getCurrentReportMode = () => { - if (!isReportMode) return "GENERAL"; - return value.replace("REPORT_", "") as "GENERAL" | "DEEP" | "DEEPER"; - }; + // Get the current report sub-mode + const getCurrentReportMode = () => { + if (!isReportMode) return "GENERAL"; + return value.replace("REPORT_", "") as "GENERAL" | "DEEP" | "DEEPER"; + }; - const reportSubOptions = [ - { value: "GENERAL", label: "General", icon: getConnectorIcon("GENERAL") }, - { value: "DEEP", label: "Deep", icon: getConnectorIcon("DEEP") }, - { value: "DEEPER", label: "Deeper", icon: getConnectorIcon("DEEPER") }, - ]; + const reportSubOptions = [ + { value: "GENERAL", label: "General", icon: getConnectorIcon("GENERAL") }, + { value: "DEEP", label: "Deep", icon: getConnectorIcon("DEEP") }, + { value: "DEEPER", label: "Deeper", icon: getConnectorIcon("DEEPER") }, + ]; - const handleModeToggle = (mode: "QNA" | "REPORT") => { - if (mode === "QNA") { - onChange("QNA"); - } else { - // Default to GENERAL for Report mode - onChange("REPORT_GENERAL"); - } - }; + const handleModeToggle = (mode: "QNA" | "REPORT") => { + if (mode === "QNA") { + onChange("QNA"); + } else { + // Default to GENERAL for Report mode + onChange("REPORT_GENERAL"); + } + }; - const handleReportSubModeChange = (subMode: string) => { - onChange(`REPORT_${subMode}` as ResearchMode); - }; + const handleReportSubModeChange = (subMode: string) => { + onChange(`REPORT_${subMode}` as ResearchMode); + }; - return ( -
    - {/* Main Q/A vs Report Toggle */} -
    - - -
    + return ( +
    + {/* Main Q/A vs Report Toggle */} +
    + + +
    - {/* Report Sub-options (only show when in Report mode) */} - {isReportMode && ( -
    - {reportSubOptions.map((option) => ( - - ))} -
    - )} -
    - ); + {/* Report Sub-options (only show when in Report mode) */} + {isReportMode && ( +
    + {reportSubOptions.map((option) => ( + + ))} +
    + )} +
    + ); }; diff --git a/surfsense_web/components/chat/DocumentsDataTable.tsx b/surfsense_web/components/chat/DocumentsDataTable.tsx index 7e441bb28..f93e72c96 100644 --- a/surfsense_web/components/chat/DocumentsDataTable.tsx +++ b/surfsense_web/components/chat/DocumentsDataTable.tsx @@ -2,501 +2,463 @@ import * as React from "react"; import { - ColumnDef, - ColumnFiltersState, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - SortingState, - useReactTable, - VisibilityState, + type ColumnDef, + type ColumnFiltersState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + type SortingState, + useReactTable, + type VisibilityState, } from "@tanstack/react-table"; import { ArrowUpDown, Calendar, FileText, Search } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; -import { Document, DocumentType } from "@/hooks/use-documents"; +import type { Document, DocumentType } from "@/hooks/use-documents"; interface DocumentsDataTableProps { - documents: Document[]; - onSelectionChange: (documents: Document[]) => void; - onDone: () => void; - initialSelectedDocuments?: Document[]; + documents: Document[]; + onSelectionChange: (documents: Document[]) => void; + onDone: () => void; + initialSelectedDocuments?: Document[]; } const DOCUMENT_TYPES: (DocumentType | "ALL")[] = [ - "ALL", - "FILE", - "EXTENSION", - "CRAWLED_URL", - "YOUTUBE_VIDEO", - "SLACK_CONNECTOR", - "NOTION_CONNECTOR", - "GITHUB_CONNECTOR", - "LINEAR_CONNECTOR", - "DISCORD_CONNECTOR", - "JIRA_CONNECTOR", + "ALL", + "FILE", + "EXTENSION", + "CRAWLED_URL", + "YOUTUBE_VIDEO", + "SLACK_CONNECTOR", + "NOTION_CONNECTOR", + "GITHUB_CONNECTOR", + "LINEAR_CONNECTOR", + "DISCORD_CONNECTOR", + "JIRA_CONNECTOR", ]; const getDocumentTypeColor = (type: DocumentType) => { - const colors = { - FILE: "bg-blue-50 text-blue-700 border-blue-200", - EXTENSION: "bg-green-50 text-green-700 border-green-200", - CRAWLED_URL: "bg-purple-50 text-purple-700 border-purple-200", - YOUTUBE_VIDEO: "bg-red-50 text-red-700 border-red-200", - SLACK_CONNECTOR: "bg-yellow-50 text-yellow-700 border-yellow-200", - NOTION_CONNECTOR: "bg-indigo-50 text-indigo-700 border-indigo-200", - GITHUB_CONNECTOR: "bg-gray-50 text-gray-700 border-gray-200", - LINEAR_CONNECTOR: "bg-pink-50 text-pink-700 border-pink-200", - DISCORD_CONNECTOR: "bg-violet-50 text-violet-700 border-violet-200", - JIRA_CONNECTOR: "bg-orange-50 text-orange-700 border-orange-200", - }; - return colors[type] || "bg-gray-50 text-gray-700 border-gray-200"; + const colors = { + FILE: "bg-blue-50 text-blue-700 border-blue-200", + EXTENSION: "bg-green-50 text-green-700 border-green-200", + CRAWLED_URL: "bg-purple-50 text-purple-700 border-purple-200", + YOUTUBE_VIDEO: "bg-red-50 text-red-700 border-red-200", + SLACK_CONNECTOR: "bg-yellow-50 text-yellow-700 border-yellow-200", + NOTION_CONNECTOR: "bg-indigo-50 text-indigo-700 border-indigo-200", + GITHUB_CONNECTOR: "bg-gray-50 text-gray-700 border-gray-200", + LINEAR_CONNECTOR: "bg-pink-50 text-pink-700 border-pink-200", + DISCORD_CONNECTOR: "bg-violet-50 text-violet-700 border-violet-200", + JIRA_CONNECTOR: "bg-orange-50 text-orange-700 border-orange-200", + }; + return colors[type] || "bg-gray-50 text-gray-700 border-gray-200"; }; const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> - ), - enableSorting: false, - enableHiding: false, - size: 40, - }, - { - accessorKey: "title", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const title = row.getValue("title") as string; - return ( -
    - {title} -
    - ); - }, - }, - { - accessorKey: "document_type", - header: "Type", - cell: ({ row }) => { - const type = row.getValue("document_type") as DocumentType; - return ( - - {type.replace(/_/g, " ")} - {type.split("_")[0]} - - ); - }, - size: 80, - meta: { - className: "hidden sm:table-cell", - }, - }, - { - accessorKey: "content", - header: "Preview", - cell: ({ row }) => { - const content = row.getValue("content") as string; - return ( -
    - {content.substring(0, 30)}... - - {content.substring(0, 100)}... - -
    - ); - }, - enableSorting: false, - meta: { - className: "hidden md:table-cell", - }, - }, - { - accessorKey: "created_at", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const date = new Date(row.getValue("created_at")); - return ( -
    - - {date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - })} - - - {date.toLocaleDateString("en-US", { - month: "numeric", - day: "numeric", - })} - -
    - ); - }, - size: 80, - }, + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + { + accessorKey: "title", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const title = row.getValue("title") as string; + return ( +
    + {title} +
    + ); + }, + }, + { + accessorKey: "document_type", + header: "Type", + cell: ({ row }) => { + const type = row.getValue("document_type") as DocumentType; + return ( + + {type.replace(/_/g, " ")} + {type.split("_")[0]} + + ); + }, + size: 80, + meta: { + className: "hidden sm:table-cell", + }, + }, + { + accessorKey: "content", + header: "Preview", + cell: ({ row }) => { + const content = row.getValue("content") as string; + return ( +
    + {content.substring(0, 30)}... + {content.substring(0, 100)}... +
    + ); + }, + enableSorting: false, + meta: { + className: "hidden md:table-cell", + }, + }, + { + accessorKey: "created_at", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return ( +
    + + {date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + })} + + + {date.toLocaleDateString("en-US", { + month: "numeric", + day: "numeric", + })} + +
    + ); + }, + size: 80, + }, ]; export function DocumentsDataTable({ - documents, - onSelectionChange, - onDone, - initialSelectedDocuments = [], + documents, + onSelectionChange, + onDone, + initialSelectedDocuments = [], }: DocumentsDataTableProps) { - const [sorting, setSorting] = React.useState([]); - const [columnFilters, setColumnFilters] = React.useState( - [], - ); - const [columnVisibility, setColumnVisibility] = - React.useState({}); - const [documentTypeFilter, setDocumentTypeFilter] = React.useState< - DocumentType | "ALL" - >("ALL"); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [documentTypeFilter, setDocumentTypeFilter] = React.useState("ALL"); - // Memoize initial row selection to prevent infinite loops - const initialRowSelection = React.useMemo(() => { - if (!documents.length || !initialSelectedDocuments.length) return {}; + // Memoize initial row selection to prevent infinite loops + const initialRowSelection = React.useMemo(() => { + if (!documents.length || !initialSelectedDocuments.length) return {}; - const selection: Record = {}; - initialSelectedDocuments.forEach((selectedDoc) => { - selection[selectedDoc.id] = true; - }); - return selection; - }, [documents, initialSelectedDocuments]); + const selection: Record = {}; + initialSelectedDocuments.forEach((selectedDoc) => { + selection[selectedDoc.id] = true; + }); + return selection; + }, [documents, initialSelectedDocuments]); - const [rowSelection, setRowSelection] = React.useState< - Record - >({}); + const [rowSelection, setRowSelection] = React.useState>({}); - // Only update row selection when initialRowSelection actually changes and is not empty - React.useEffect(() => { - const hasChanges = - JSON.stringify(rowSelection) !== JSON.stringify(initialRowSelection); - if (hasChanges && Object.keys(initialRowSelection).length > 0) { - setRowSelection(initialRowSelection); - } - }, [initialRowSelection]); + // Only update row selection when initialRowSelection actually changes and is not empty + React.useEffect(() => { + const hasChanges = JSON.stringify(rowSelection) !== JSON.stringify(initialRowSelection); + if (hasChanges && Object.keys(initialRowSelection).length > 0) { + setRowSelection(initialRowSelection); + } + }, [initialRowSelection]); - // Initialize row selection on mount - React.useEffect(() => { - if ( - Object.keys(rowSelection).length === 0 && - Object.keys(initialRowSelection).length > 0 - ) { - setRowSelection(initialRowSelection); - } - }, []); + // Initialize row selection on mount + React.useEffect(() => { + if (Object.keys(rowSelection).length === 0 && Object.keys(initialRowSelection).length > 0) { + setRowSelection(initialRowSelection); + } + }, []); - const filteredDocuments = React.useMemo(() => { - if (documentTypeFilter === "ALL") return documents; - return documents.filter((doc) => doc.document_type === documentTypeFilter); - }, [documents, documentTypeFilter]); + const filteredDocuments = React.useMemo(() => { + if (documentTypeFilter === "ALL") return documents; + return documents.filter((doc) => doc.document_type === documentTypeFilter); + }, [documents, documentTypeFilter]); - const table = useReactTable({ - data: filteredDocuments, - columns, - getRowId: (row) => row.id.toString(), - onSortingChange: setSorting, - onColumnFiltersChange: setColumnFilters, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility, - onRowSelectionChange: setRowSelection, - initialState: { pagination: { pageSize: 10 } }, - state: { sorting, columnFilters, columnVisibility, rowSelection }, - }); + const table = useReactTable({ + data: filteredDocuments, + columns, + getRowId: (row) => row.id.toString(), + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + initialState: { pagination: { pageSize: 10 } }, + state: { sorting, columnFilters, columnVisibility, rowSelection }, + }); - React.useEffect(() => { - const selectedRows = table.getFilteredSelectedRowModel().rows; - const selectedDocuments = selectedRows.map((row) => row.original); - onSelectionChange(selectedDocuments); - }, [rowSelection, onSelectionChange, table]); + React.useEffect(() => { + const selectedRows = table.getFilteredSelectedRowModel().rows; + const selectedDocuments = selectedRows.map((row) => row.original); + onSelectionChange(selectedDocuments); + }, [rowSelection, onSelectionChange, table]); - const handleClearAll = () => setRowSelection({}); + const handleClearAll = () => setRowSelection({}); - const handleSelectPage = () => { - const currentPageRows = table.getRowModel().rows; - const newSelection = { ...rowSelection }; - currentPageRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectPage = () => { + const currentPageRows = table.getRowModel().rows; + const newSelection = { ...rowSelection }; + currentPageRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const handleSelectAllFiltered = () => { - const allFilteredRows = table.getFilteredRowModel().rows; - const newSelection: Record = {}; - allFilteredRows.forEach((row) => { - newSelection[row.id] = true; - }); - setRowSelection(newSelection); - }; + const handleSelectAllFiltered = () => { + const allFilteredRows = table.getFilteredRowModel().rows; + const newSelection: Record = {}; + allFilteredRows.forEach((row) => { + newSelection[row.id] = true; + }); + setRowSelection(newSelection); + }; - const selectedCount = table.getFilteredSelectedRowModel().rows.length; - const totalFiltered = table.getFilteredRowModel().rows.length; + const selectedCount = table.getFilteredSelectedRowModel().rows.length; + const totalFiltered = table.getFilteredRowModel().rows.length; - return ( -
    - {/* Header Controls */} -
    - {/* Search and Filter Row */} -
    -
    - - - table.getColumn("title")?.setFilterValue(event.target.value) - } - className="pl-10 text-sm" - /> -
    - -
    + return ( +
    + {/* Header Controls */} +
    + {/* Search and Filter Row */} +
    +
    + + table.getColumn("title")?.setFilterValue(event.target.value)} + className="pl-10 text-sm" + /> +
    + +
    - {/* Action Controls Row */} -
    -
    - - {selectedCount} of {totalFiltered} selected - -
    -
    - - - - -
    -
    - -
    -
    + {/* Action Controls Row */} +
    +
    + + {selectedCount} of {totalFiltered} selected + +
    +
    + + + + +
    +
    + +
    +
    - {/* Table Container */} -
    -
    - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No documents found. - - - )} - -
    -
    -
    + {/* Table Container */} +
    +
    + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No documents found. + + + )} + +
    +
    +
    - {/* Footer Pagination */} -
    -
    - Showing{" "} - {table.getState().pagination.pageIndex * - table.getState().pagination.pageSize + - 1}{" "} - to{" "} - {Math.min( - (table.getState().pagination.pageIndex + 1) * - table.getState().pagination.pageSize, - table.getFilteredRowModel().rows.length, - )}{" "} - of {table.getFilteredRowModel().rows.length} documents -
    -
    - -
    - Page - {table.getState().pagination.pageIndex + 1} - of - {table.getPageCount()} -
    - -
    -
    -
    - ); + {/* Footer Pagination */} +
    +
    + Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}{" "} + to{" "} + {Math.min( + (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length + )}{" "} + of {table.getFilteredRowModel().rows.length} documents +
    +
    + +
    + Page + {table.getState().pagination.pageIndex + 1} + of + {table.getPageCount()} +
    + +
    +
    +
    + ); } diff --git a/surfsense_web/components/chat/ScrollUtils.tsx b/surfsense_web/components/chat/ScrollUtils.tsx index 8db7ca2c6..922d836af 100644 --- a/surfsense_web/components/chat/ScrollUtils.tsx +++ b/surfsense_web/components/chat/ScrollUtils.tsx @@ -1,80 +1,81 @@ -import { RefObject, useEffect } from 'react'; +import { type RefObject, useEffect } from "react"; /** * Function to scroll to the bottom of a container */ export const scrollToBottom = (ref: RefObject) => { - ref.current?.scrollIntoView({ behavior: 'smooth' }); + ref.current?.scrollIntoView({ behavior: "smooth" }); }; /** * Hook to scroll to bottom when messages change */ export const useScrollToBottom = (ref: RefObject, dependencies: any[]) => { - useEffect(() => { - scrollToBottom(ref); - }, dependencies); + useEffect(() => { + scrollToBottom(ref); + }, dependencies); }; /** * Function to check scroll position and update indicators */ export const updateScrollIndicators = ( - tabsListRef: RefObject, - setCanScrollLeft: (value: boolean) => void, - setCanScrollRight: (value: boolean) => void + tabsListRef: RefObject, + setCanScrollLeft: (value: boolean) => void, + setCanScrollRight: (value: boolean) => void ) => { - if (tabsListRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = tabsListRef.current; - setCanScrollLeft(scrollLeft > 0); - setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 10); // 10px buffer - } + if (tabsListRef.current) { + const { scrollLeft, scrollWidth, clientWidth } = tabsListRef.current; + setCanScrollLeft(scrollLeft > 0); + setCanScrollRight(scrollLeft + clientWidth < scrollWidth - 10); // 10px buffer + } }; /** * Hook to initialize scroll indicators and add resize listener */ export const useScrollIndicators = ( - tabsListRef: RefObject, - setCanScrollLeft: (value: boolean) => void, - setCanScrollRight: (value: boolean) => void + tabsListRef: RefObject, + setCanScrollLeft: (value: boolean) => void, + setCanScrollRight: (value: boolean) => void ) => { - const updateIndicators = () => updateScrollIndicators(tabsListRef, setCanScrollLeft, setCanScrollRight); - - useEffect(() => { - updateIndicators(); - // Add resize listener to update indicators when window size changes - window.addEventListener('resize', updateIndicators); - return () => window.removeEventListener('resize', updateIndicators); - }, []); - - return updateIndicators; + const updateIndicators = () => + updateScrollIndicators(tabsListRef, setCanScrollLeft, setCanScrollRight); + + useEffect(() => { + updateIndicators(); + // Add resize listener to update indicators when window size changes + window.addEventListener("resize", updateIndicators); + return () => window.removeEventListener("resize", updateIndicators); + }, []); + + return updateIndicators; }; /** * Function to scroll tabs list left */ export const scrollTabsLeft = ( - tabsListRef: RefObject, - updateIndicators: () => void + tabsListRef: RefObject, + updateIndicators: () => void ) => { - if (tabsListRef.current) { - tabsListRef.current.scrollBy({ left: -200, behavior: 'smooth' }); - // Update indicators after scrolling - setTimeout(updateIndicators, 300); - } + if (tabsListRef.current) { + tabsListRef.current.scrollBy({ left: -200, behavior: "smooth" }); + // Update indicators after scrolling + setTimeout(updateIndicators, 300); + } }; /** * Function to scroll tabs list right */ export const scrollTabsRight = ( - tabsListRef: RefObject, - updateIndicators: () => void + tabsListRef: RefObject, + updateIndicators: () => void ) => { - if (tabsListRef.current) { - tabsListRef.current.scrollBy({ left: 200, behavior: 'smooth' }); - // Update indicators after scrolling - setTimeout(updateIndicators, 300); - } -}; \ No newline at end of file + if (tabsListRef.current) { + tabsListRef.current.scrollBy({ left: 200, behavior: "smooth" }); + // Update indicators after scrolling + setTimeout(updateIndicators, 300); + } +}; diff --git a/surfsense_web/components/chat/SegmentedControl.tsx b/surfsense_web/components/chat/SegmentedControl.tsx index 7d81a528a..11cab6fae 100644 --- a/surfsense_web/components/chat/SegmentedControl.tsx +++ b/surfsense_web/components/chat/SegmentedControl.tsx @@ -1,38 +1,40 @@ -import React from 'react'; +import type React from "react"; type SegmentedControlProps = { - value: T; - onChange: (value: T) => void; - options: Array<{ - value: T; - label: string; - icon: React.ReactNode; - }>; + value: T; + onChange: (value: T) => void; + options: Array<{ + value: T; + label: string; + icon: React.ReactNode; + }>; }; /** * A segmented control component for selecting between different options */ -function SegmentedControl({ value, onChange, options }: SegmentedControlProps) { - return ( -
    - {options.map((option) => ( - - ))} -
    - ); +function SegmentedControl({ + value, + onChange, + options, +}: SegmentedControlProps) { + return ( +
    + {options.map((option) => ( + + ))} +
    + ); } -export default SegmentedControl; \ No newline at end of file +export default SegmentedControl; diff --git a/surfsense_web/components/chat/SourceUtils.tsx b/surfsense_web/components/chat/SourceUtils.tsx index 941ea4253..bd83c4b42 100644 --- a/surfsense_web/components/chat/SourceUtils.tsx +++ b/surfsense_web/components/chat/SourceUtils.tsx @@ -1,68 +1,69 @@ -import { Source, Connector } from './types'; +import type { Source, Connector } from "./types"; /** * Function to get sources for the main view */ export const getMainViewSources = (connector: Connector, initialSourcesDisplay: number) => { - return connector.sources?.slice(0, initialSourcesDisplay); + return connector.sources?.slice(0, initialSourcesDisplay); }; /** * Function to get filtered sources for the dialog */ export const getFilteredSources = (connector: Connector, sourceFilter: string) => { - if (!sourceFilter.trim()) { - return connector.sources; - } - - const filter = sourceFilter.toLowerCase().trim(); - return connector.sources?.filter(source => - source.title.toLowerCase().includes(filter) || - source.description.toLowerCase().includes(filter) - ); + if (!sourceFilter.trim()) { + return connector.sources; + } + + const filter = sourceFilter.toLowerCase().trim(); + return connector.sources?.filter( + (source) => + source.title.toLowerCase().includes(filter) || + source.description.toLowerCase().includes(filter) + ); }; /** * Function to get paginated and filtered sources for the dialog */ export const getPaginatedDialogSources = ( - connector: Connector, - sourceFilter: string, - expandedSources: boolean, - sourcesPage: number, - sourcesPerPage: number + connector: Connector, + sourceFilter: string, + expandedSources: boolean, + sourcesPage: number, + sourcesPerPage: number ) => { - const filteredSources = getFilteredSources(connector, sourceFilter); - - if (expandedSources) { - return filteredSources; - } - return filteredSources?.slice(0, sourcesPage * sourcesPerPage); + const filteredSources = getFilteredSources(connector, sourceFilter); + + if (expandedSources) { + return filteredSources; + } + return filteredSources?.slice(0, sourcesPage * sourcesPerPage); }; /** * Function to get the count of sources for a connector type */ export const getSourcesCount = (connectorSources: Connector[], connectorType: string) => { - const connector = connectorSources.find(c => c.type === connectorType); - return connector?.sources?.length || 0; + const connector = connectorSources.find((c) => c.type === connectorType); + return connector?.sources?.length || 0; }; /** * Function to get a citation source by ID */ export const getCitationSource = ( - citationId: number, - connectorSources: Connector[] + citationId: number, + connectorSources: Connector[] ): Source | null => { - for (const connector of connectorSources) { - const source = connector.sources?.find(s => s.id === citationId); - if (source) { - return { - ...source, - connectorType: connector.type - }; - } - } - return null; -}; \ No newline at end of file + for (const connector of connectorSources) { + const source = connector.sources?.find((s) => s.id === citationId); + if (source) { + return { + ...source, + connectorType: connector.type, + }; + } + } + return null; +}; diff --git a/surfsense_web/components/chat/index.ts b/surfsense_web/components/chat/index.ts index 812bf805b..9ce695909 100644 --- a/surfsense_web/components/chat/index.ts +++ b/surfsense_web/components/chat/index.ts @@ -1,8 +1,8 @@ // Export all components and utilities from the chat folder -export { default as SegmentedControl } from './SegmentedControl'; -export * from './ConnectorComponents'; -export * from './Citation'; -export * from './SourceUtils'; -export * from './ScrollUtils'; -export * from './CodeBlock'; -export * from './types'; \ No newline at end of file +export { default as SegmentedControl } from "./SegmentedControl"; +export * from "./ConnectorComponents"; +export * from "./Citation"; +export * from "./SourceUtils"; +export * from "./ScrollUtils"; +export * from "./CodeBlock"; +export * from "./types"; diff --git a/surfsense_web/components/chat/types.ts b/surfsense_web/components/chat/types.ts index 5ac770630..1544dd0c6 100644 --- a/surfsense_web/components/chat/types.ts +++ b/surfsense_web/components/chat/types.ts @@ -3,49 +3,48 @@ */ export type Source = { - id: number; - title: string; - description: string; - url: string; - connectorType?: string; + id: number; + title: string; + description: string; + url: string; + connectorType?: string; }; export type Connector = { - id: number; - type: string; - name: string; - sources?: Source[]; + id: number; + type: string; + name: string; + sources?: Source[]; }; export type StatusMessage = { - id: number; - message: string; - type: 'info' | 'success' | 'error' | 'warning'; - timestamp: string; + id: number; + message: string; + type: "info" | "success" | "error" | "warning"; + timestamp: string; }; export type ChatMessage = { - id: string; - role: 'user' | 'assistant'; - content: string; - timestamp?: string; + id: string; + role: "user" | "assistant"; + content: string; + timestamp?: string; }; // Define message types to match useChat() structure -export type MessageRole = 'user' | 'assistant' | 'system' | 'data'; +export type MessageRole = "user" | "assistant" | "system" | "data"; export interface ToolInvocation { - state: 'call' | 'result'; - toolCallId: string; - toolName: string; - args: any; - result?: any; + state: "call" | "result"; + toolCallId: string; + toolName: string; + args: any; + result?: any; } export interface ToolInvocationUIPart { - type: 'tool-invocation'; - toolInvocation: ToolInvocation; + type: "tool-invocation"; + toolInvocation: ToolInvocation; } - -export type ResearchMode = 'QNA' | 'REPORT_GENERAL' | 'REPORT_DEEP' | 'REPORT_DEEPER'; \ No newline at end of file +export type ResearchMode = "QNA" | "REPORT_GENERAL" | "REPORT_DEEP" | "REPORT_DEEPER"; diff --git a/surfsense_web/components/copy-button.tsx b/surfsense_web/components/copy-button.tsx index 7842f6ae6..b8466753e 100644 --- a/surfsense_web/components/copy-button.tsx +++ b/surfsense_web/components/copy-button.tsx @@ -4,11 +4,7 @@ import type { RefObject } from "react"; import { Button } from "./ui/button"; import { Copy, CopyCheck } from "lucide-react"; -export default function CopyButton({ - ref, -}: { - ref: RefObject; -}) { +export default function CopyButton({ ref }: { ref: RefObject }) { const [copy, setCopy] = useState(false); const timeoutRef = useRef(null); diff --git a/surfsense_web/components/document-viewer.tsx b/surfsense_web/components/document-viewer.tsx index b4fb7a419..07efddd24 100644 --- a/surfsense_web/components/document-viewer.tsx +++ b/surfsense_web/components/document-viewer.tsx @@ -1,34 +1,40 @@ -import React from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import type React from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { MarkdownViewer } from "@/components/markdown-viewer"; import { FileText } from "lucide-react"; interface DocumentViewerProps { - title: string; - content: string; - trigger?: React.ReactNode; + title: string; + content: string; + trigger?: React.ReactNode; } export function DocumentViewer({ title, content, trigger }: DocumentViewerProps) { - return ( - - - {trigger || ( - - )} - - - - {title} - -
    - -
    -
    -
    - ); -} \ No newline at end of file + return ( + + + {trigger || ( + + )} + + + + {title} + +
    + +
    +
    +
    + ); +} diff --git a/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx b/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx index dc1320fa3..aa8c0032e 100644 --- a/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx +++ b/surfsense_web/components/editConnector/EditConnectorLoadingSkeleton.tsx @@ -1,21 +1,21 @@ -import React from 'react'; +import React from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; export function EditConnectorLoadingSkeleton() { - return ( -
    - - - - - - - - - - - -
    - ); -} + return ( +
    + + + + + + + + + + + +
    + ); +} diff --git a/surfsense_web/components/editConnector/EditConnectorNameForm.tsx b/surfsense_web/components/editConnector/EditConnectorNameForm.tsx index 3f1882004..e0e60e06a 100644 --- a/surfsense_web/components/editConnector/EditConnectorNameForm.tsx +++ b/surfsense_web/components/editConnector/EditConnectorNameForm.tsx @@ -1,25 +1,27 @@ -import React from 'react'; -import { Control } from 'react-hook-form'; +import React from "react"; +import type { Control } from "react-hook-form"; import { FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; // Assuming EditConnectorFormValues is defined elsewhere or passed as generic interface EditConnectorNameFormProps { - control: Control; // Use Control if type is available + control: Control; // Use Control if type is available } export function EditConnectorNameForm({ control }: EditConnectorNameFormProps) { - return ( - ( - - Connector Name - - - - )} - /> - ); -} + return ( + ( + + Connector Name + + + + + + )} + /> + ); +} diff --git a/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx b/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx index 17f83f7ac..30721751b 100644 --- a/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx +++ b/surfsense_web/components/editConnector/EditGitHubConnectorConfig.tsx @@ -1,160 +1,189 @@ -import React from 'react'; -import { UseFormReturn } from 'react-hook-form'; -import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from "@/components/ui/form"; +import type React from "react"; +import type { UseFormReturn } from "react-hook-form"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, +} from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Skeleton } from "@/components/ui/skeleton"; -import { Edit, KeyRound, Loader2, CircleAlert } from 'lucide-react'; +import { Edit, KeyRound, Loader2, CircleAlert } from "lucide-react"; // Types needed from parent interface GithubRepo { - id: number; - name: string; - full_name: string; - private: boolean; - url: string; - description: string | null; - last_updated: string | null; + id: number; + name: string; + full_name: string; + private: boolean; + url: string; + description: string | null; + last_updated: string | null; } -type GithubPatFormValues = { github_pat: string; }; -type EditMode = 'viewing' | 'editing_repos'; +type GithubPatFormValues = { github_pat: string }; +type EditMode = "viewing" | "editing_repos"; interface EditGitHubConnectorConfigProps { - // State from parent - editMode: EditMode; - originalPat: string; - currentSelectedRepos: string[]; - fetchedRepos: GithubRepo[] | null; - newSelectedRepos: string[]; - isFetchingRepos: boolean; - // Forms from parent - patForm: UseFormReturn; - // Handlers from parent - setEditMode: (mode: EditMode) => void; - handleFetchRepositories: (values: GithubPatFormValues) => Promise; - handleRepoSelectionChange: (repoFullName: string, checked: boolean) => void; - setNewSelectedRepos: React.Dispatch>; - setFetchedRepos: React.Dispatch>; + // State from parent + editMode: EditMode; + originalPat: string; + currentSelectedRepos: string[]; + fetchedRepos: GithubRepo[] | null; + newSelectedRepos: string[]; + isFetchingRepos: boolean; + // Forms from parent + patForm: UseFormReturn; + // Handlers from parent + setEditMode: (mode: EditMode) => void; + handleFetchRepositories: (values: GithubPatFormValues) => Promise; + handleRepoSelectionChange: (repoFullName: string, checked: boolean) => void; + setNewSelectedRepos: React.Dispatch>; + setFetchedRepos: React.Dispatch>; } export function EditGitHubConnectorConfig({ - editMode, - originalPat, - currentSelectedRepos, - fetchedRepos, - newSelectedRepos, - isFetchingRepos, - patForm, - setEditMode, - handleFetchRepositories, - handleRepoSelectionChange, - setNewSelectedRepos, - setFetchedRepos + editMode, + originalPat, + currentSelectedRepos, + fetchedRepos, + newSelectedRepos, + isFetchingRepos, + patForm, + setEditMode, + handleFetchRepositories, + handleRepoSelectionChange, + setNewSelectedRepos, + setFetchedRepos, }: EditGitHubConnectorConfigProps) { + return ( +
    +

    Repository Selection & Access

    - return ( -
    -

    Repository Selection & Access

    + {/* Viewing Mode */} + {editMode === "viewing" && ( +
    + Currently Indexed Repositories: + {currentSelectedRepos.length > 0 ? ( +
      + {currentSelectedRepos.map((repo) => ( +
    • {repo}
    • + ))} +
    + ) : ( +

    (No repositories currently selected)

    + )} + + + To change repo selections or update the PAT, click above. + +
    + )} - {/* Viewing Mode */} - {editMode === 'viewing' && ( -
    - Currently Indexed Repositories: - {currentSelectedRepos.length > 0 ? ( -
      - {currentSelectedRepos.map(repo =>
    • {repo}
    • )} -
    - ) : ( -

    (No repositories currently selected)

    - )} - - To change repo selections or update the PAT, click above. -
    - )} + {/* Editing Mode */} + {editMode === "editing_repos" && ( +
    + {/* PAT Input */} +
    + ( + + + GitHub PAT + + + + + + Enter PAT to fetch/update repos or if you need to update the stored token. + + + + )} + /> + +
    - {/* Editing Mode */} - {editMode === 'editing_repos' && ( -
    - {/* PAT Input */} -
    - ( - - GitHub PAT - - Enter PAT to fetch/update repos or if you need to update the stored token. - - - )} - /> - -
    - - {/* Repo List */} - {isFetchingRepos && } - {!isFetchingRepos && fetchedRepos !== null && ( - fetchedRepos.length === 0 ? ( - - - No Repositories Found - Check PAT & permissions. - - ) : ( -
    - Select Repositories to Index ({newSelectedRepos.length} selected): -
    - {fetchedRepos.map((repo) => ( -
    - handleRepoSelectionChange(repo.full_name, !!checked)} - /> - -
    - ))} -
    -
    - ) - )} - -
    - )} -
    - ); -} + {/* Repo List */} + {isFetchingRepos && } + {!isFetchingRepos && + fetchedRepos !== null && + (fetchedRepos.length === 0 ? ( + + + No Repositories Found + Check PAT & permissions. + + ) : ( +
    + + Select Repositories to Index ({newSelectedRepos.length} selected): + +
    + {fetchedRepos.map((repo) => ( +
    + + handleRepoSelectionChange(repo.full_name, !!checked) + } + /> + +
    + ))} +
    +
    + ))} + +
    + )} +
    + ); +} diff --git a/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx b/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx index c0c803209..5e2a92eb4 100644 --- a/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx +++ b/surfsense_web/components/editConnector/EditSimpleTokenForm.tsx @@ -1,37 +1,48 @@ -import React from 'react'; -import { Control } from 'react-hook-form'; -import { FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from "@/components/ui/form"; +import React from "react"; +import type { Control } from "react-hook-form"; +import { + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, +} from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { KeyRound } from 'lucide-react'; +import { KeyRound } from "lucide-react"; // Assuming EditConnectorFormValues is defined elsewhere or passed as generic interface EditSimpleTokenFormProps { - control: Control; - fieldName: string; // e.g., "SLACK_BOT_TOKEN" - fieldLabel: string; // e.g., "Slack Bot Token" - fieldDescription: string; - placeholder?: string; + control: Control; + fieldName: string; // e.g., "SLACK_BOT_TOKEN" + fieldLabel: string; // e.g., "Slack Bot Token" + fieldDescription: string; + placeholder?: string; } export function EditSimpleTokenForm({ - control, - fieldName, - fieldLabel, - fieldDescription, - placeholder + control, + fieldName, + fieldLabel, + fieldDescription, + placeholder, }: EditSimpleTokenFormProps) { - return ( - ( - - {fieldLabel} - - {fieldDescription} - - - )} - /> - ); -} + return ( + ( + + + {fieldLabel} + + + + + {fieldDescription} + + + )} + /> + ); +} diff --git a/surfsense_web/components/editConnector/types.ts b/surfsense_web/components/editConnector/types.ts index ad16010bc..435f320e4 100644 --- a/surfsense_web/components/editConnector/types.ts +++ b/surfsense_web/components/editConnector/types.ts @@ -2,35 +2,36 @@ import * as z from "zod"; // Types export interface GithubRepo { - id: number; - name: string; - full_name: string; - private: boolean; - url: string; - description: string | null; - last_updated: string | null; + id: number; + name: string; + full_name: string; + private: boolean; + url: string; + description: string | null; + last_updated: string | null; } -export type EditMode = 'viewing' | 'editing_repos'; +export type EditMode = "viewing" | "editing_repos"; // Schemas export const githubPatSchema = z.object({ - github_pat: z.string() - .min(20, { message: "GitHub Personal Access Token seems too short." }) - .refine(pat => pat.startsWith('ghp_') || pat.startsWith('github_pat_'), { - message: "GitHub PAT should start with 'ghp_' or 'github_pat_'", - }), + github_pat: z + .string() + .min(20, { message: "GitHub Personal Access Token seems too short." }) + .refine((pat) => pat.startsWith("ghp_") || pat.startsWith("github_pat_"), { + message: "GitHub PAT should start with 'ghp_' or 'github_pat_'", + }), }); export type GithubPatFormValues = z.infer; export const editConnectorSchema = z.object({ - name: z.string().min(3, { message: "Connector name must be at least 3 characters." }), - SLACK_BOT_TOKEN: z.string().optional(), - NOTION_INTEGRATION_TOKEN: z.string().optional(), - SERPER_API_KEY: z.string().optional(), - TAVILY_API_KEY: z.string().optional(), - LINEAR_API_KEY: z.string().optional(), - LINKUP_API_KEY: z.string().optional(), - DISCORD_BOT_TOKEN: z.string().optional(), + name: z.string().min(3, { message: "Connector name must be at least 3 characters." }), + SLACK_BOT_TOKEN: z.string().optional(), + NOTION_INTEGRATION_TOKEN: z.string().optional(), + SERPER_API_KEY: z.string().optional(), + TAVILY_API_KEY: z.string().optional(), + LINEAR_API_KEY: z.string().optional(), + LINKUP_API_KEY: z.string().optional(), + DISCORD_BOT_TOKEN: z.string().optional(), }); -export type EditConnectorFormValues = z.infer; +export type EditConnectorFormValues = z.infer; diff --git a/surfsense_web/components/json-metadata-viewer.tsx b/surfsense_web/components/json-metadata-viewer.tsx index 3c0d8b03a..33c7bce10 100644 --- a/surfsense_web/components/json-metadata-viewer.tsx +++ b/surfsense_web/components/json-metadata-viewer.tsx @@ -1,55 +1,58 @@ import React from "react"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { FileJson } from "lucide-react"; import { JsonView, defaultStyles } from "react-json-view-lite"; import "react-json-view-lite/dist/index.css"; interface JsonMetadataViewerProps { - title: string; - metadata: any; - trigger?: React.ReactNode; + title: string; + metadata: any; + trigger?: React.ReactNode; } export function JsonMetadataViewer({ title, metadata, trigger }: JsonMetadataViewerProps) { - // Ensure metadata is a valid object - const jsonData = React.useMemo(() => { - if (!metadata) return {}; - - try { - // If metadata is a string, try to parse it - if (typeof metadata === "string") { - return JSON.parse(metadata); - } - // Otherwise, use it as is - return metadata; - } catch (error) { - console.error("Error parsing JSON metadata:", error); - return { error: "Invalid JSON metadata" }; - } - }, [metadata]); + // Ensure metadata is a valid object + const jsonData = React.useMemo(() => { + if (!metadata) return {}; - return ( - - - {trigger || ( - - )} - - - - {title} - Metadata - -
    - -
    -
    -
    - ); -} \ No newline at end of file + try { + // If metadata is a string, try to parse it + if (typeof metadata === "string") { + return JSON.parse(metadata); + } + // Otherwise, use it as is + return metadata; + } catch (error) { + console.error("Error parsing JSON metadata:", error); + return { error: "Invalid JSON metadata" }; + } + }, [metadata]); + + return ( + + + {trigger || ( + + )} + + + + {title} - Metadata + +
    + +
    +
    +
    + ); +} diff --git a/surfsense_web/components/markdown-viewer.tsx b/surfsense_web/components/markdown-viewer.tsx index 2e75e776d..48b12b19a 100644 --- a/surfsense_web/components/markdown-viewer.tsx +++ b/surfsense_web/components/markdown-viewer.tsx @@ -5,12 +5,9 @@ import rehypeSanitize from "rehype-sanitize"; import remarkGfm from "remark-gfm"; import { cn } from "@/lib/utils"; import { Citation } from "./chat/Citation"; -import { Source } from "./chat/types"; +import type { Source } from "./chat/types"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; -import { - oneLight, - oneDark, -} from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { oneLight, oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { Check, Copy } from "lucide-react"; import { useTheme } from "next-themes"; import CopyButton from "./copy-button"; @@ -68,12 +65,8 @@ export function MarkdownViewer({ : children; return
  • {processedChildren}
  • ; }, - ul: ({ node, ...props }: any) => ( -
      - ), - ol: ({ node, ...props }: any) => ( -
        - ), + ul: ({ node, ...props }: any) =>
          , + ol: ({ node, ...props }: any) =>
            , h1: ({ node, children, ...props }: any) => { const processedChildren = getCitationSource ? processCitationsInReactChildren(children, getCitationSource) @@ -115,14 +108,9 @@ export function MarkdownViewer({ ); }, blockquote: ({ node, ...props }: any) => ( -
            - ), - hr: ({ node, ...props }: any) => ( -
            +
            ), + hr: ({ node, ...props }: any) =>
            , img: ({ node, ...props }: any) => ( ), @@ -161,10 +149,7 @@ export function MarkdownViewer({ }, [getCitationSource]); return ( -
            +
            { +const CodeBlock = ({ children, language }: { children: string; language: string }) => { const [copied, setCopied] = useState(false); const { resolvedTheme, theme } = useTheme(); const [mounted, setMounted] = useState(false); @@ -272,9 +251,7 @@ const CodeBlock = ({ ) : (
            -						
            -							{children}
            -						
            +						{children}
             					
            )} @@ -285,7 +262,7 @@ const CodeBlock = ({ // Helper function to process citations within React children const processCitationsInReactChildren = ( children: React.ReactNode, - getCitationSource: (id: number) => Source | null, + getCitationSource: (id: number) => Source | null ): React.ReactNode => { // If children is not an array or string, just return it if (!children || (typeof children !== "string" && !Array.isArray(children))) { @@ -313,7 +290,7 @@ const processCitationsInReactChildren = ( // Process citation references in text content const processCitationsInText = ( text: string, - getCitationSource: (id: number) => Source | null, + getCitationSource: (id: number) => Source | null ): React.ReactNode[] => { // Use improved regex to catch citation numbers more reliably // This will match patterns like [1], [42], etc. including when they appear at the end of a line or sentence @@ -340,7 +317,7 @@ const processCitationsInText = ( citationText={match[0]} position={position} source={source} - />, + /> ); lastIndex = match.index + match[0].length; diff --git a/surfsense_web/components/onboard/add-provider-step.tsx b/surfsense_web/components/onboard/add-provider-step.tsx index b7871c6c7..6c2065de8 100644 --- a/surfsense_web/components/onboard/add-provider-step.tsx +++ b/surfsense_web/components/onboard/add-provider-step.tsx @@ -1,267 +1,282 @@ "use client"; -import React, { useState } from 'react'; -import { motion } from 'framer-motion'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import type React from "react"; +import { useState } from "react"; +import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; -import { Badge } from '@/components/ui/badge'; -import { Plus, Trash2, Bot, AlertCircle } from 'lucide-react'; -import { useLLMConfigs, CreateLLMConfig } from '@/hooks/use-llm-configs'; -import { toast } from 'sonner'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Badge } from "@/components/ui/badge"; +import { Plus, Trash2, Bot, AlertCircle } from "lucide-react"; +import { useLLMConfigs, type CreateLLMConfig } from "@/hooks/use-llm-configs"; +import { toast } from "sonner"; +import { Alert, AlertDescription } from "@/components/ui/alert"; const LLM_PROVIDERS = [ - { value: 'OPENAI', label: 'OpenAI', example: 'gpt-4o, gpt-4, gpt-3.5-turbo' }, - { value: 'ANTHROPIC', label: 'Anthropic', example: 'claude-3-5-sonnet-20241022, claude-3-opus-20240229' }, - { value: 'GROQ', label: 'Groq', example: 'llama3-70b-8192, mixtral-8x7b-32768' }, - { value: 'COHERE', label: 'Cohere', example: 'command-r-plus, command-r' }, - { value: 'HUGGINGFACE', label: 'HuggingFace', example: 'microsoft/DialoGPT-medium' }, - { value: 'AZURE_OPENAI', label: 'Azure OpenAI', example: 'gpt-4, gpt-35-turbo' }, - { value: 'GOOGLE', label: 'Google', example: 'gemini-pro, gemini-pro-vision' }, - { value: 'AWS_BEDROCK', label: 'AWS Bedrock', example: 'anthropic.claude-v2' }, - { value: 'OLLAMA', label: 'Ollama', example: 'llama2, codellama' }, - { value: 'MISTRAL', label: 'Mistral', example: 'mistral-large-latest, mistral-medium' }, - { value: 'TOGETHER_AI', label: 'Together AI', example: 'togethercomputer/llama-2-70b-chat' }, - { value: 'REPLICATE', label: 'Replicate', example: 'meta/llama-2-70b-chat' }, - { value: 'CUSTOM', label: 'Custom Provider', example: 'your-custom-model' }, + { value: "OPENAI", label: "OpenAI", example: "gpt-4o, gpt-4, gpt-3.5-turbo" }, + { + value: "ANTHROPIC", + label: "Anthropic", + example: "claude-3-5-sonnet-20241022, claude-3-opus-20240229", + }, + { value: "GROQ", label: "Groq", example: "llama3-70b-8192, mixtral-8x7b-32768" }, + { value: "COHERE", label: "Cohere", example: "command-r-plus, command-r" }, + { value: "HUGGINGFACE", label: "HuggingFace", example: "microsoft/DialoGPT-medium" }, + { value: "AZURE_OPENAI", label: "Azure OpenAI", example: "gpt-4, gpt-35-turbo" }, + { value: "GOOGLE", label: "Google", example: "gemini-pro, gemini-pro-vision" }, + { value: "AWS_BEDROCK", label: "AWS Bedrock", example: "anthropic.claude-v2" }, + { value: "OLLAMA", label: "Ollama", example: "llama2, codellama" }, + { value: "MISTRAL", label: "Mistral", example: "mistral-large-latest, mistral-medium" }, + { value: "TOGETHER_AI", label: "Together AI", example: "togethercomputer/llama-2-70b-chat" }, + { value: "REPLICATE", label: "Replicate", example: "meta/llama-2-70b-chat" }, + { value: "CUSTOM", label: "Custom Provider", example: "your-custom-model" }, ]; interface AddProviderStepProps { - onConfigCreated?: () => void; - onConfigDeleted?: () => void; + onConfigCreated?: () => void; + onConfigDeleted?: () => void; } export function AddProviderStep({ onConfigCreated, onConfigDeleted }: AddProviderStepProps) { - const { llmConfigs, createLLMConfig, deleteLLMConfig } = useLLMConfigs(); - const [isAddingNew, setIsAddingNew] = useState(false); - const [formData, setFormData] = useState({ - name: '', - provider: '', - custom_provider: '', - model_name: '', - api_key: '', - api_base: '', - litellm_params: {} - }); - const [isSubmitting, setIsSubmitting] = useState(false); + const { llmConfigs, createLLMConfig, deleteLLMConfig } = useLLMConfigs(); + const [isAddingNew, setIsAddingNew] = useState(false); + const [formData, setFormData] = useState({ + name: "", + provider: "", + custom_provider: "", + model_name: "", + api_key: "", + api_base: "", + litellm_params: {}, + }); + const [isSubmitting, setIsSubmitting] = useState(false); - const handleInputChange = (field: keyof CreateLLMConfig, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })); - }; + const handleInputChange = (field: keyof CreateLLMConfig, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) { - toast.error('Please fill in all required fields'); - return; - } + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) { + toast.error("Please fill in all required fields"); + return; + } - setIsSubmitting(true); - const result = await createLLMConfig(formData); - setIsSubmitting(false); + setIsSubmitting(true); + const result = await createLLMConfig(formData); + setIsSubmitting(false); - if (result) { - setFormData({ - name: '', - provider: '', - custom_provider: '', - model_name: '', - api_key: '', - api_base: '', - litellm_params: {} - }); - setIsAddingNew(false); - // Notify parent component that a config was created - onConfigCreated?.(); - } - }; + if (result) { + setFormData({ + name: "", + provider: "", + custom_provider: "", + model_name: "", + api_key: "", + api_base: "", + litellm_params: {}, + }); + setIsAddingNew(false); + // Notify parent component that a config was created + onConfigCreated?.(); + } + }; - const selectedProvider = LLM_PROVIDERS.find(p => p.value === formData.provider); + const selectedProvider = LLM_PROVIDERS.find((p) => p.value === formData.provider); - return ( -
            - {/* Info Alert */} - - - - Add at least one LLM provider to continue. You can configure multiple providers and choose specific roles for each one in the next step. - - + return ( +
            + {/* Info Alert */} + + + + Add at least one LLM provider to continue. You can configure multiple providers and choose + specific roles for each one in the next step. + + - {/* Existing Configurations */} - {llmConfigs.length > 0 && ( -
            -

            Your LLM Configurations

            -
            - {llmConfigs.map((config) => ( - - - -
            -
            -
            - -

            {config.name}

            - {config.provider} -
            -

            - Model: {config.model_name} - {config.api_base && ` • Base: ${config.api_base}`} -

            -
            - -
            -
            -
            -
            - ))} -
            -
            - )} + {/* Existing Configurations */} + {llmConfigs.length > 0 && ( +
            +

            Your LLM Configurations

            +
            + {llmConfigs.map((config) => ( + + + +
            +
            +
            + +

            {config.name}

            + {config.provider} +
            +

            + Model: {config.model_name} + {config.api_base && ` • Base: ${config.api_base}`} +

            +
            + +
            +
            +
            +
            + ))} +
            +
            + )} - {/* Add New Provider */} - {!isAddingNew ? ( - - - -

            Add LLM Provider

            -

            - Configure your first model provider to get started -

            - -
            -
            - ) : ( - - - Add New LLM Provider - - Configure a new language model provider for your AI assistant - - - -
            -
            -
            - - handleInputChange('name', e.target.value)} - required - /> -
            + {/* Add New Provider */} + {!isAddingNew ? ( + + + +

            Add LLM Provider

            +

            + Configure your first model provider to get started +

            + +
            +
            + ) : ( + + + Add New LLM Provider + + Configure a new language model provider for your AI assistant + + + + +
            +
            + + handleInputChange("name", e.target.value)} + required + /> +
            -
            - - -
            -
            +
            + + +
            +
            - {formData.provider === 'CUSTOM' && ( -
            - - handleInputChange('custom_provider', e.target.value)} - required - /> -
            - )} + {formData.provider === "CUSTOM" && ( +
            + + handleInputChange("custom_provider", e.target.value)} + required + /> +
            + )} -
            - - handleInputChange('model_name', e.target.value)} - required - /> - {selectedProvider && ( -

            - Examples: {selectedProvider.example} -

            - )} -
            +
            + + handleInputChange("model_name", e.target.value)} + required + /> + {selectedProvider && ( +

            + Examples: {selectedProvider.example} +

            + )} +
            -
            - - handleInputChange('api_key', e.target.value)} - required - /> -
            +
            + + handleInputChange("api_key", e.target.value)} + required + /> +
            -
            - - handleInputChange('api_base', e.target.value)} - /> -
            +
            + + handleInputChange("api_base", e.target.value)} + /> +
            -
            - - -
            -
            -
            -
            - )} -
            - ); -} \ No newline at end of file +
            + + +
            + + + + )} +
            + ); +} diff --git a/surfsense_web/components/onboard/assign-roles-step.tsx b/surfsense_web/components/onboard/assign-roles-step.tsx index 255fdeed1..c32cc82d4 100644 --- a/surfsense_web/components/onboard/assign-roles-step.tsx +++ b/surfsense_web/components/onboard/assign-roles-step.tsx @@ -1,232 +1,246 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Badge } from '@/components/ui/badge'; -import { Brain, Zap, Bot, AlertCircle, CheckCircle } from 'lucide-react'; -import { useLLMConfigs, useLLMPreferences } from '@/hooks/use-llm-configs'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import React, { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { Brain, Zap, Bot, AlertCircle, CheckCircle } from "lucide-react"; +import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; +import { Alert, AlertDescription } from "@/components/ui/alert"; const ROLE_DESCRIPTIONS = { - long_context: { - icon: Brain, - title: 'Long Context LLM', - description: 'Handles complex tasks requiring extensive context understanding and reasoning', - color: 'bg-blue-100 text-blue-800 border-blue-200', - examples: 'Document analysis, research synthesis, complex Q&A' - }, - fast: { - icon: Zap, - title: 'Fast LLM', - description: 'Optimized for quick responses and real-time interactions', - color: 'bg-green-100 text-green-800 border-green-200', - examples: 'Quick searches, simple questions, instant responses' - }, - strategic: { - icon: Bot, - title: 'Strategic LLM', - description: 'Advanced reasoning for planning and strategic decision making', - color: 'bg-purple-100 text-purple-800 border-purple-200', - examples: 'Planning workflows, strategic analysis, complex problem solving' - } + long_context: { + icon: Brain, + title: "Long Context LLM", + description: "Handles complex tasks requiring extensive context understanding and reasoning", + color: "bg-blue-100 text-blue-800 border-blue-200", + examples: "Document analysis, research synthesis, complex Q&A", + }, + fast: { + icon: Zap, + title: "Fast LLM", + description: "Optimized for quick responses and real-time interactions", + color: "bg-green-100 text-green-800 border-green-200", + examples: "Quick searches, simple questions, instant responses", + }, + strategic: { + icon: Bot, + title: "Strategic LLM", + description: "Advanced reasoning for planning and strategic decision making", + color: "bg-purple-100 text-purple-800 border-purple-200", + examples: "Planning workflows, strategic analysis, complex problem solving", + }, }; interface AssignRolesStepProps { - onPreferencesUpdated?: () => Promise; + onPreferencesUpdated?: () => Promise; } export function AssignRolesStep({ onPreferencesUpdated }: AssignRolesStepProps) { - const { llmConfigs } = useLLMConfigs(); - const { preferences, updatePreferences } = useLLMPreferences(); - + const { llmConfigs } = useLLMConfigs(); + const { preferences, updatePreferences } = useLLMPreferences(); - const [assignments, setAssignments] = useState({ - long_context_llm_id: preferences.long_context_llm_id || '', - fast_llm_id: preferences.fast_llm_id || '', - strategic_llm_id: preferences.strategic_llm_id || '' - }); + const [assignments, setAssignments] = useState({ + long_context_llm_id: preferences.long_context_llm_id || "", + fast_llm_id: preferences.fast_llm_id || "", + strategic_llm_id: preferences.strategic_llm_id || "", + }); + useEffect(() => { + setAssignments({ + long_context_llm_id: preferences.long_context_llm_id || "", + fast_llm_id: preferences.fast_llm_id || "", + strategic_llm_id: preferences.strategic_llm_id || "", + }); + }, [preferences]); - useEffect(() => { - setAssignments({ - long_context_llm_id: preferences.long_context_llm_id || '', - fast_llm_id: preferences.fast_llm_id || '', - strategic_llm_id: preferences.strategic_llm_id || '' - }); - }, [preferences]); + const handleRoleAssignment = async (role: string, configId: string) => { + const newAssignments = { + ...assignments, + [role]: configId === "" ? "" : parseInt(configId), + }; - const handleRoleAssignment = async (role: string, configId: string) => { - const newAssignments = { - ...assignments, - [role]: configId === '' ? '' : parseInt(configId) - }; - - setAssignments(newAssignments); - - // Auto-save if this assignment completes all roles - const hasAllAssignments = newAssignments.long_context_llm_id && newAssignments.fast_llm_id && newAssignments.strategic_llm_id; - - if (hasAllAssignments) { - const numericAssignments = { - long_context_llm_id: typeof newAssignments.long_context_llm_id === 'string' ? parseInt(newAssignments.long_context_llm_id) : newAssignments.long_context_llm_id, - fast_llm_id: typeof newAssignments.fast_llm_id === 'string' ? parseInt(newAssignments.fast_llm_id) : newAssignments.fast_llm_id, - strategic_llm_id: typeof newAssignments.strategic_llm_id === 'string' ? parseInt(newAssignments.strategic_llm_id) : newAssignments.strategic_llm_id, - }; - - const success = await updatePreferences(numericAssignments); - - // Refresh parent preferences state - if (success && onPreferencesUpdated) { - await onPreferencesUpdated(); - } - } - }; + setAssignments(newAssignments); + // Auto-save if this assignment completes all roles + const hasAllAssignments = + newAssignments.long_context_llm_id && + newAssignments.fast_llm_id && + newAssignments.strategic_llm_id; + if (hasAllAssignments) { + const numericAssignments = { + long_context_llm_id: + typeof newAssignments.long_context_llm_id === "string" + ? parseInt(newAssignments.long_context_llm_id) + : newAssignments.long_context_llm_id, + fast_llm_id: + typeof newAssignments.fast_llm_id === "string" + ? parseInt(newAssignments.fast_llm_id) + : newAssignments.fast_llm_id, + strategic_llm_id: + typeof newAssignments.strategic_llm_id === "string" + ? parseInt(newAssignments.strategic_llm_id) + : newAssignments.strategic_llm_id, + }; - const isAssignmentComplete = assignments.long_context_llm_id && assignments.fast_llm_id && assignments.strategic_llm_id; + const success = await updatePreferences(numericAssignments); - if (llmConfigs.length === 0) { - return ( -
            - -

            No LLM Configurations Found

            -

            - Please add at least one LLM provider in the previous step before assigning roles. -

            -
            - ); - } + // Refresh parent preferences state + if (success && onPreferencesUpdated) { + await onPreferencesUpdated(); + } + } + }; - return ( -
            - {/* Info Alert */} - - - - Assign your LLM configurations to specific roles. Each role serves different purposes in your workflow. - - + const isAssignmentComplete = + assignments.long_context_llm_id && assignments.fast_llm_id && assignments.strategic_llm_id; - {/* Role Assignment Cards */} -
            - {Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => { - const IconComponent = role.icon; - const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments]; - const assignedConfig = llmConfigs.find(config => config.id === currentAssignment); - - return ( - - - -
            -
            -
            - -
            -
            - {role.title} - {role.description} -
            -
            - {currentAssignment && ( - - )} -
            -
            - -
            - Use cases: {role.examples} -
            - -
            - - -
            + return ( +
            + {/* Info Alert */} + + + + Assign your LLM configurations to specific roles. Each role serves different purposes in + your workflow. + + - {assignedConfig && ( -
            -
            - - Assigned: - {assignedConfig.provider} - {assignedConfig.name} -
            -
            - Model: {assignedConfig.model_name} -
            -
            - )} - - - - ); - })} -
            + {/* Role Assignment Cards */} +
            + {Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => { + const IconComponent = role.icon; + const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments]; + const assignedConfig = llmConfigs.find((config) => config.id === currentAssignment); + return ( + + + +
            +
            +
            + +
            +
            + {role.title} + {role.description} +
            +
            + {currentAssignment && } +
            +
            + +
            + Use cases: {role.examples} +
            +
            + + +
            - {/* Status Indicator */} - {isAssignmentComplete && ( -
            -
            - - All roles assigned and saved! -
            -
            - )} + {assignedConfig && ( +
            +
            + + Assigned: + {assignedConfig.provider} + {assignedConfig.name} +
            +
            + Model: {assignedConfig.model_name} +
            +
            + )} +
            +
            +
            + ); + })} +
            - {/* Progress Indicator */} -
            -
            - Progress: -
            - {Object.keys(ROLE_DESCRIPTIONS).map((key, index) => ( -
            - ))} -
            - - {Object.values(assignments).filter(Boolean).length} of {Object.keys(ROLE_DESCRIPTIONS).length} roles assigned - -
            -
            -
            - ); -} \ No newline at end of file + {/* Status Indicator */} + {isAssignmentComplete && ( +
            +
            + + All roles assigned and saved! +
            +
            + )} + + {/* Progress Indicator */} +
            +
            + Progress: +
            + {Object.keys(ROLE_DESCRIPTIONS).map((key, index) => ( +
            + ))} +
            + + {Object.values(assignments).filter(Boolean).length} of{" "} + {Object.keys(ROLE_DESCRIPTIONS).length} roles assigned + +
            +
            +
            + ); +} diff --git a/surfsense_web/components/onboard/completion-step.tsx b/surfsense_web/components/onboard/completion-step.tsx index 1a14753b2..837944141 100644 --- a/surfsense_web/components/onboard/completion-step.tsx +++ b/surfsense_web/components/onboard/completion-step.tsx @@ -1,125 +1,125 @@ "use client"; -import React from 'react'; -import { motion } from 'framer-motion'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { CheckCircle, Bot, Brain, Zap, Sparkles, ArrowRight } from 'lucide-react'; -import { useLLMConfigs, useLLMPreferences } from '@/hooks/use-llm-configs'; - +import React from "react"; +import { motion } from "framer-motion"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { CheckCircle, Bot, Brain, Zap, Sparkles, ArrowRight } from "lucide-react"; +import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; const ROLE_ICONS = { - long_context: Brain, - fast: Zap, - strategic: Bot + long_context: Brain, + fast: Zap, + strategic: Bot, }; export function CompletionStep() { - const { llmConfigs } = useLLMConfigs(); - const { preferences } = useLLMPreferences(); + const { llmConfigs } = useLLMConfigs(); + const { preferences } = useLLMPreferences(); - const assignedConfigs = { - long_context: llmConfigs.find(c => c.id === preferences.long_context_llm_id), - fast: llmConfigs.find(c => c.id === preferences.fast_llm_id), - strategic: llmConfigs.find(c => c.id === preferences.strategic_llm_id) - }; + const assignedConfigs = { + long_context: llmConfigs.find((c) => c.id === preferences.long_context_llm_id), + fast: llmConfigs.find((c) => c.id === preferences.fast_llm_id), + strategic: llmConfigs.find((c) => c.id === preferences.strategic_llm_id), + }; - return ( -
            - {/* Success Message */} - -
            - -
            -

            Setup Complete!

            -
            + return ( +
            + {/* Success Message */} + +
            + +
            +

            Setup Complete!

            +
            - {/* Configuration Summary */} - - - - - - Your LLM Configuration - - - Here's a summary of your setup - - - - {Object.entries(assignedConfigs).map(([role, config]) => { - if (!config) return null; - - const IconComponent = ROLE_ICONS[role as keyof typeof ROLE_ICONS]; - const roleDisplayNames = { - long_context: 'Long Context LLM', - fast: 'Fast LLM', - strategic: 'Strategic LLM' - }; - - return ( - -
            -
            - -
            -
            -

            {roleDisplayNames[role as keyof typeof roleDisplayNames]}

            -

            {config.name}

            -
            -
            -
            - {config.provider} - {config.model_name} -
            -
            - ); - })} -
            -
            -
            + {/* Configuration Summary */} + + + + + + Your LLM Configuration + + Here's a summary of your setup + + + {Object.entries(assignedConfigs).map(([role, config]) => { + if (!config) return null; + const IconComponent = ROLE_ICONS[role as keyof typeof ROLE_ICONS]; + const roleDisplayNames = { + long_context: "Long Context LLM", + fast: "Fast LLM", + strategic: "Strategic LLM", + }; - {/* Next Steps */} - - - -
            -
            - -
            -

            Ready to Get Started?

            -
            -

            - Click "Complete Setup" to enter your dashboard and start exploring! -

            -
            - ✓ {llmConfigs.length} LLM provider{llmConfigs.length > 1 ? 's' : ''} configured - ✓ All roles assigned - ✓ Ready to use -
            -
            -
            -
            -
            - ); -} \ No newline at end of file + return ( + +
            +
            + +
            +
            +

            + {roleDisplayNames[role as keyof typeof roleDisplayNames]} +

            +

            {config.name}

            +
            +
            +
            + {config.provider} + {config.model_name} +
            +
            + ); + })} + + + + + {/* Next Steps */} + + + +
            +
            + +
            +

            Ready to Get Started?

            +
            +

            + Click "Complete Setup" to enter your dashboard and start exploring! +

            +
            + + ✓ {llmConfigs.length} LLM provider{llmConfigs.length > 1 ? "s" : ""} configured + + ✓ All roles assigned + ✓ Ready to use +
            +
            +
            +
            +
            + ); +} diff --git a/surfsense_web/components/search-space-form.tsx b/surfsense_web/components/search-space-form.tsx index d03cab385..bdac6f22f 100644 --- a/surfsense_web/components/search-space-form.tsx +++ b/surfsense_web/components/search-space-form.tsx @@ -11,254 +11,246 @@ import { Separator } from "@/components/ui/separator"; import { Tilt } from "@/components/ui/tilt"; import { Spotlight } from "@/components/ui/spotlight"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, } from "@/components/ui/form"; import { useRouter } from "next/navigation"; // Define the form schema with Zod const searchSpaceFormSchema = z.object({ - name: z.string().min(3, "Name is required"), - description: z.string().min(10, "Description is required"), + name: z.string().min(3, "Name is required"), + description: z.string().min(10, "Description is required"), }); // Define the type for the form values type SearchSpaceFormValues = z.infer; interface SearchSpaceFormProps { - onSubmit?: (data: { name: string; description: string }) => void; - onDelete?: () => void; - className?: string; - isEditing?: boolean; - initialData?: { name: string; description: string }; + onSubmit?: (data: { name: string; description: string }) => void; + onDelete?: () => void; + className?: string; + isEditing?: boolean; + initialData?: { name: string; description: string }; } -export function SearchSpaceForm({ - onSubmit, - onDelete, - className, - isEditing = false, - initialData = { name: "", description: "" } +export function SearchSpaceForm({ + onSubmit, + onDelete, + className, + isEditing = false, + initialData = { name: "", description: "" }, }: SearchSpaceFormProps) { - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const router = useRouter(); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const router = useRouter(); - // Initialize the form with React Hook Form and Zod validation - const form = useForm({ - resolver: zodResolver(searchSpaceFormSchema), - defaultValues: { - name: initialData.name, - description: initialData.description, - }, - }); + // Initialize the form with React Hook Form and Zod validation + const form = useForm({ + resolver: zodResolver(searchSpaceFormSchema), + defaultValues: { + name: initialData.name, + description: initialData.description, + }, + }); - // Handle form submission - const handleFormSubmit = (values: SearchSpaceFormValues) => { - if (onSubmit) { - onSubmit(values); - } - }; + // Handle form submission + const handleFormSubmit = (values: SearchSpaceFormValues) => { + if (onSubmit) { + onSubmit(values); + } + }; - // Handle delete confirmation - const handleDelete = () => { - if (onDelete) { - onDelete(); - } - setShowDeleteDialog(false); - }; + // Handle delete confirmation + const handleDelete = () => { + if (onDelete) { + onDelete(); + } + setShowDeleteDialog(false); + }; - // Animation variants - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - }, - }, - }; + // Animation variants + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, + }; - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { - type: "spring", - stiffness: 300, - damping: 24, - }, - }, - }; + const itemVariants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 300, + damping: 24, + }, + }, + }; - return ( - - -
            -

            - {isEditing ? "Edit Search Space" : "Create Search Space"} -

            -

            - {isEditing - ? "Update your search space details" - : "Create a new search space to organize your documents, chats, and podcasts."} -

            -
            - + return ( + + +
            +

            + {isEditing ? "Edit Search Space" : "Create Search Space"} +

            +

            + {isEditing + ? "Update your search space details" + : "Create a new search space to organize your documents, chats, and podcasts."} +

            +
            + +
            -
            + + + +
            +
            +
            + + + +

            Search Space

            +
            + {isEditing && onDelete && ( + + + + + + + Are you sure? + + This action cannot be undone. This will permanently delete your search + space. + + + + Cancel + Delete + + + + )} +
            +

            + A search space allows you to organize and search through your documents, generate + podcasts, and have AI-powered conversations about your content. +

            +
            +
            +
            - - - -
            -
            -
            - - - -

            Search Space

            -
            - {isEditing && onDelete && ( - - - - - - - Are you sure? - - This action cannot be undone. This will permanently delete your search space. - - - - Cancel - Delete - - - - )} -
            -

            - A search space allows you to organize and search through your documents, - generate podcasts, and have AI-powered conversations about your content. -

            -
            -
            -
            + - +
            + + ( + + Name + + + + A unique name for your search space. + + + )} + /> - - - ( - - Name - - - - - A unique name for your search space. - - - - )} - /> + ( + + Description + + + + + A brief description of what this search space will be used for. + + + + )} + /> - ( - - Description - - - - - A brief description of what this search space will be used for. - - - - )} - /> - -
            - -
            - - -
            - ); +
            + +
            + + +
            + ); } -export default SearchSpaceForm; \ No newline at end of file +export default SearchSpaceForm; diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx index 581732d1b..0307b202b 100644 --- a/surfsense_web/components/settings/llm-role-manager.tsx +++ b/surfsense_web/components/settings/llm-role-manager.tsx @@ -1,465 +1,517 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { motion } from 'framer-motion'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { - Brain, - Zap, - Bot, - AlertCircle, - CheckCircle, - Settings2, - RefreshCw, - Save, - RotateCcw, - Loader2 -} from 'lucide-react'; -import { useLLMConfigs, useLLMPreferences } from '@/hooks/use-llm-configs'; -import { Alert, AlertDescription } from '@/components/ui/alert'; -import { toast } from 'sonner'; +import React, { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Brain, + Zap, + Bot, + AlertCircle, + CheckCircle, + Settings2, + RefreshCw, + Save, + RotateCcw, + Loader2, +} from "lucide-react"; +import { useLLMConfigs, useLLMPreferences } from "@/hooks/use-llm-configs"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { toast } from "sonner"; const ROLE_DESCRIPTIONS = { - long_context: { - icon: Brain, - title: 'Long Context LLM', - description: 'Handles complex tasks requiring extensive context understanding and reasoning', - color: 'bg-blue-100 text-blue-800 border-blue-200', - examples: 'Document analysis, research synthesis, complex Q&A', - characteristics: ['Large context window', 'Deep reasoning', 'Complex analysis'] - }, - fast: { - icon: Zap, - title: 'Fast LLM', - description: 'Optimized for quick responses and real-time interactions', - color: 'bg-green-100 text-green-800 border-green-200', - examples: 'Quick searches, simple questions, instant responses', - characteristics: ['Low latency', 'Quick responses', 'Real-time chat'] - }, - strategic: { - icon: Bot, - title: 'Strategic LLM', - description: 'Advanced reasoning for planning and strategic decision making', - color: 'bg-purple-100 text-purple-800 border-purple-200', - examples: 'Planning workflows, strategic analysis, complex problem solving', - characteristics: ['Strategic thinking', 'Long-term planning', 'Complex reasoning'] - } + long_context: { + icon: Brain, + title: "Long Context LLM", + description: "Handles complex tasks requiring extensive context understanding and reasoning", + color: "bg-blue-100 text-blue-800 border-blue-200", + examples: "Document analysis, research synthesis, complex Q&A", + characteristics: ["Large context window", "Deep reasoning", "Complex analysis"], + }, + fast: { + icon: Zap, + title: "Fast LLM", + description: "Optimized for quick responses and real-time interactions", + color: "bg-green-100 text-green-800 border-green-200", + examples: "Quick searches, simple questions, instant responses", + characteristics: ["Low latency", "Quick responses", "Real-time chat"], + }, + strategic: { + icon: Bot, + title: "Strategic LLM", + description: "Advanced reasoning for planning and strategic decision making", + color: "bg-purple-100 text-purple-800 border-purple-200", + examples: "Planning workflows, strategic analysis, complex problem solving", + characteristics: ["Strategic thinking", "Long-term planning", "Complex reasoning"], + }, }; export function LLMRoleManager() { - const { llmConfigs, loading: configsLoading, error: configsError, refreshConfigs } = useLLMConfigs(); - const { preferences, loading: preferencesLoading, error: preferencesError, updatePreferences, refreshPreferences } = useLLMPreferences(); - - const [assignments, setAssignments] = useState({ - long_context_llm_id: preferences.long_context_llm_id || '', - fast_llm_id: preferences.fast_llm_id || '', - strategic_llm_id: preferences.strategic_llm_id || '' - }); + const { + llmConfigs, + loading: configsLoading, + error: configsError, + refreshConfigs, + } = useLLMConfigs(); + const { + preferences, + loading: preferencesLoading, + error: preferencesError, + updatePreferences, + refreshPreferences, + } = useLLMPreferences(); - const [hasChanges, setHasChanges] = useState(false); - const [isSaving, setIsSaving] = useState(false); + const [assignments, setAssignments] = useState({ + long_context_llm_id: preferences.long_context_llm_id || "", + fast_llm_id: preferences.fast_llm_id || "", + strategic_llm_id: preferences.strategic_llm_id || "", + }); - useEffect(() => { - const newAssignments = { - long_context_llm_id: preferences.long_context_llm_id || '', - fast_llm_id: preferences.fast_llm_id || '', - strategic_llm_id: preferences.strategic_llm_id || '' - }; - setAssignments(newAssignments); - setHasChanges(false); - }, [preferences]); + const [hasChanges, setHasChanges] = useState(false); + const [isSaving, setIsSaving] = useState(false); - const handleRoleAssignment = (role: string, configId: string) => { - const newAssignments = { - ...assignments, - [role]: configId === 'unassigned' ? '' : parseInt(configId) - }; - - setAssignments(newAssignments); - - // Check if there are changes compared to current preferences - const currentPrefs = { - long_context_llm_id: preferences.long_context_llm_id || '', - fast_llm_id: preferences.fast_llm_id || '', - strategic_llm_id: preferences.strategic_llm_id || '' - }; - - const hasChangesNow = Object.keys(newAssignments).some( - key => newAssignments[key as keyof typeof newAssignments] !== currentPrefs[key as keyof typeof currentPrefs] - ); - - setHasChanges(hasChangesNow); - }; + useEffect(() => { + const newAssignments = { + long_context_llm_id: preferences.long_context_llm_id || "", + fast_llm_id: preferences.fast_llm_id || "", + strategic_llm_id: preferences.strategic_llm_id || "", + }; + setAssignments(newAssignments); + setHasChanges(false); + }, [preferences]); - const handleSave = async () => { - setIsSaving(true); - - const numericAssignments = { - long_context_llm_id: typeof assignments.long_context_llm_id === 'string' - ? (assignments.long_context_llm_id ? parseInt(assignments.long_context_llm_id) : undefined) - : assignments.long_context_llm_id, - fast_llm_id: typeof assignments.fast_llm_id === 'string' - ? (assignments.fast_llm_id ? parseInt(assignments.fast_llm_id) : undefined) - : assignments.fast_llm_id, - strategic_llm_id: typeof assignments.strategic_llm_id === 'string' - ? (assignments.strategic_llm_id ? parseInt(assignments.strategic_llm_id) : undefined) - : assignments.strategic_llm_id, - }; - - const success = await updatePreferences(numericAssignments); - - if (success) { - setHasChanges(false); - toast.success('LLM role assignments saved successfully!'); - } - - setIsSaving(false); - }; + const handleRoleAssignment = (role: string, configId: string) => { + const newAssignments = { + ...assignments, + [role]: configId === "unassigned" ? "" : parseInt(configId), + }; - const handleReset = () => { - setAssignments({ - long_context_llm_id: preferences.long_context_llm_id || '', - fast_llm_id: preferences.fast_llm_id || '', - strategic_llm_id: preferences.strategic_llm_id || '' - }); - setHasChanges(false); - }; + setAssignments(newAssignments); - const isAssignmentComplete = assignments.long_context_llm_id && assignments.fast_llm_id && assignments.strategic_llm_id; - const assignedConfigIds = Object.values(assignments).filter(id => id !== ''); - const availableConfigs = llmConfigs.filter(config => config.id && config.id.toString().trim() !== ''); - - const isLoading = configsLoading || preferencesLoading; - const hasError = configsError || preferencesError; + // Check if there are changes compared to current preferences + const currentPrefs = { + long_context_llm_id: preferences.long_context_llm_id || "", + fast_llm_id: preferences.fast_llm_id || "", + strategic_llm_id: preferences.strategic_llm_id || "", + }; - return ( -
            - {/* Header */} -
            -
            -
            -
            - -
            -
            -

            LLM Role Management

            -

            - Assign your LLM configurations to specific roles for different purposes. -

            -
            -
            -
            -
            - - -
            -
            + const hasChangesNow = Object.keys(newAssignments).some( + (key) => + newAssignments[key as keyof typeof newAssignments] !== + currentPrefs[key as keyof typeof currentPrefs] + ); - {/* Error Alert */} - {hasError && ( - - - - {configsError || preferencesError} - - - )} + setHasChanges(hasChangesNow); + }; - {/* Loading State */} - {isLoading && ( - - -
            - - - {configsLoading && preferencesLoading ? 'Loading configurations and preferences...' : - configsLoading ? 'Loading configurations...' : - 'Loading preferences...'} - -
            -
            -
            - )} + const handleSave = async () => { + setIsSaving(true); - {/* Stats Overview */} - {!isLoading && !hasError && ( -
            - - -
            -
            -

            {availableConfigs.length}

            -

            Available Models

            -
            -
            - -
            -
            -
            -
            - - - -
            -
            -

            {assignedConfigIds.length}

            -

            Assigned Roles

            -
            -
            - -
            -
            -
            -
            + const numericAssignments = { + long_context_llm_id: + typeof assignments.long_context_llm_id === "string" + ? assignments.long_context_llm_id + ? parseInt(assignments.long_context_llm_id) + : undefined + : assignments.long_context_llm_id, + fast_llm_id: + typeof assignments.fast_llm_id === "string" + ? assignments.fast_llm_id + ? parseInt(assignments.fast_llm_id) + : undefined + : assignments.fast_llm_id, + strategic_llm_id: + typeof assignments.strategic_llm_id === "string" + ? assignments.strategic_llm_id + ? parseInt(assignments.strategic_llm_id) + : undefined + : assignments.strategic_llm_id, + }; - - -
            -
            -

            - {Math.round((assignedConfigIds.length / 3) * 100)}% -

            -

            Completion

            -
            -
            - {isAssignmentComplete ? ( - - ) : ( - - )} -
            -
            -
            -
            + const success = await updatePreferences(numericAssignments); - - -
            -
            -

            - {isAssignmentComplete ? 'Ready' : 'Setup'} -

            -

            Status

            -
            -
            - {isAssignmentComplete ? ( - - ) : ( - - )} -
            -
            -
            -
            -
            - )} + if (success) { + setHasChanges(false); + toast.success("LLM role assignments saved successfully!"); + } - {/* Info Alert */} - {!isLoading && !hasError && ( -
            - {availableConfigs.length === 0 ? ( - - - - No LLM configurations found. Please add at least one LLM provider in the Model Configs tab before assigning roles. - - - ) : !isAssignmentComplete ? ( - - - - Complete all role assignments to enable full functionality. Each role serves different purposes in your workflow. - - - ) : ( - - - - All roles are assigned and ready to use! Your LLM configuration is complete. - - - )} + setIsSaving(false); + }; - {/* Role Assignment Cards */} - {availableConfigs.length > 0 && ( -
            - {Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => { - const IconComponent = role.icon; - const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments]; - const assignedConfig = availableConfigs.find(config => config.id === currentAssignment); - - return ( - - - -
            -
            -
            - -
            -
            - {role.title} - {role.description} -
            -
            - {currentAssignment && ( - - )} -
            -
            - -
            -
            - Use cases: {role.examples} -
            -
            - {role.characteristics.map((char, idx) => ( - - {char} - - ))} -
            -
            - -
            - - -
            + const handleReset = () => { + setAssignments({ + long_context_llm_id: preferences.long_context_llm_id || "", + fast_llm_id: preferences.fast_llm_id || "", + strategic_llm_id: preferences.strategic_llm_id || "", + }); + setHasChanges(false); + }; - {assignedConfig && ( -
            -
            - - Assigned: - {assignedConfig.provider} - {assignedConfig.name} -
            -
            - Model: {assignedConfig.model_name} -
            - {assignedConfig.api_base && ( -
            - Base: {assignedConfig.api_base} -
            - )} -
            - )} -
            -
            -
            - ); - })} -
            - )} + const isAssignmentComplete = + assignments.long_context_llm_id && assignments.fast_llm_id && assignments.strategic_llm_id; + const assignedConfigIds = Object.values(assignments).filter((id) => id !== ""); + const availableConfigs = llmConfigs.filter( + (config) => config.id && config.id.toString().trim() !== "" + ); - {/* Action Buttons */} - {hasChanges && ( -
            - - -
            - )} + const isLoading = configsLoading || preferencesLoading; + const hasError = configsError || preferencesError; - {/* Status Indicator */} - {isAssignmentComplete && !hasChanges && ( -
            -
            - - All roles assigned and saved! -
            -
            - )} + return ( +
            + {/* Header */} +
            +
            +
            +
            + +
            +
            +

            LLM Role Management

            +

            + Assign your LLM configurations to specific roles for different purposes. +

            +
            +
            +
            +
            + + +
            +
            - {/* Progress Indicator */} -
            -
            - Progress: -
            - {Object.keys(ROLE_DESCRIPTIONS).map((key, index) => ( -
            - ))} -
            - - {assignedConfigIds.length} of {Object.keys(ROLE_DESCRIPTIONS).length} roles assigned - -
            -
            -
            - )} -
            - ); -} \ No newline at end of file + {/* Error Alert */} + {hasError && ( + + + {configsError || preferencesError} + + )} + + {/* Loading State */} + {isLoading && ( + + +
            + + + {configsLoading && preferencesLoading + ? "Loading configurations and preferences..." + : configsLoading + ? "Loading configurations..." + : "Loading preferences..."} + +
            +
            +
            + )} + + {/* Stats Overview */} + {!isLoading && !hasError && ( +
            + + +
            +
            +

            {availableConfigs.length}

            +

            Available Models

            +
            +
            + +
            +
            +
            +
            + + + +
            +
            +

            {assignedConfigIds.length}

            +

            Assigned Roles

            +
            +
            + +
            +
            +
            +
            + + + +
            +
            +

            + {Math.round((assignedConfigIds.length / 3) * 100)}% +

            +

            Completion

            +
            +
            + {isAssignmentComplete ? ( + + ) : ( + + )} +
            +
            +
            +
            + + + +
            +
            +

            + {isAssignmentComplete ? "Ready" : "Setup"} +

            +

            Status

            +
            +
            + {isAssignmentComplete ? ( + + ) : ( + + )} +
            +
            +
            +
            +
            + )} + + {/* Info Alert */} + {!isLoading && !hasError && ( +
            + {availableConfigs.length === 0 ? ( + + + + No LLM configurations found. Please add at least one LLM provider in the Model + Configs tab before assigning roles. + + + ) : !isAssignmentComplete ? ( + + + + Complete all role assignments to enable full functionality. Each role serves + different purposes in your workflow. + + + ) : ( + + + + All roles are assigned and ready to use! Your LLM configuration is complete. + + + )} + + {/* Role Assignment Cards */} + {availableConfigs.length > 0 && ( +
            + {Object.entries(ROLE_DESCRIPTIONS).map(([key, role]) => { + const IconComponent = role.icon; + const currentAssignment = assignments[`${key}_llm_id` as keyof typeof assignments]; + const assignedConfig = availableConfigs.find( + (config) => config.id === currentAssignment + ); + + return ( + + + +
            +
            +
            + +
            +
            + {role.title} + {role.description} +
            +
            + {currentAssignment && } +
            +
            + +
            +
            + Use cases: {role.examples} +
            +
            + {role.characteristics.map((char, idx) => ( + + {char} + + ))} +
            +
            + +
            + + +
            + + {assignedConfig && ( +
            +
            + + Assigned: + {assignedConfig.provider} + {assignedConfig.name} +
            +
            + Model: {assignedConfig.model_name} +
            + {assignedConfig.api_base && ( +
            + Base: {assignedConfig.api_base} +
            + )} +
            + )} +
            +
            +
            + ); + })} +
            + )} + + {/* Action Buttons */} + {hasChanges && ( +
            + + +
            + )} + + {/* Status Indicator */} + {isAssignmentComplete && !hasChanges && ( +
            +
            + + All roles assigned and saved! +
            +
            + )} + + {/* Progress Indicator */} +
            +
            + Progress: +
            + {Object.keys(ROLE_DESCRIPTIONS).map((key, index) => ( +
            + ))} +
            + + {assignedConfigIds.length} of {Object.keys(ROLE_DESCRIPTIONS).length} roles assigned + +
            +
            +
            + )} +
            + ); +} diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index 13b8e5ac1..08dcfd2ba 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -1,631 +1,669 @@ "use client"; -import React, { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Badge } from '@/components/ui/badge'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { - Plus, - Trash2, - Bot, - AlertCircle, - Edit3, - Settings2, - Eye, - EyeOff, - CheckCircle, - Clock, - AlertTriangle, - RefreshCw, - Loader2 -} from 'lucide-react'; -import { useLLMConfigs, CreateLLMConfig, UpdateLLMConfig, LLMConfig } from '@/hooks/use-llm-configs'; -import { toast } from 'sonner'; -import { Alert, AlertDescription } from '@/components/ui/alert'; +import type React from "react"; +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Plus, + Trash2, + Bot, + AlertCircle, + Edit3, + Settings2, + Eye, + EyeOff, + CheckCircle, + Clock, + AlertTriangle, + RefreshCw, + Loader2, +} from "lucide-react"; +import { + useLLMConfigs, + type CreateLLMConfig, + UpdateLLMConfig, + type LLMConfig, +} from "@/hooks/use-llm-configs"; +import { toast } from "sonner"; +import { Alert, AlertDescription } from "@/components/ui/alert"; const LLM_PROVIDERS = [ - { - value: 'OPENAI', - label: 'OpenAI', - example: 'gpt-4o, gpt-4, gpt-3.5-turbo', - description: 'Most popular and versatile AI models' - }, - { - value: 'ANTHROPIC', - label: 'Anthropic', - example: 'claude-3-5-sonnet-20241022, claude-3-opus-20240229', - description: 'Constitutional AI with strong reasoning' - }, - { - value: 'GROQ', - label: 'Groq', - example: 'llama3-70b-8192, mixtral-8x7b-32768', - description: 'Ultra-fast inference speeds' - }, - { - value: 'COHERE', - label: 'Cohere', - example: 'command-r-plus, command-r', - description: 'Enterprise-focused language models' - }, - { - value: 'HUGGINGFACE', - label: 'HuggingFace', - example: 'microsoft/DialoGPT-medium', - description: 'Open source model hub' - }, - { - value: 'AZURE_OPENAI', - label: 'Azure OpenAI', - example: 'gpt-4, gpt-35-turbo', - description: 'Enterprise OpenAI through Azure' - }, - { - value: 'GOOGLE', - label: 'Google', - example: 'gemini-pro, gemini-pro-vision', - description: 'Google\'s Gemini AI models' - }, - { - value: 'AWS_BEDROCK', - label: 'AWS Bedrock', - example: 'anthropic.claude-v2', - description: 'AWS managed AI service' - }, - { - value: 'OLLAMA', - label: 'Ollama', - example: 'llama2, codellama', - description: 'Run models locally' - }, - { - value: 'MISTRAL', - label: 'Mistral', - example: 'mistral-large-latest, mistral-medium', - description: 'European AI excellence' - }, - { - value: 'TOGETHER_AI', - label: 'Together AI', - example: 'togethercomputer/llama-2-70b-chat', - description: 'Decentralized AI platform' - }, - { - value: 'REPLICATE', - label: 'Replicate', - example: 'meta/llama-2-70b-chat', - description: 'Run models via API' - }, - { - value: 'CUSTOM', - label: 'Custom Provider', - example: 'your-custom-model', - description: 'Your own model endpoint' - }, + { + value: "OPENAI", + label: "OpenAI", + example: "gpt-4o, gpt-4, gpt-3.5-turbo", + description: "Most popular and versatile AI models", + }, + { + value: "ANTHROPIC", + label: "Anthropic", + example: "claude-3-5-sonnet-20241022, claude-3-opus-20240229", + description: "Constitutional AI with strong reasoning", + }, + { + value: "GROQ", + label: "Groq", + example: "llama3-70b-8192, mixtral-8x7b-32768", + description: "Ultra-fast inference speeds", + }, + { + value: "COHERE", + label: "Cohere", + example: "command-r-plus, command-r", + description: "Enterprise-focused language models", + }, + { + value: "HUGGINGFACE", + label: "HuggingFace", + example: "microsoft/DialoGPT-medium", + description: "Open source model hub", + }, + { + value: "AZURE_OPENAI", + label: "Azure OpenAI", + example: "gpt-4, gpt-35-turbo", + description: "Enterprise OpenAI through Azure", + }, + { + value: "GOOGLE", + label: "Google", + example: "gemini-pro, gemini-pro-vision", + description: "Google's Gemini AI models", + }, + { + value: "AWS_BEDROCK", + label: "AWS Bedrock", + example: "anthropic.claude-v2", + description: "AWS managed AI service", + }, + { + value: "OLLAMA", + label: "Ollama", + example: "llama2, codellama", + description: "Run models locally", + }, + { + value: "MISTRAL", + label: "Mistral", + example: "mistral-large-latest, mistral-medium", + description: "European AI excellence", + }, + { + value: "TOGETHER_AI", + label: "Together AI", + example: "togethercomputer/llama-2-70b-chat", + description: "Decentralized AI platform", + }, + { + value: "REPLICATE", + label: "Replicate", + example: "meta/llama-2-70b-chat", + description: "Run models via API", + }, + { + value: "CUSTOM", + label: "Custom Provider", + example: "your-custom-model", + description: "Your own model endpoint", + }, ]; export function ModelConfigManager() { - const { llmConfigs, loading, error, createLLMConfig, updateLLMConfig, deleteLLMConfig, refreshConfigs } = useLLMConfigs(); - const [isAddingNew, setIsAddingNew] = useState(false); - const [editingConfig, setEditingConfig] = useState(null); - const [showApiKey, setShowApiKey] = useState>({}); - const [formData, setFormData] = useState({ - name: '', - provider: '', - custom_provider: '', - model_name: '', - api_key: '', - api_base: '', - litellm_params: {} - }); - const [isSubmitting, setIsSubmitting] = useState(false); + const { + llmConfigs, + loading, + error, + createLLMConfig, + updateLLMConfig, + deleteLLMConfig, + refreshConfigs, + } = useLLMConfigs(); + const [isAddingNew, setIsAddingNew] = useState(false); + const [editingConfig, setEditingConfig] = useState(null); + const [showApiKey, setShowApiKey] = useState>({}); + const [formData, setFormData] = useState({ + name: "", + provider: "", + custom_provider: "", + model_name: "", + api_key: "", + api_base: "", + litellm_params: {}, + }); + const [isSubmitting, setIsSubmitting] = useState(false); - // Populate form when editing - useEffect(() => { - if (editingConfig) { - setFormData({ - name: editingConfig.name, - provider: editingConfig.provider, - custom_provider: editingConfig.custom_provider || '', - model_name: editingConfig.model_name, - api_key: editingConfig.api_key, - api_base: editingConfig.api_base || '', - litellm_params: editingConfig.litellm_params || {} - }); - } - }, [editingConfig]); + // Populate form when editing + useEffect(() => { + if (editingConfig) { + setFormData({ + name: editingConfig.name, + provider: editingConfig.provider, + custom_provider: editingConfig.custom_provider || "", + model_name: editingConfig.model_name, + api_key: editingConfig.api_key, + api_base: editingConfig.api_base || "", + litellm_params: editingConfig.litellm_params || {}, + }); + } + }, [editingConfig]); - const handleInputChange = (field: keyof CreateLLMConfig, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })); - }; + const handleInputChange = (field: keyof CreateLLMConfig, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) { - toast.error('Please fill in all required fields'); - return; - } + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!formData.name || !formData.provider || !formData.model_name || !formData.api_key) { + toast.error("Please fill in all required fields"); + return; + } - setIsSubmitting(true); - - let result; - if (editingConfig) { - // Update existing config - result = await updateLLMConfig(editingConfig.id, formData); - } else { - // Create new config - result = await createLLMConfig(formData); - } - - setIsSubmitting(false); + setIsSubmitting(true); - if (result) { - setFormData({ - name: '', - provider: '', - custom_provider: '', - model_name: '', - api_key: '', - api_base: '', - litellm_params: {} - }); - setIsAddingNew(false); - setEditingConfig(null); - } - }; + let result; + if (editingConfig) { + // Update existing config + result = await updateLLMConfig(editingConfig.id, formData); + } else { + // Create new config + result = await createLLMConfig(formData); + } - const handleDelete = async (id: number) => { - if (confirm('Are you sure you want to delete this configuration? This action cannot be undone.')) { - await deleteLLMConfig(id); - } - }; + setIsSubmitting(false); - const toggleApiKeyVisibility = (configId: number) => { - setShowApiKey(prev => ({ - ...prev, - [configId]: !prev[configId] - })); - }; + if (result) { + setFormData({ + name: "", + provider: "", + custom_provider: "", + model_name: "", + api_key: "", + api_base: "", + litellm_params: {}, + }); + setIsAddingNew(false); + setEditingConfig(null); + } + }; - const selectedProvider = LLM_PROVIDERS.find(p => p.value === formData.provider); + const handleDelete = async (id: number) => { + if ( + confirm("Are you sure you want to delete this configuration? This action cannot be undone.") + ) { + await deleteLLMConfig(id); + } + }; - const getProviderInfo = (providerValue: string) => { - return LLM_PROVIDERS.find(p => p.value === providerValue); - }; + const toggleApiKeyVisibility = (configId: number) => { + setShowApiKey((prev) => ({ + ...prev, + [configId]: !prev[configId], + })); + }; - const maskApiKey = (apiKey: string) => { - if (apiKey.length <= 8) return '*'.repeat(apiKey.length); - return apiKey.substring(0, 4) + '*'.repeat(apiKey.length - 8) + apiKey.substring(apiKey.length - 4); - }; + const selectedProvider = LLM_PROVIDERS.find((p) => p.value === formData.provider); - return ( -
            - {/* Header */} -
            -
            -
            -
            - -
            -
            -

            Model Configurations

            -

            - Manage your LLM provider configurations and API settings. -

            -
            -
            -
            -
            - -
            -
            + const getProviderInfo = (providerValue: string) => { + return LLM_PROVIDERS.find((p) => p.value === providerValue); + }; - {/* Error Alert */} - {error && ( - - - - {error} - - - )} + const maskApiKey = (apiKey: string) => { + if (apiKey.length <= 8) return "*".repeat(apiKey.length); + return ( + apiKey.substring(0, 4) + "*".repeat(apiKey.length - 8) + apiKey.substring(apiKey.length - 4) + ); + }; - {/* Loading State */} - {loading && ( - - -
            - - Loading configurations... -
            -
            -
            - )} + return ( +
            + {/* Header */} +
            +
            +
            +
            + +
            +
            +

            Model Configurations

            +

            + Manage your LLM provider configurations and API settings. +

            +
            +
            +
            +
            + +
            +
            - {/* Stats Overview */} - {!loading && !error && ( -
            - - -
            -
            -

            {llmConfigs.length}

            -

            Total Configurations

            -
            -
            - -
            -
            -
            -
            - - - -
            -
            -

            - {new Set(llmConfigs.map(c => c.provider)).size} -

            -

            Unique Providers

            -
            -
            - -
            -
            -
            -
            + {/* Error Alert */} + {error && ( + + + {error} + + )} - - -
            -
            -

            Active

            -

            System Status

            -
            -
            - -
            -
            -
            -
            -
            - )} + {/* Loading State */} + {loading && ( + + +
            + + Loading configurations... +
            +
            +
            + )} - {/* Configuration Management */} - {!loading && !error && ( -
            -
            -
            -

            Your Configurations

            -

            - Manage and configure your LLM providers -

            -
            - -
            + {/* Stats Overview */} + {!loading && !error && ( +
            + + +
            +
            +

            {llmConfigs.length}

            +

            Total Configurations

            +
            +
            + +
            +
            +
            +
            - {llmConfigs.length === 0 ? ( - - -
            - -
            -
            -

            No Configurations Yet

            -

            - Get started by adding your first LLM provider configuration to begin using the system. -

            -
            - -
            -
            - ) : ( -
            - - {llmConfigs.map((config) => { - const providerInfo = getProviderInfo(config.provider); - return ( - - - -
            -
            - {/* Header */} -
            -
            - -
            -
            -
            -

            {config.name}

            - - {config.provider} - -
            -

            - {config.model_name} -

            -
            -
            + + +
            +
            +

            + {new Set(llmConfigs.map((c) => c.provider)).size} +

            +

            Unique Providers

            +
            +
            + +
            +
            +
            +
            - {/* Provider Description */} - {providerInfo && ( -

            - {providerInfo.description} -

            - )} + + +
            +
            +

            Active

            +

            System Status

            +
            +
            + +
            +
            +
            +
            +
            + )} - {/* Configuration Details */} -
            -
            - -
            - - {showApiKey[config.id] - ? config.api_key - : maskApiKey(config.api_key) - } - - -
            -
            + {/* Configuration Management */} + {!loading && !error && ( +
            +
            +
            +

            Your Configurations

            +

            + Manage and configure your LLM providers +

            +
            + +
            - {config.api_base && ( -
            - - - {config.api_base} - -
            - )} -
            + {llmConfigs.length === 0 ? ( + + +
            + +
            +
            +

            No Configurations Yet

            +

            + Get started by adding your first LLM provider configuration to begin using the + system. +

            +
            + +
            +
            + ) : ( +
            + + {llmConfigs.map((config) => { + const providerInfo = getProviderInfo(config.provider); + return ( + + + +
            +
            + {/* Header */} +
            +
            + +
            +
            +
            +

            + {config.name} +

            + + {config.provider} + +
            +

            + {config.model_name} +

            +
            +
            - {/* Metadata */} -
            -
            - - Created {new Date(config.created_at).toLocaleDateString()} -
            -
            -
            - Active -
            -
            -
            + {/* Provider Description */} + {providerInfo && ( +

            + {providerInfo.description} +

            + )} - {/* Actions */} -
            - - -
            -
            -
            -
            -
            - ); - })} -
            -
            - )} -
            - )} + {/* Configuration Details */} +
            +
            + +
            + + {showApiKey[config.id] + ? config.api_key + : maskApiKey(config.api_key)} + + +
            +
            - {/* Add/Edit Configuration Dialog */} - { - if (!open) { - setIsAddingNew(false); - setEditingConfig(null); - setFormData({ - name: '', - provider: '', - custom_provider: '', - model_name: '', - api_key: '', - api_base: '', - litellm_params: {} - }); - } - }}> - - - - {editingConfig ? : } - {editingConfig ? 'Edit LLM Configuration' : 'Add New LLM Configuration'} - - - {editingConfig - ? 'Update your language model provider configuration' - : 'Configure a new language model provider for your AI assistant' - } - - + {config.api_base && ( +
            + + + {config.api_base} + +
            + )} +
            -
            -
            -
            - - handleInputChange('name', e.target.value)} - required - /> -
            + {/* Metadata */} +
            +
            + + + Created {new Date(config.created_at).toLocaleDateString()} + +
            +
            +
            + Active +
            +
            +
            -
            - - -
            -
            + {/* Actions */} +
            + + +
            +
            + + + + ); + })} + +
            + )} +
            + )} - {formData.provider === 'CUSTOM' && ( -
            - - handleInputChange('custom_provider', e.target.value)} - required - /> -
            - )} + {/* Add/Edit Configuration Dialog */} + { + if (!open) { + setIsAddingNew(false); + setEditingConfig(null); + setFormData({ + name: "", + provider: "", + custom_provider: "", + model_name: "", + api_key: "", + api_base: "", + litellm_params: {}, + }); + } + }} + > + + + + {editingConfig ? : } + {editingConfig ? "Edit LLM Configuration" : "Add New LLM Configuration"} + + + {editingConfig + ? "Update your language model provider configuration" + : "Configure a new language model provider for your AI assistant"} + + -
            - - handleInputChange('model_name', e.target.value)} - required - /> - {selectedProvider && ( -

            - Examples: {selectedProvider.example} -

            - )} -
            + +
            +
            + + handleInputChange("name", e.target.value)} + required + /> +
            -
            - - handleInputChange('api_key', e.target.value)} - required - /> -
            +
            + + +
            +
            -
            - - handleInputChange('api_base', e.target.value)} - /> -
            + {formData.provider === "CUSTOM" && ( +
            + + handleInputChange("custom_provider", e.target.value)} + required + /> +
            + )} -
            - - -
            - -
            -
            -
            - ); -} \ No newline at end of file +
            + + handleInputChange("model_name", e.target.value)} + required + /> + {selectedProvider && ( +

            + Examples: {selectedProvider.example} +

            + )} +
            + +
            + + handleInputChange("api_key", e.target.value)} + required + /> +
            + +
            + + handleInputChange("api_base", e.target.value)} + /> +
            + +
            + + +
            + + +
    +
    + ); +} diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 0039d3bb7..e3c71df3e 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -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(null); - const [isLoadingChats, setIsLoadingChats] = useState(true); - const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true); - const [chatError, setChatError] = useState(null); - const [searchSpaceError, setSearchSpaceError] = useState(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(null); + const [isLoadingChats, setIsLoadingChats] = useState(true); + const [isLoadingSearchSpace, setIsLoadingSearchSpace] = useState(true); + const [chatError, setChatError] = useState(null); + const [searchSpaceError, setSearchSpaceError] = useState(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(`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( + `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( + `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(`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 ( + <> + - return ( - <> - - - {/* Delete Confirmation Dialog - Only render on client */} - {isClient && ( - - - - - - Delete Chat - - - Are you sure you want to delete {chatToDelete?.name}? This action cannot be undone. - - - - - - - - - )} - - ); -} \ No newline at end of file + {/* Delete Confirmation Dialog - Only render on client */} + {isClient && ( + + + + + + Delete Chat + + + Are you sure you want to delete{" "} + {chatToDelete?.name}? This action cannot be + undone. + + + + + + + + + )} + + ); +} diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 6b9cd9eb3..1b9fe5cfa 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -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 = { - 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 { - 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 ( - - - - - -
    -
    - -
    -
    - SurfSense - beta v0.0.7 -
    -
    -
    -
    -
    -
    - - - {processedRecentChats.length > 0 && } - - - {/* + return ( + + + + + +
    +
    + +
    +
    + SurfSense + beta v0.0.7 +
    +
    +
    +
    +
    +
    + + + {processedRecentChats.length > 0 && } + + + {/* footer */} -
    - ) +
    + ); } diff --git a/surfsense_web/components/sidebar/nav-main.tsx b/surfsense_web/components/sidebar/nav-main.tsx index 43b31e319..41859b628 100644 --- a/surfsense_web/components/sidebar/nav-main.tsx +++ b/surfsense_web/components/sidebar/nav-main.tsx @@ -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 ( - - Platform - - {items.map((item, index) => ( - - - - - - {item.title} - - - {item.items?.length ? ( - <> - - - - Toggle - - - - - {item.items?.map((subItem, subIndex) => ( - - - - {subItem.title} - - - - ))} - - - - ) : null} - - - ))} - - - ) + return ( + + Platform + + {items.map((item, index) => ( + + + + + + {item.title} + + + {item.items?.length ? ( + <> + + + + Toggle + + + + + {item.items?.map((subItem, subIndex) => ( + + + + {subItem.title} + + + + ))} + + + + ) : null} + + + ))} + + + ); } diff --git a/surfsense_web/components/sidebar/nav-projects.tsx b/surfsense_web/components/sidebar/nav-projects.tsx index 756ca8026..3594f4515 100644 --- a/surfsense_web/components/sidebar/nav-projects.tsx +++ b/surfsense_web/components/sidebar/nav-projects.tsx @@ -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 = { - 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 ( - - Recent Chats - - {chats.map((item, index) => ( - - - - {item.name} - - - - - - More - - - - {item.actions ? ( - // Use the actions provided by the item - item.actions.map((action, actionIndex) => { - const ActionIcon = actionIconMap[action.icon] || Folder; - return ( - - - {action.name} - - ); - }) - ) : ( - // Default actions if none provided - <> - - - View Chat - - - - - Delete Chat - - - )} - - - - ))} - - router.push(`/dashboard/${searchSpaceId}/chats`)}> - - View All Chats - - - - - ) + const searchSpaceId = chats[0]?.search_space_id || ""; + + return ( + + Recent Chats + + {chats.map((item, index) => ( + + + + {item.name} + + + + + + More + + + + {item.actions ? ( + // Use the actions provided by the item + item.actions.map((action, actionIndex) => { + const ActionIcon = actionIconMap[action.icon] || Folder; + return ( + + + {action.name} + + ); + }) + ) : ( + // Default actions if none provided + <> + + + View Chat + + + + + Delete Chat + + + )} + + + + ))} + + router.push(`/dashboard/${searchSpaceId}/chats`)}> + + View All Chats + + + + + ); } diff --git a/surfsense_web/components/sidebar/nav-secondary.tsx b/surfsense_web/components/sidebar/nav-secondary.tsx index 5425583ab..2f101dbb2 100644 --- a/surfsense_web/components/sidebar/nav-secondary.tsx +++ b/surfsense_web/components/sidebar/nav-secondary.tsx @@ -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) { - return ( - - SearchSpace - - {items.map((item, index) => ( - - - - - {item.title} - - - - ))} - - - ) + return ( + + SearchSpace + + {items.map((item, index) => ( + + + + + {item.title} + + + + ))} + + + ); } diff --git a/surfsense_web/components/sidebar/nav-user.tsx b/surfsense_web/components/sidebar/nav-user.tsx index fe1653034..cfa6c41f7 100644 --- a/surfsense_web/components/sidebar/nav-user.tsx +++ b/surfsense_web/components/sidebar/nav-user.tsx @@ -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 ( - - - - - - - - CN - -
    - {user.name} - {user.email} -
    - -
    -
    - - -
    - - - CN - -
    - {user.name} - {user.email} -
    -
    -
    - - - router.push(`/dashboard/${search_space_id}/api-key`)}> - - API Key - - - - router.push(`/settings`)}> - - Settings - - - - Log out - -
    -
    -
    -
    - ) + const handleLogout = () => { + if (typeof window !== "undefined") { + localStorage.removeItem("surfsense_bearer_token"); + router.push("/"); + } + }; + return ( + + + + + + + + CN + +
    + {user.name} + {user.email} +
    + +
    +
    + + +
    + + + CN + +
    + {user.name} + {user.email} +
    +
    +
    + + + router.push(`/dashboard/${search_space_id}/api-key`)} + > + + API Key + + + + router.push(`/settings`)}> + + Settings + + + + Log out + +
    +
    +
    +
    + ); } diff --git a/surfsense_web/components/theme/theme-provider.tsx b/surfsense_web/components/theme/theme-provider.tsx index 7ff65a887..bae99955c 100644 --- a/surfsense_web/components/theme/theme-provider.tsx +++ b/surfsense_web/components/theme/theme-provider.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import { ThemeProvider as NextThemesProvider } from "next-themes" -import type { ThemeProviderProps } from "next-themes" +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import type { ThemeProviderProps } from "next-themes"; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} + return {children}; } diff --git a/surfsense_web/components/theme/theme-toggle.tsx b/surfsense_web/components/theme/theme-toggle.tsx index c95527dc5..361a0ab7d 100644 --- a/surfsense_web/components/theme/theme-toggle.tsx +++ b/surfsense_web/components/theme/theme-toggle.tsx @@ -6,64 +6,64 @@ import { MoonIcon, SunIcon } from "lucide-react"; import { motion } from "framer-motion"; export function ThemeTogglerComponent() { - const { theme, setTheme } = useTheme(); + const { theme, setTheme } = useTheme(); - const [isClient, setIsClient] = React.useState(false); + const [isClient, setIsClient] = React.useState(false); - React.useEffect(() => { - setIsClient(true); - }, []); + React.useEffect(() => { + setIsClient(true); + }, []); - return ( - isClient && ( - - ) - ); + Toggle theme + + ) + ); } diff --git a/surfsense_web/components/ui/accordion.tsx b/surfsense_web/components/ui/accordion.tsx index 4a8cca46b..8dfa1438e 100644 --- a/surfsense_web/components/ui/accordion.tsx +++ b/surfsense_web/components/ui/accordion.tsx @@ -1,66 +1,64 @@ -"use client" +"use client"; -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDownIcon } from "lucide-react" +import type * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -function Accordion({ - ...props -}: React.ComponentProps) { - return +function Accordion({ ...props }: React.ComponentProps) { + return ; } function AccordionItem({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AccordionTrigger({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ) + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ); } function AccordionContent({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - -
    {children}
    -
    - ) + return ( + +
    {children}
    +
    + ); } -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/surfsense_web/components/ui/alert-dialog.tsx b/surfsense_web/components/ui/alert-dialog.tsx index 27a1b7781..613f14dcb 100644 --- a/surfsense_web/components/ui/alert-dialog.tsx +++ b/surfsense_web/components/ui/alert-dialog.tsx @@ -1,157 +1,135 @@ -"use client" +"use client"; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import type * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; -function AlertDialog({ - ...props -}: React.ComponentProps) { - return +function AlertDialog({ ...props }: React.ComponentProps) { + return ; } function AlertDialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ; } -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return ; } function AlertDialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogContent({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - - ) + return ( + + + + + ); } -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
    - ) +function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); } -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
    - ) +function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
    + ); } function AlertDialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogAction({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ; } function AlertDialogCancel({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/surfsense_web/components/ui/alert.tsx b/surfsense_web/components/ui/alert.tsx index 26a60d3ba..1e58eda59 100644 --- a/surfsense_web/components/ui/alert.tsx +++ b/surfsense_web/components/ui/alert.tsx @@ -1,58 +1,48 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "@/lib/utils" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps + HTMLDivElement, + React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => ( -
    -)) -Alert.displayName = "Alert" +
    +)); +Alert.displayName = "Alert"; -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
    -)) -AlertTitle.displayName = "AlertTitle" +const AlertTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
    + ) +); +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes + HTMLParagraphElement, + React.HTMLAttributes >(({ className, ...props }, ref) => ( -
    -)) -AlertDescription.displayName = "AlertDescription" +
    +)); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } \ No newline at end of file +export { Alert, AlertTitle, AlertDescription }; diff --git a/surfsense_web/components/ui/avatar.tsx b/surfsense_web/components/ui/avatar.tsx index 71e428b4c..a57ac8c39 100644 --- a/surfsense_web/components/ui/avatar.tsx +++ b/surfsense_web/components/ui/avatar.tsx @@ -1,53 +1,41 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import type * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -function Avatar({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) +function Avatar({ className, ...props }: React.ComponentProps) { + return ( + + ); } -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) +function AvatarImage({ className, ...props }: React.ComponentProps) { + return ( + + ); } function AvatarFallback({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/surfsense_web/components/ui/badge.tsx b/surfsense_web/components/ui/badge.tsx index 37e088abb..fe0c5950c 100644 --- a/surfsense_web/components/ui/badge.tsx +++ b/surfsense_web/components/ui/badge.tsx @@ -1,46 +1,39 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import type * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40", + outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; - return ( - - ) + return ( + + ); } -export { Badge, badgeVariants } +export { Badge, badgeVariants }; diff --git a/surfsense_web/components/ui/breadcrumb.tsx b/surfsense_web/components/ui/breadcrumb.tsx index eb88f3212..a3f579126 100644 --- a/surfsense_web/components/ui/breadcrumb.tsx +++ b/surfsense_web/components/ui/breadcrumb.tsx @@ -1,109 +1,102 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import type * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return