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,
+ }
};