Merge pull request #583 from CREDO23/feat/add-jotai-tanstack-search-spaces-new

[Feat]  Search spaces | Add jotai & tanstack
This commit is contained in:
Rohan Verma 2025-12-15 20:20:38 -08:00 committed by GitHub
commit 8aedead33e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 401 additions and 267 deletions

View file

@ -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";

View file

@ -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 (

View file

@ -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 (

View file

@ -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) => {

View file

@ -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";

View file

@ -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,

View file

@ -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";

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -1,3 +0,0 @@
import { atom } from "jotai";
export const activeSearchSpaceIdAtom = atom<string | null>(null);

View file

@ -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)),
});
}
},
};
});

View file

@ -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();
},
};
});

View file

@ -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,

View file

@ -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

View file

@ -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);

View file

@ -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("");

View file

@ -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();

View 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>;

View file

@ -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";

View file

@ -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 };
}

View file

@ -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 };
}

View file

@ -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 };
}

View 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();

View file

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