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 40f68938b..028c0efdc 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx @@ -9,7 +9,7 @@ import type React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { activeChathatUIAtom, activeChatIdAtom } from "@/atoms/chats/ui.atoms"; import { llmPreferencesAtom } from "@/atoms/llm-config/llm-config-query.atoms"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { ChatPanelContainer } from "@/components/chat/ChatPanel/ChatPanelContainer"; import { DashboardBreadcrumb } from "@/components/dashboard-breadcrumb"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx index 21dbce82b..b1525a9db 100644 --- a/surfsense_web/app/dashboard/page.tsx +++ b/surfsense_web/app/dashboard/page.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAtomValue } from "jotai"; import { AlertCircle, Loader2, Plus, Search, Trash2, UserCheck, Users } from "lucide-react"; import { motion, type Variants } from "motion/react"; import Image from "next/image"; @@ -35,7 +36,8 @@ import { import { Spotlight } from "@/components/ui/spotlight"; import { Tilt } from "@/components/ui/tilt"; import { useUser } from "@/hooks"; -import { useSearchSpaces } from "@/hooks/use-search-spaces"; +import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; import { authenticatedFetch } from "@/lib/auth-utils"; /** @@ -154,7 +156,8 @@ const DashboardPage = () => { }, }; - const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces(); + const { data: searchSpaces = [], isLoading: loading, error, refetch: refreshSearchSpaces } = useAtomValue(searchSpacesAtom); + const { mutateAsync: deleteSearchSpace } = useAtomValue(deleteSearchSpaceMutationAtom); // Fetch user details const { user, loading: isLoadingUser, error: userError } = useUser(); @@ -169,29 +172,11 @@ const DashboardPage = () => { }; if (loading) return ; - if (error) return ; + if (error) return ; const handleDeleteSearchSpace = async (id: number) => { - // Send DELETE request to the API - try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${id}`, - { method: "DELETE" } - ); - - 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"); + await deleteSearchSpace({ id }); + refreshSearchSpaces(); }; return ( diff --git a/surfsense_web/app/dashboard/searchspaces/page.tsx b/surfsense_web/app/dashboard/searchspaces/page.tsx index 520c4358e..76e17c0ce 100644 --- a/surfsense_web/app/dashboard/searchspaces/page.tsx +++ b/surfsense_web/app/dashboard/searchspaces/page.tsx @@ -1,46 +1,25 @@ "use client"; +import { useAtomValue } from "jotai"; import { motion } from "motion/react"; import { useRouter } from "next/navigation"; -import { toast } from "sonner"; import { SearchSpaceForm } from "@/components/search-space-form"; -import { authenticatedFetch } from "@/lib/auth-utils"; +import { createSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms"; export default function SearchSpacesPage() { const router = useRouter(); + const { mutateAsync: createSearchSpace } = useAtomValue(createSearchSpaceMutationAtom); + const handleCreateSearchSpace = async (data: { name: string; description?: string }) => { - try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name: data.name, - description: data.description || "", - }), - } - ); + const result = await createSearchSpace({ + name: data.name, + description: data.description || "", + }); - if (!response.ok) { - toast.error("Failed to create search space"); - throw new Error("Failed to create search space"); - } + // Redirect to the newly created search space's onboarding + router.push(`/dashboard/${result.id}/onboard`); - const result = await response.json(); - - toast.success("Search space created successfully", { - description: `"${data.name}" has been created.`, - }); - - // Redirect to the newly created search space's onboarding - router.push(`/dashboard/${result.id}/onboard`); - - return result; - } catch (error) { - console.error("Error creating search space:", error); - throw error; - } + return result; }; return ( diff --git a/surfsense_web/atoms/chats/chat-mutation.atoms.ts b/surfsense_web/atoms/chats/chat-mutation.atoms.ts index 6105ef615..a08dcd21f 100644 --- a/surfsense_web/atoms/chats/chat-mutation.atoms.ts +++ b/surfsense_web/atoms/chats/chat-mutation.atoms.ts @@ -10,7 +10,7 @@ import { chatsApiService } from "@/lib/apis/chats-api.service"; import { getBearerToken } from "@/lib/auth-utils"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { queryClient } from "@/lib/query-client/client"; -import { activeSearchSpaceIdAtom } from "../seach-spaces/seach-space-queries.atom"; +import { activeSearchSpaceIdAtom } from "../search-spaces/search-space-query.atoms"; import { globalChatsQueryParamsAtom } from "./ui.atoms"; export const deleteChatMutationAtom = atomWithMutation((get) => { diff --git a/surfsense_web/atoms/chats/chat-query.atoms.ts b/surfsense_web/atoms/chats/chat-query.atoms.ts index 36871dbd0..5a1242ded 100644 --- a/surfsense_web/atoms/chats/chat-query.atoms.ts +++ b/surfsense_web/atoms/chats/chat-query.atoms.ts @@ -1,5 +1,5 @@ import { atomWithQuery } from "jotai-tanstack-query"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { chatsApiService } from "@/lib/apis/chats-api.service"; import { podcastsApiService } from "@/lib/apis/podcasts-api.service"; import { getBearerToken } from "@/lib/auth-utils"; diff --git a/surfsense_web/atoms/documents/document-mutation.atoms.ts b/surfsense_web/atoms/documents/document-mutation.atoms.ts index 6eaaa014a..0eae1081d 100644 --- a/surfsense_web/atoms/documents/document-mutation.atoms.ts +++ b/surfsense_web/atoms/documents/document-mutation.atoms.ts @@ -1,6 +1,6 @@ import { atomWithMutation } from "jotai-tanstack-query"; import { toast } from "sonner"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import type { CreateDocumentRequest, DeleteDocumentRequest, diff --git a/surfsense_web/atoms/documents/document-query.atoms.ts b/surfsense_web/atoms/documents/document-query.atoms.ts index acdff9aaf..656706a62 100644 --- a/surfsense_web/atoms/documents/document-query.atoms.ts +++ b/surfsense_web/atoms/documents/document-query.atoms.ts @@ -1,5 +1,5 @@ import { atomWithQuery } from "jotai-tanstack-query"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import type { SearchDocumentsRequest } from "@/contracts/types/document.types"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; diff --git a/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts b/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts index 8d0bedc82..f28b1d708 100644 --- a/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts +++ b/surfsense_web/atoms/llm-config/llm-config-mutation.atoms.ts @@ -1,6 +1,5 @@ import { atomWithMutation } from "jotai-tanstack-query"; import { toast } from "sonner"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; import type { CreateLLMConfigRequest, DeleteLLMConfigRequest, @@ -12,6 +11,7 @@ import type { import { llmConfigApiService } from "@/lib/apis/llm-config-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; import { queryClient } from "@/lib/query-client/client"; +import { activeSearchSpaceIdAtom } from "../search-spaces/search-space-query.atoms"; export const createLLMConfigMutationAtom = atomWithMutation((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); diff --git a/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts b/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts index 66c11b61a..22ae63d7f 100644 --- a/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts +++ b/surfsense_web/atoms/llm-config/llm-config-query.atoms.ts @@ -1,7 +1,7 @@ import { atomWithQuery } from "jotai-tanstack-query"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; import { llmConfigApiService } from "@/lib/apis/llm-config-api.service"; import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { activeSearchSpaceIdAtom } from "../search-spaces/search-space-query.atoms"; export const llmConfigsAtom = atomWithQuery((get) => { const searchSpaceId = get(activeSearchSpaceIdAtom); diff --git a/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts b/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts index 7e51891be..cdb28ceb2 100644 --- a/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts +++ b/surfsense_web/atoms/podcasts/podcast-mutation.atoms.ts @@ -1,6 +1,6 @@ import { atomWithMutation } from "jotai-tanstack-query"; import { toast } from "sonner"; -import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-queries.atom"; +import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import type { DeletePodcastRequest, GeneratePodcastRequest, diff --git a/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts b/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts deleted file mode 100644 index 4bccf496f..000000000 --- a/surfsense_web/atoms/seach-spaces/seach-space-queries.atom.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { atom } from "jotai"; - -export const activeSearchSpaceIdAtom = atom(null); diff --git a/surfsense_web/atoms/search-spaces/search-space-mutation.atoms.ts b/surfsense_web/atoms/search-spaces/search-space-mutation.atoms.ts new file mode 100644 index 000000000..ea1415869 --- /dev/null +++ b/surfsense_web/atoms/search-spaces/search-space-mutation.atoms.ts @@ -0,0 +1,75 @@ +import { atomWithMutation } from "jotai-tanstack-query"; +import { toast } from "sonner"; +import type { + CreateSearchSpaceRequest, + UpdateSearchSpaceRequest, + DeleteSearchSpaceRequest, +} from "@/contracts/types/search-space.types"; +import { activeSearchSpaceIdAtom } from "./search-space-query.atoms"; +import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { queryClient } from "@/lib/query-client/client"; + +export const createSearchSpaceMutationAtom = atomWithMutation(() => { + return { + mutationKey: ["create-search-space"], + mutationFn: async (request: CreateSearchSpaceRequest) => { + return searchSpacesApiService.createSearchSpace(request); + }, + + onSuccess: () => { + toast.success("Search space created successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.searchSpaces.all, + }); + }, + }; +}); + +export const updateSearchSpaceMutationAtom = atomWithMutation((get) => { + const activeSearchSpaceId = get(activeSearchSpaceIdAtom); + + return { + mutationKey: ["update-search-space", activeSearchSpaceId], + enabled: !!activeSearchSpaceId, + mutationFn: async (request: UpdateSearchSpaceRequest) => { + return searchSpacesApiService.updateSearchSpace(request); + }, + + onSuccess: (_, request: UpdateSearchSpaceRequest) => { + toast.success("Search space updated successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.searchSpaces.all, + }); + if (request.id) { + queryClient.invalidateQueries({ + queryKey: cacheKeys.searchSpaces.detail(String(request.id)), + }); + } + }, + }; +}); + +export const deleteSearchSpaceMutationAtom = atomWithMutation((get) => { + const activeSearchSpaceId = get(activeSearchSpaceIdAtom); + + return { + mutationKey: ["delete-search-space", activeSearchSpaceId], + enabled: !!activeSearchSpaceId, + mutationFn: async (request: DeleteSearchSpaceRequest) => { + return searchSpacesApiService.deleteSearchSpace(request); + }, + + onSuccess: (_, request: DeleteSearchSpaceRequest) => { + toast.success("Search space deleted successfully"); + queryClient.invalidateQueries({ + queryKey: cacheKeys.searchSpaces.all, + }); + if (request.id) { + queryClient.removeQueries({ + queryKey: cacheKeys.searchSpaces.detail(String(request.id)), + }); + } + }, + }; +}); diff --git a/surfsense_web/atoms/search-spaces/search-space-query.atoms.ts b/surfsense_web/atoms/search-spaces/search-space-query.atoms.ts new file mode 100644 index 000000000..1f03e25a2 --- /dev/null +++ b/surfsense_web/atoms/search-spaces/search-space-query.atoms.ts @@ -0,0 +1,37 @@ +import { atomWithQuery } from "jotai-tanstack-query"; +import { atom } from "jotai"; +import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types"; +import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; + +export const activeSearchSpaceIdAtom = atom(null); + +export const searchSpacesQueryParamsAtom = atom({ + skip: 0, + limit: 10, + owned_only: false, +}); + +export const searchSpacesAtom = atomWithQuery((get) => { + const queryParams = get(searchSpacesQueryParamsAtom); + + return { + queryKey: cacheKeys.searchSpaces.withQueryParams(queryParams), + staleTime: 5 * 60 * 1000, + queryFn: async () => { + return searchSpacesApiService.getSearchSpaces({ + queryParams, + }); + }, + }; +}); + +export const communityPromptsAtom = atomWithQuery(() => { + return { + queryKey: cacheKeys.searchSpaces.communityPrompts, + staleTime: 30 * 60 * 1000, + queryFn: async () => { + return searchSpacesApiService.getCommunityPrompts(); + }, + }; +}); diff --git a/surfsense_web/components/chat/SourceDetailSheet.tsx b/surfsense_web/components/chat/SourceDetailSheet.tsx index 70b73fcf2..7f5ebbeab 100644 --- a/surfsense_web/components/chat/SourceDetailSheet.tsx +++ b/surfsense_web/components/chat/SourceDetailSheet.tsx @@ -52,7 +52,6 @@ export function SourceDetailSheet({ const highlightedChunkRef = useRef(null); const [summaryOpen, setSummaryOpen] = useState(false); - // Add useQuery to fetch document by chunk const { data: document, isLoading: isDocumentByChunkFetching, diff --git a/surfsense_web/components/dashboard-breadcrumb.tsx b/surfsense_web/components/dashboard-breadcrumb.tsx index f809f36b6..6335f9f1a 100644 --- a/surfsense_web/components/dashboard-breadcrumb.tsx +++ b/surfsense_web/components/dashboard-breadcrumb.tsx @@ -13,7 +13,9 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb"; -import { useSearchSpace } from "@/hooks/use-search-space"; +import { useQuery } from "@tanstack/react-query"; +import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; import { authenticatedFetch, getBearerToken } from "@/lib/auth-utils"; interface BreadcrumbItemInterface { @@ -29,10 +31,10 @@ export function DashboardBreadcrumb() { const segments = pathname.split("/").filter(Boolean); const searchSpaceId = segments[0] === "dashboard" && segments[1] ? segments[1] : null; - // Fetch search space details if we have an ID - const { searchSpace } = useSearchSpace({ - searchSpaceId: searchSpaceId || "", - autoFetch: !!searchSpaceId, + const { data: searchSpace } = useQuery({ + queryKey: cacheKeys.searchSpaces.detail(searchSpaceId || ""), + queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }), + enabled: !!searchSpaceId, }); // State to store document title for editor breadcrumb diff --git a/surfsense_web/components/onboard/setup-prompt-step.tsx b/surfsense_web/components/onboard/setup-prompt-step.tsx index 899d856fa..5e3683031 100644 --- a/surfsense_web/components/onboard/setup-prompt-step.tsx +++ b/surfsense_web/components/onboard/setup-prompt-step.tsx @@ -12,8 +12,9 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; -import { type CommunityPrompt, useCommunityPrompts } from "@/hooks/use-community-prompts"; +import { communityPromptsAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { authenticatedFetch } from "@/lib/auth-utils"; +import { useAtomValue } from "jotai"; interface SetupPromptStepProps { searchSpaceId: number; @@ -21,7 +22,7 @@ interface SetupPromptStepProps { } export function SetupPromptStep({ searchSpaceId, onComplete }: SetupPromptStepProps) { - const { prompts, loading: loadingPrompts } = useCommunityPrompts(); + const { data: prompts = [], isPending: loadingPrompts } = useAtomValue(communityPromptsAtom); const [enableCitations, setEnableCitations] = useState(true); const [customInstructions, setCustomInstructions] = useState(""); const [saving, setSaving] = useState(false); diff --git a/surfsense_web/components/settings/prompt-config-manager.tsx b/surfsense_web/components/settings/prompt-config-manager.tsx index a1199c10d..dae842305 100644 --- a/surfsense_web/components/settings/prompt-config-manager.tsx +++ b/surfsense_web/components/settings/prompt-config-manager.tsx @@ -23,8 +23,11 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; -import { type CommunityPrompt, useCommunityPrompts } from "@/hooks/use-community-prompts"; -import { useSearchSpace } from "@/hooks/use-search-space"; +import { useQuery } from "@tanstack/react-query"; +import { communityPromptsAtom } from "@/atoms/search-spaces/search-space-query.atoms"; +import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; +import { useAtomValue } from "jotai"; import { authenticatedFetch } from "@/lib/auth-utils"; interface PromptConfigManagerProps { @@ -32,11 +35,12 @@ interface PromptConfigManagerProps { } export function PromptConfigManager({ searchSpaceId }: PromptConfigManagerProps) { - const { searchSpace, loading, fetchSearchSpace } = useSearchSpace({ - searchSpaceId, - autoFetch: true, + const { data: searchSpace, isLoading: loading, refetch: fetchSearchSpace } = useQuery({ + queryKey: cacheKeys.searchSpaces.detail(searchSpaceId.toString()), + queryFn: () => searchSpacesApiService.getSearchSpace({ id: searchSpaceId }), + enabled: !!searchSpaceId, }); - const { prompts, loading: loadingPrompts } = useCommunityPrompts(); + const { data: prompts = [], isPending: loadingPrompts } = useAtomValue(communityPromptsAtom); const [enableCitations, setEnableCitations] = useState(true); const [customInstructions, setCustomInstructions] = useState(""); diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 76d92ba3b..fce8697f7 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -17,7 +17,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { useSearchSpace, useUser } from "@/hooks"; +import { useUser } from "@/hooks"; +import { useQuery } from "@tanstack/react-query"; +import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; +import { cacheKeys } from "@/lib/query-client/cache-keys"; interface AppSidebarProviderProps { searchSpaceId: string; @@ -55,11 +58,15 @@ export function AppSidebarProvider({ }, [searchSpaceId]); const { - searchSpace, - loading: isLoadingSearchSpace, + data: searchSpace, + isLoading: isLoadingSearchSpace, error: searchSpaceError, - fetchSearchSpace, - } = useSearchSpace({ searchSpaceId }); + refetch: fetchSearchSpace, + } = useQuery({ + queryKey: cacheKeys.searchSpaces.detail(searchSpaceId), + queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }), + enabled: !!searchSpaceId, + }); const { user } = useUser(); diff --git a/surfsense_web/contracts/types/search-space.types.ts b/surfsense_web/contracts/types/search-space.types.ts new file mode 100644 index 000000000..c0096b41c --- /dev/null +++ b/surfsense_web/contracts/types/search-space.types.ts @@ -0,0 +1,94 @@ +import { z } from "zod"; +import { paginationQueryParams } from "."; + +export const searchSpace = z.object({ + id: z.number(), + name: z.string(), + description: z.string().nullable(), + created_at: z.string(), + user_id: z.string(), + citations_enabled: z.boolean(), + qna_custom_instructions: z.string().nullable(), + member_count: z.number(), + is_owner: z.boolean(), +}); + +/** + * Get search spaces + */ +export const getSearchSpacesRequest = z.object({ + queryParams: paginationQueryParams + .extend({ + owned_only: z.boolean().optional(), + }) + .nullish(), +}); + +export const getSearchSpacesResponse = z.array(searchSpace); + +/** + * Create search space + */ +export const createSearchSpaceRequest = searchSpace + .pick({ name: true, description: true }) + .extend({ + citations_enabled: z.boolean().default(true).optional(), + qna_custom_instructions: z.string().nullable().optional(), + }); + +export const createSearchSpaceResponse = searchSpace.omit({ member_count: true, is_owner: true }); + +/** + * Get community prompts + */ +export const getCommunityPromptsResponse = z.array( + z.object({ + key: z.string(), + value: z.string(), + author: z.string(), + link: z.string(), + category: z.string(), + }) +); + +/** + * Get search space + */ +export const getSearchSpaceRequest = searchSpace.pick({ id: true }); + +export const getSearchSpaceResponse = searchSpace.omit({ member_count: true, is_owner: true }); + +/** + * Update search space + */ +export const updateSearchSpaceRequest = z.object({ + id: z.number(), + data: searchSpace + .pick({ name: true, description: true, citations_enabled: true, qna_custom_instructions: true }) + .partial(), +}); + +export const updateSearchSpaceResponse = searchSpace.omit({ member_count: true, is_owner: true }); + +/** + * Delete search space + */ +export const deleteSearchSpaceRequest = searchSpace.pick({ id: true }); + +export const deleteSearchSpaceResponse = z.object({ + message: z.literal("Search space deleted successfully"), +}); + +// Inferred types +export type SearchSpace = z.infer; +export type GetSearchSpacesRequest = z.infer; +export type GetSearchSpacesResponse = z.infer; +export type CreateSearchSpaceRequest = z.infer; +export type CreateSearchSpaceResponse = z.infer; +export type GetCommunityPromptsResponse = z.infer; +export type GetSearchSpaceRequest = z.infer; +export type GetSearchSpaceResponse = z.infer; +export type UpdateSearchSpaceRequest = z.infer; +export type UpdateSearchSpaceResponse = z.infer; +export type DeleteSearchSpaceRequest = z.infer; +export type DeleteSearchSpaceResponse = z.infer; diff --git a/surfsense_web/hooks/index.ts b/surfsense_web/hooks/index.ts index 2cea293e8..f7ef22534 100644 --- a/surfsense_web/hooks/index.ts +++ b/surfsense_web/hooks/index.ts @@ -1,5 +1,4 @@ export * from "./use-logs"; export * from "./use-rbac"; export * from "./use-search-source-connectors"; -export * from "./use-search-space"; export * from "./use-user"; diff --git a/surfsense_web/hooks/use-community-prompts.ts b/surfsense_web/hooks/use-community-prompts.ts deleted file mode 100644 index 3b4ac59db..000000000 --- a/surfsense_web/hooks/use-community-prompts.ts +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; - -export interface CommunityPrompt { - key: string; - value: string; - author: string; - link: string | null; - category?: string; -} - -export function useCommunityPrompts() { - const [prompts, setPrompts] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchPrompts = useCallback(async () => { - try { - setLoading(true); - const response = await fetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/prompts/community` - ); - - if (!response.ok) { - throw new Error(`Failed to fetch community prompts: ${response.status}`); - } - - const data = await response.json(); - setPrompts(data); - setError(null); - } catch (err: any) { - setError(err.message || "Failed to fetch community prompts"); - console.error("Error fetching community prompts:", err); - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchPrompts(); - }, [fetchPrompts]); - - return { prompts, loading, error, refetch: fetchPrompts }; -} diff --git a/surfsense_web/hooks/use-search-space.ts b/surfsense_web/hooks/use-search-space.ts deleted file mode 100644 index 849aad413..000000000 --- a/surfsense_web/hooks/use-search-space.ts +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; -import { authenticatedFetch } from "@/lib/auth-utils"; - -interface SearchSpace { - created_at: string; - id: number; - name: string; - description: string; - user_id: string; - citations_enabled: boolean; - qna_custom_instructions: string | null; -} - -interface UseSearchSpaceOptions { - searchSpaceId: string | number; - autoFetch?: boolean; -} - -export function useSearchSpace({ searchSpaceId, autoFetch = true }: UseSearchSpaceOptions) { - const [searchSpace, setSearchSpace] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - const fetchSearchSpace = useCallback(async () => { - try { - // Only run on client-side - if (typeof window === "undefined") return; - - setLoading(true); - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces/${searchSpaceId}`, - { method: "GET" } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch search space: ${response.status}`); - } - - const data = await response.json(); - setSearchSpace(data); - setError(null); - } catch (err: any) { - setError(err.message || "Failed to fetch search space"); - console.error("Error fetching search space:", err); - } finally { - setLoading(false); - } - }, [searchSpaceId]); - - useEffect(() => { - if (autoFetch) { - fetchSearchSpace(); - } - }, [autoFetch, fetchSearchSpace]); - - return { searchSpace, loading, error, fetchSearchSpace }; -} diff --git a/surfsense_web/hooks/use-search-spaces.ts b/surfsense_web/hooks/use-search-spaces.ts deleted file mode 100644 index 03a87881c..000000000 --- a/surfsense_web/hooks/use-search-spaces.ts +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { authenticatedFetch } from "@/lib/auth-utils"; - -interface SearchSpace { - id: number; - name: string; - description: string; - created_at: string; - citations_enabled: boolean; - qna_custom_instructions: string | null; - member_count: number; - is_owner: boolean; -} - -export function useSearchSpaces() { - const [searchSpaces, setSearchSpaces] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchSearchSpaces = async () => { - try { - setLoading(true); - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { method: "GET" } - ); - - if (!response.ok) { - toast.error("Failed to fetch search spaces"); - throw new Error("Failed to fetch search spaces"); - } - - const data = await response.json(); - setSearchSpaces(data); - setError(null); - } catch (err: any) { - setError(err.message || "Failed to fetch search spaces"); - console.error("Error fetching search spaces:", err); - } finally { - setLoading(false); - } - }; - - fetchSearchSpaces(); - }, []); - - // Function to refresh the search spaces list - const refreshSearchSpaces = async () => { - setLoading(true); - try { - const response = await authenticatedFetch( - `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/searchspaces`, - { method: "GET" } - ); - - if (!response.ok) { - toast.error("Failed to fetch search spaces"); - throw new Error("Failed to fetch search spaces"); - } - - const data = await response.json(); - setSearchSpaces(data); - setError(null); - } catch (err: any) { - setError(err.message || "Failed to fetch search spaces"); - } finally { - setLoading(false); - } - }; - - return { searchSpaces, loading, error, refreshSearchSpaces }; -} diff --git a/surfsense_web/lib/apis/search-spaces-api.service.ts b/surfsense_web/lib/apis/search-spaces-api.service.ts new file mode 100644 index 000000000..73b57ee3c --- /dev/null +++ b/surfsense_web/lib/apis/search-spaces-api.service.ts @@ -0,0 +1,128 @@ +import { + type CreateSearchSpaceRequest, + type DeleteSearchSpaceRequest, + type GetSearchSpaceRequest, + type GetSearchSpacesRequest, + type UpdateSearchSpaceRequest, + createSearchSpaceRequest, + createSearchSpaceResponse, + deleteSearchSpaceRequest, + deleteSearchSpaceResponse, + getCommunityPromptsResponse, + getSearchSpaceRequest, + getSearchSpaceResponse, + getSearchSpacesRequest, + getSearchSpacesResponse, + updateSearchSpaceRequest, + updateSearchSpaceResponse, +} from "@/contracts/types/search-space.types"; +import { ValidationError } from "../error"; +import { baseApiService } from "./base-api.service"; + +class SearchSpacesApiService { + /** + * Get a list of search spaces with optional filtering and pagination + */ + getSearchSpaces = async (request?: GetSearchSpacesRequest) => { + const parsedRequest = getSearchSpacesRequest.safeParse(request || {}); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + // Transform query params to be string values + const transformedQueryParams = parsedRequest.data.queryParams + ? Object.fromEntries( + Object.entries(parsedRequest.data.queryParams).map(([k, v]) => { + return [k, String(v)]; + }) + ) + : undefined; + + const queryParams = transformedQueryParams + ? new URLSearchParams(transformedQueryParams).toString() + : ""; + + return baseApiService.get(`/api/v1/searchspaces?${queryParams}`, getSearchSpacesResponse); + }; + + /** + * Create a new search space + */ + createSearchSpace = async (request: CreateSearchSpaceRequest) => { + const parsedRequest = createSearchSpaceRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.post(`/api/v1/searchspaces`, createSearchSpaceResponse, { + body: parsedRequest.data, + }); + }; + + /** + * Get community-curated prompts for search space system instructions + */ + getCommunityPrompts = async () => { + return baseApiService.get(`/api/v1/searchspaces/prompts/community`, getCommunityPromptsResponse); + }; + + /** + * Get a single search space by ID + */ + getSearchSpace = async (request: GetSearchSpaceRequest) => { + const parsedRequest = getSearchSpaceRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.get(`/api/v1/searchspaces/${request.id}`, getSearchSpaceResponse); + }; + + /** + * Update an existing search space + */ + updateSearchSpace = async (request: UpdateSearchSpaceRequest) => { + const parsedRequest = updateSearchSpaceRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.put(`/api/v1/searchspaces/${request.id}`, updateSearchSpaceResponse, { + body: parsedRequest.data.data, + }); + }; + + /** + * Delete a search space + */ + deleteSearchSpace = async (request: DeleteSearchSpaceRequest) => { + const parsedRequest = deleteSearchSpaceRequest.safeParse(request); + + if (!parsedRequest.success) { + console.error("Invalid request:", parsedRequest.error); + + const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.delete(`/api/v1/searchspaces/${request.id}`, deleteSearchSpaceResponse); + }; +} + +export const searchSpacesApiService = new SearchSpacesApiService(); diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index fcdf322bc..797c40b65 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -2,6 +2,7 @@ import type { GetChatsRequest } from "@/contracts/types/chat.types"; import type { GetDocumentsRequest } from "@/contracts/types/document.types"; import type { GetLLMConfigsRequest } from "@/contracts/types/llm-config.types"; import type { GetPodcastsRequest } from "@/contracts/types/podcast.types"; +import type { GetSearchSpacesRequest } from "@/contracts/types/search-space.types"; export const cacheKeys = { chats: { @@ -33,4 +34,11 @@ export const cacheKeys = { auth: { user: ["auth", "user"] as const, }, + searchSpaces: { + all: ["search-spaces"] as const, + withQueryParams: (queries: GetSearchSpacesRequest["queryParams"]) => + ["search-spaces", ...(queries ? Object.values(queries) : [])] as const, + detail: (searchSpaceId: string) => ["search-spaces", searchSpaceId] as const, + communityPrompts: ["search-spaces", "community-prompts"] as const, + } };