diff --git a/surfsense_backend/app/routes/prompts_routes.py b/surfsense_backend/app/routes/prompts_routes.py
index 810ec2f18..0625e3611 100644
--- a/surfsense_backend/app/routes/prompts_routes.py
+++ b/surfsense_backend/app/routes/prompts_routes.py
@@ -41,6 +41,7 @@ async def create_prompt(
name=body.name,
prompt=body.prompt,
mode=body.mode,
+ is_public=body.is_public,
)
session.add(prompt)
await session.commit()
@@ -65,10 +66,15 @@ async def update_prompt(
if not prompt:
raise HTTPException(status_code=404, detail="Prompt not found")
- for field, value in body.model_dump(exclude_unset=True).items():
+ updates = body.model_dump(exclude_unset=True)
+ content_fields = {"name", "prompt", "mode"}
+ has_content_change = bool(updates.keys() & content_fields)
+
+ for field, value in updates.items():
setattr(prompt, field, value)
- prompt.version = (prompt.version or 0) + 1
+ if has_content_change:
+ prompt.version = (prompt.version or 0) + 1
session.add(prompt)
await session.commit()
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/CommunityPromptsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/CommunityPromptsContent.tsx
index 363919b43..79384465d 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/CommunityPromptsContent.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/CommunityPromptsContent.tsx
@@ -1,38 +1,34 @@
"use client";
+import { useAtomValue } from "jotai";
import { Copy, Globe, Sparkles } from "lucide-react";
-import { useCallback, useEffect, useState } from "react";
-import { toast } from "sonner";
+import { useCallback, useState } from "react";
+import { copyPromptMutationAtom } from "@/atoms/prompts/prompts-mutation.atoms";
+import { publicPromptsAtom } from "@/atoms/prompts/prompts-query.atoms";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
-import type { PublicPromptRead } from "@/contracts/types/prompts.types";
-import { promptsApiService } from "@/lib/apis/prompts-api.service";
export function CommunityPromptsContent() {
- const [prompts, setPrompts] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
+ const { data: prompts, isLoading } = useAtomValue(publicPromptsAtom);
+ const { mutateAsync: copyPrompt, isPending: isCopying } = useAtomValue(copyPromptMutationAtom);
const [copyingId, setCopyingId] = useState(null);
const [expandedId, setExpandedId] = useState(null);
- useEffect(() => {
- promptsApiService
- .listPublic()
- .then(setPrompts)
- .catch(() => toast.error("Failed to load community prompts"))
- .finally(() => setIsLoading(false));
- }, []);
+ const handleCopy = useCallback(
+ async (id: number) => {
+ setCopyingId(id);
+ try {
+ await copyPrompt(id);
+ } catch {
+ // toast handled by mutation atom
+ } finally {
+ setCopyingId(null);
+ }
+ },
+ [copyPrompt]
+ );
- const handleCopy = useCallback(async (id: number) => {
- setCopyingId(id);
- try {
- await promptsApiService.copy(id);
- toast.success("Prompt added to your collection");
- } catch {
- toast.error("Failed to copy prompt");
- } finally {
- setCopyingId(null);
- }
- }, []);
+ const list = prompts ?? [];
if (isLoading) {
return (
@@ -48,7 +44,7 @@ export function CommunityPromptsContent() {
Prompts shared by other users. Add any to your collection with one click.
- {prompts.length === 0 && (
+ {list.length === 0 && (
No community prompts yet
@@ -58,9 +54,9 @@ export function CommunityPromptsContent() {
)}
- {prompts.length > 0 && (
+ {list.length > 0 && (
- {prompts.map((prompt) => (
+ {list.map((prompt) => (
)}
-
+
{prompt.prompt}
{prompt.prompt.length > 100 && (
@@ -97,10 +95,10 @@ export function CommunityPromptsContent() {
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
- disabled={copyingId === prompt.id}
+ disabled={copyingId === prompt.id && isCopying}
onClick={() => handleCopy(prompt.id)}
>
- {copyingId === prompt.id ? (
+ {copyingId === prompt.id && isCopying ? (
) : (
diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx
index e8c45a31a..919af3e79 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/PromptsContent.tsx
@@ -1,15 +1,21 @@
"use client";
+import { useAtomValue } from "jotai";
import { Globe, Lock, PenLine, Plus, Sparkles, Trash2 } from "lucide-react";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useState } from "react";
import { toast } from "sonner";
+import {
+ createPromptMutationAtom,
+ deletePromptMutationAtom,
+ updatePromptMutationAtom,
+} from "@/atoms/prompts/prompts-mutation.atoms";
+import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import type { PromptRead } from "@/contracts/types/prompts.types";
-import { promptsApiService } from "@/lib/apis/prompts-api.service";
interface PromptFormData {
name: string;
@@ -21,22 +27,17 @@ interface PromptFormData {
const EMPTY_FORM: PromptFormData = { name: "", prompt: "", mode: "transform", is_public: false };
export function PromptsContent() {
- const [prompts, setPrompts] = useState
([]);
- const [isLoading, setIsLoading] = useState(true);
+ const { data: prompts, isLoading } = useAtomValue(promptsAtom);
+ const { mutateAsync: createPrompt } = useAtomValue(createPromptMutationAtom);
+ const { mutateAsync: updatePrompt } = useAtomValue(updatePromptMutationAtom);
+ const { mutateAsync: deletePrompt } = useAtomValue(deletePromptMutationAtom);
+
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState(null);
const [formData, setFormData] = useState(EMPTY_FORM);
const [isSaving, setIsSaving] = useState(false);
const [expandedId, setExpandedId] = useState(null);
- useEffect(() => {
- promptsApiService
- .list()
- .then(setPrompts)
- .catch(() => toast.error("Failed to load prompts"))
- .finally(() => setIsLoading(false));
- }, []);
-
const handleSave = useCallback(async () => {
if (!formData.name.trim() || !formData.prompt.trim()) {
toast.error("Name and prompt are required");
@@ -46,23 +47,19 @@ export function PromptsContent() {
setIsSaving(true);
try {
if (editingId) {
- const updated = await promptsApiService.update(editingId, formData);
- setPrompts((prev) => prev.map((p) => (p.id === editingId ? updated : p)));
- toast.success("Prompt updated");
+ await updatePrompt({ id: editingId, ...formData });
} else {
- const created = await promptsApiService.create(formData);
- setPrompts((prev) => [created, ...prev]);
- toast.success("Prompt created");
+ await createPrompt(formData);
}
setShowForm(false);
setFormData(EMPTY_FORM);
setEditingId(null);
} catch {
- toast.error("Failed to save prompt");
+ // toast handled by mutation atoms
} finally {
setIsSaving(false);
}
- }, [formData, editingId]);
+ }, [formData, editingId, createPrompt, updatePrompt]);
const handleEdit = useCallback((prompt: PromptRead) => {
setFormData({
@@ -75,15 +72,27 @@ export function PromptsContent() {
setShowForm(true);
}, []);
- const handleDelete = useCallback(async (id: number) => {
- try {
- await promptsApiService.delete(id);
- setPrompts((prev) => prev.filter((p) => p.id !== id));
- toast.success("Prompt deleted");
- } catch {
- toast.error("Failed to delete prompt");
- }
- }, []);
+ const handleDelete = useCallback(
+ async (id: number) => {
+ try {
+ await deletePrompt(id);
+ } catch {
+ // toast handled by mutation atom
+ }
+ },
+ [deletePrompt]
+ );
+
+ const handleTogglePublic = useCallback(
+ async (prompt: PromptRead) => {
+ try {
+ await updatePrompt({ id: prompt.id, is_public: !prompt.is_public });
+ } catch {
+ // toast handled by mutation atom
+ }
+ },
+ [updatePrompt]
+ );
const handleCancel = useCallback(() => {
setShowForm(false);
@@ -91,6 +100,8 @@ export function PromptsContent() {
setEditingId(null);
}, []);
+ const list = prompts ?? [];
+
if (isLoading) {
return (
@@ -195,7 +206,7 @@ export function PromptsContent() {
)}
- {prompts.length === 0 && !showForm && (
+ {list.length === 0 && !showForm && (
No prompts yet
@@ -205,9 +216,9 @@ export function PromptsContent() {
)}
- {prompts.length > 0 && (
+ {list.length > 0 && (
- {prompts.map((prompt) => (
+ {list.map((prompt) => (
{
- try {
- const updated = await promptsApiService.update(prompt.id, {
- is_public: !prompt.is_public,
- });
- setPrompts((prev) => prev.map((p) => (p.id === prompt.id ? updated : p)));
- toast.success(updated.is_public ? "Shared with community" : "Made private");
- } catch {
- toast.error("Failed to update");
- }
- }}
+ onClick={() => handleTogglePublic(prompt)}
className="flex items-center justify-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"
>
{prompt.is_public ? (
diff --git a/surfsense_web/atoms/prompts/prompts-mutation.atoms.ts b/surfsense_web/atoms/prompts/prompts-mutation.atoms.ts
new file mode 100644
index 000000000..b1f102329
--- /dev/null
+++ b/surfsense_web/atoms/prompts/prompts-mutation.atoms.ts
@@ -0,0 +1,71 @@
+import { atomWithMutation } from "jotai-tanstack-query";
+import { toast } from "sonner";
+import type {
+ PromptCreateRequest,
+ PromptRead,
+ PromptUpdateRequest,
+} from "@/contracts/types/prompts.types";
+import { promptsApiService } from "@/lib/apis/prompts-api.service";
+import { cacheKeys } from "@/lib/query-client/cache-keys";
+import { queryClient } from "@/lib/query-client/client";
+
+export const createPromptMutationAtom = atomWithMutation(() => ({
+ mutationKey: ["prompts", "create"],
+ mutationFn: async (request: PromptCreateRequest) => {
+ return promptsApiService.create(request);
+ },
+ onSuccess: () => {
+ toast.success("Prompt created");
+ queryClient.invalidateQueries({ queryKey: cacheKeys.prompts.all() });
+ },
+ onError: (error: Error) => {
+ toast.error(error.message || "Failed to create prompt");
+ },
+}));
+
+export const updatePromptMutationAtom = atomWithMutation(() => ({
+ mutationKey: ["prompts", "update"],
+ mutationFn: async ({ id, ...data }: PromptUpdateRequest & { id: number }) => {
+ return promptsApiService.update(id, data);
+ },
+ onSuccess: () => {
+ toast.success("Prompt updated");
+ queryClient.invalidateQueries({ queryKey: cacheKeys.prompts.all() });
+ queryClient.invalidateQueries({ queryKey: cacheKeys.prompts.public() });
+ },
+ onError: (error: Error) => {
+ toast.error(error.message || "Failed to update prompt");
+ },
+}));
+
+export const deletePromptMutationAtom = atomWithMutation(() => ({
+ mutationKey: ["prompts", "delete"],
+ mutationFn: async (id: number) => {
+ return promptsApiService.delete(id);
+ },
+ onSuccess: (_: unknown, id: number) => {
+ toast.success("Prompt deleted");
+ queryClient.setQueryData(cacheKeys.prompts.all(), (old: PromptRead[] | undefined) => {
+ if (!old) return old;
+ return old.filter((p) => p.id !== id);
+ });
+ queryClient.invalidateQueries({ queryKey: cacheKeys.prompts.public() });
+ },
+ onError: (error: Error) => {
+ toast.error(error.message || "Failed to delete prompt");
+ },
+}));
+
+export const copyPromptMutationAtom = atomWithMutation(() => ({
+ mutationKey: ["prompts", "copy"],
+ mutationFn: async (promptId: number) => {
+ return promptsApiService.copy(promptId);
+ },
+ onSuccess: () => {
+ toast.success("Prompt added to your collection");
+ queryClient.invalidateQueries({ queryKey: cacheKeys.prompts.all() });
+ },
+ onError: (error: Error) => {
+ toast.error(error.message || "Failed to copy prompt");
+ },
+}));
diff --git a/surfsense_web/atoms/prompts/prompts-query.atoms.ts b/surfsense_web/atoms/prompts/prompts-query.atoms.ts
new file mode 100644
index 000000000..f9042c03a
--- /dev/null
+++ b/surfsense_web/atoms/prompts/prompts-query.atoms.ts
@@ -0,0 +1,23 @@
+import { atomWithQuery } from "jotai-tanstack-query";
+import { promptsApiService } from "@/lib/apis/prompts-api.service";
+import { cacheKeys } from "@/lib/query-client/cache-keys";
+
+export const promptsAtom = atomWithQuery(() => {
+ return {
+ queryKey: cacheKeys.prompts.all(),
+ staleTime: 5 * 60 * 1000,
+ queryFn: async () => {
+ return promptsApiService.list();
+ },
+ };
+});
+
+export const publicPromptsAtom = atomWithQuery(() => {
+ return {
+ queryKey: cacheKeys.prompts.public(),
+ staleTime: 2 * 60 * 1000,
+ queryFn: async () => {
+ return promptsApiService.listPublic();
+ },
+ };
+});
diff --git a/surfsense_web/components/new-chat/prompt-picker.tsx b/surfsense_web/components/new-chat/prompt-picker.tsx
index a24ec5a08..ec84363dd 100644
--- a/surfsense_web/components/new-chat/prompt-picker.tsx
+++ b/surfsense_web/components/new-chat/prompt-picker.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useSetAtom } from "jotai";
+import { useAtomValue, useSetAtom } from "jotai";
import { Plus, Zap } from "lucide-react";
import {
forwardRef,
@@ -12,9 +12,8 @@ import {
useState,
} from "react";
+import { promptsAtom } from "@/atoms/prompts/prompts-query.atoms";
import { userSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
-import type { PromptRead } from "@/contracts/types/prompts.types";
-import { promptsApiService } from "@/lib/apis/prompts-api.service";
import { cn } from "@/lib/utils";
export interface PromptPickerRef {
@@ -35,22 +34,16 @@ export const PromptPicker = forwardRef
(funct
ref
) {
const setUserSettingsDialog = useSetAtom(userSettingsDialogAtom);
+ const { data: prompts } = useAtomValue(promptsAtom);
const [highlightedIndex, setHighlightedIndex] = useState(0);
- const [prompts, setPrompts] = useState([]);
const scrollContainerRef = useRef(null);
const shouldScrollRef = useRef(false);
const itemRefs = useRef