mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
Merge pull request #583 from CREDO23/feat/add-jotai-tanstack-search-spaces-new
[Feat] Search spaces | Add jotai & tanstack
This commit is contained in:
commit
8aedead33e
25 changed files with 401 additions and 267 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 <LoadingScreen />;
|
||||
if (error) return <ErrorScreen message={error} />;
|
||||
if (error) return <ErrorScreen message={error?.message || 'Failed to load search spaces'} />;
|
||||
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import { atom } from "jotai";
|
||||
|
||||
export const activeSearchSpaceIdAtom = atom<string | null>(null);
|
||||
|
|
@ -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)),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -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<string | null>(null);
|
||||
|
||||
export const searchSpacesQueryParamsAtom = atom<GetSearchSpacesRequest["queryParams"]>({
|
||||
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();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
@ -52,7 +52,6 @@ export function SourceDetailSheet({
|
|||
const highlightedChunkRef = useRef<HTMLDivElement>(null);
|
||||
const [summaryOpen, setSummaryOpen] = useState(false);
|
||||
|
||||
// Add useQuery to fetch document by chunk
|
||||
const {
|
||||
data: document,
|
||||
isLoading: isDocumentByChunkFetching,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
94
surfsense_web/contracts/types/search-space.types.ts
Normal file
94
surfsense_web/contracts/types/search-space.types.ts
Normal file
|
|
@ -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<typeof searchSpace>;
|
||||
export type GetSearchSpacesRequest = z.infer<typeof getSearchSpacesRequest>;
|
||||
export type GetSearchSpacesResponse = z.infer<typeof getSearchSpacesResponse>;
|
||||
export type CreateSearchSpaceRequest = z.infer<typeof createSearchSpaceRequest>;
|
||||
export type CreateSearchSpaceResponse = z.infer<typeof createSearchSpaceResponse>;
|
||||
export type GetCommunityPromptsResponse = z.infer<typeof getCommunityPromptsResponse>;
|
||||
export type GetSearchSpaceRequest = z.infer<typeof getSearchSpaceRequest>;
|
||||
export type GetSearchSpaceResponse = z.infer<typeof getSearchSpaceResponse>;
|
||||
export type UpdateSearchSpaceRequest = z.infer<typeof updateSearchSpaceRequest>;
|
||||
export type UpdateSearchSpaceResponse = z.infer<typeof updateSearchSpaceResponse>;
|
||||
export type DeleteSearchSpaceRequest = z.infer<typeof deleteSearchSpaceRequest>;
|
||||
export type DeleteSearchSpaceResponse = z.infer<typeof deleteSearchSpaceResponse>;
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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<CommunityPrompt[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 };
|
||||
}
|
||||
|
|
@ -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<SearchSpace | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 };
|
||||
}
|
||||
|
|
@ -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<SearchSpace[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 };
|
||||
}
|
||||
128
surfsense_web/lib/apis/search-spaces-api.service.ts
Normal file
128
surfsense_web/lib/apis/search-spaces-api.service.ts
Normal file
|
|
@ -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();
|
||||
|
|
@ -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,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue