diff --git a/surfsense_web/components/new-chat/action-picker.tsx b/surfsense_web/components/new-chat/action-picker.tsx index d5ef01ae1..4bfac23f4 100644 --- a/surfsense_web/components/new-chat/action-picker.tsx +++ b/surfsense_web/components/new-chat/action-picker.tsx @@ -21,6 +21,8 @@ import { useState, } from "react"; +import type { QuickAskActionRead } from "@/contracts/types/quick-ask-actions.types"; +import { quickAskActionsApiService } from "@/lib/apis/quick-ask-actions-api.service"; import { cn } from "@/lib/utils"; export interface ActionPickerRef { @@ -62,11 +64,24 @@ const DEFAULT_ACTIONS: { name: string; prompt: string; mode: "transform" | "expl export const ActionPicker = forwardRef( function ActionPicker({ onSelect, onDone, externalSearch = "", containerStyle }, ref) { const [highlightedIndex, setHighlightedIndex] = useState(0); + const [customActions, setCustomActions] = useState([]); const scrollContainerRef = useRef(null); const shouldScrollRef = useRef(false); const itemRefs = useRef>(new Map()); - const allActions = DEFAULT_ACTIONS; + useEffect(() => { + quickAskActionsApiService.list().then(setCustomActions).catch(() => {}); + }, []); + + const allActions = useMemo(() => { + const customs = customActions.map((a) => ({ + name: a.name, + prompt: a.prompt, + mode: a.mode as "transform" | "explore", + icon: a.icon || "zap", + })); + return [...DEFAULT_ACTIONS, ...customs]; + }, [customActions]); const filtered = useMemo(() => { if (!externalSearch) return allActions; diff --git a/surfsense_web/contracts/types/quick-ask-actions.types.ts b/surfsense_web/contracts/types/quick-ask-actions.types.ts index f7ee22c0b..eaee09501 100644 --- a/surfsense_web/contracts/types/quick-ask-actions.types.ts +++ b/surfsense_web/contracts/types/quick-ask-actions.types.ts @@ -1,5 +1,44 @@ +import { z } from "zod"; + export type QuickAskActionMode = "transform" | "explore"; +export const quickAskActionRead = z.object({ + id: z.number(), + name: z.string(), + prompt: z.string(), + mode: z.enum(["transform", "explore"]), + icon: z.string().nullable(), + search_space_id: z.number().nullable(), + created_at: z.string(), +}); + +export type QuickAskActionRead = z.infer; + +export const quickAskActionsListResponse = z.array(quickAskActionRead); + +export const quickAskActionCreateRequest = z.object({ + name: z.string().min(1).max(200), + prompt: z.string().min(1), + mode: z.enum(["transform", "explore"]), + icon: z.string().max(50).nullable().optional(), + search_space_id: z.number().nullable().optional(), +}); + +export type QuickAskActionCreateRequest = z.infer; + +export const quickAskActionUpdateRequest = z.object({ + name: z.string().min(1).max(200).optional(), + prompt: z.string().min(1).optional(), + mode: z.enum(["transform", "explore"]).optional(), + icon: z.string().max(50).nullable().optional(), +}); + +export type QuickAskActionUpdateRequest = z.infer; + +export const quickAskActionDeleteResponse = z.object({ + success: z.boolean(), +}); + export interface QuickAskAction { id: string; name: string; diff --git a/surfsense_web/lib/apis/quick-ask-actions-api.service.ts b/surfsense_web/lib/apis/quick-ask-actions-api.service.ts new file mode 100644 index 000000000..ae1c3a360 --- /dev/null +++ b/surfsense_web/lib/apis/quick-ask-actions-api.service.ts @@ -0,0 +1,59 @@ +import { + type QuickAskActionCreateRequest, + type QuickAskActionUpdateRequest, + quickAskActionCreateRequest, + quickAskActionDeleteResponse, + quickAskActionRead, + quickAskActionUpdateRequest, + quickAskActionsListResponse, +} from "@/contracts/types/quick-ask-actions.types"; +import { ValidationError } from "@/lib/error"; +import { baseApiService } from "./base-api.service"; + +class QuickAskActionsApiService { + list = async (searchSpaceId?: number) => { + const params = new URLSearchParams(); + if (searchSpaceId !== undefined) { + params.set("search_space_id", String(searchSpaceId)); + } + const queryString = params.toString(); + const url = queryString + ? `/api/v1/quick-ask-actions?${queryString}` + : "/api/v1/quick-ask-actions"; + + return baseApiService.get(url, quickAskActionsListResponse); + }; + + create = async (request: QuickAskActionCreateRequest) => { + const parsed = quickAskActionCreateRequest.safeParse(request); + if (!parsed.success) { + const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.post("/api/v1/quick-ask-actions", quickAskActionRead, { + body: parsed.data, + }); + }; + + update = async (actionId: number, request: QuickAskActionUpdateRequest) => { + const parsed = quickAskActionUpdateRequest.safeParse(request); + if (!parsed.success) { + const errorMessage = parsed.error.issues.map((issue) => issue.message).join(", "); + throw new ValidationError(`Invalid request: ${errorMessage}`); + } + + return baseApiService.put(`/api/v1/quick-ask-actions/${actionId}`, quickAskActionRead, { + body: parsed.data, + }); + }; + + delete = async (actionId: number) => { + return baseApiService.delete( + `/api/v1/quick-ask-actions/${actionId}`, + quickAskActionDeleteResponse + ); + }; +} + +export const quickAskActionsApiService = new QuickAskActionsApiService();