mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 17:56:25 +02:00
Fix create_prompt is_public bug, conditional version bump, and add Jotai prompts atoms
- Pass is_public from request body in create_prompt route - Only bump version on content field changes (name, prompt, mode), not on is_public toggle - Add prompts query and mutation atoms (atomWithQuery/atomWithMutation) with TanStack Query caching, replacing manual useEffect fetches - Update PromptPicker, PromptsContent, and CommunityPromptsContent to consume shared atoms instead of local state
This commit is contained in:
parent
95620a4331
commit
5f4f7780d1
7 changed files with 184 additions and 88 deletions
|
|
@ -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<PublicPromptRead[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { data: prompts, isLoading } = useAtomValue(publicPromptsAtom);
|
||||
const { mutateAsync: copyPrompt, isPending: isCopying } = useAtomValue(copyPromptMutationAtom);
|
||||
const [copyingId, setCopyingId] = useState<number | null>(null);
|
||||
const [expandedId, setExpandedId] = useState<number | null>(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.
|
||||
</p>
|
||||
|
||||
{prompts.length === 0 && (
|
||||
{list.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center">
|
||||
<Globe className="mx-auto size-8 text-muted-foreground/40" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">No community prompts yet</p>
|
||||
|
|
@ -58,9 +54,9 @@ export function CommunityPromptsContent() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{prompts.length > 0 && (
|
||||
{list.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{prompts.map((prompt) => (
|
||||
{list.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className="group flex items-start gap-3 rounded-lg border border-border/60 bg-card p-4"
|
||||
|
|
@ -80,7 +76,9 @@ export function CommunityPromptsContent() {
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className={`mt-1 text-xs text-muted-foreground ${expandedId === prompt.id ? "whitespace-pre-wrap" : "line-clamp-2"}`}>
|
||||
<p
|
||||
className={`mt-1 text-xs text-muted-foreground ${expandedId === prompt.id ? "whitespace-pre-wrap" : "line-clamp-2"}`}
|
||||
>
|
||||
{prompt.prompt}
|
||||
</p>
|
||||
{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 ? (
|
||||
<Spinner className="size-3" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
|
|
|
|||
|
|
@ -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<PromptRead[]>([]);
|
||||
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<number | null>(null);
|
||||
const [formData, setFormData] = useState<PromptFormData>(EMPTY_FORM);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [expandedId, setExpandedId] = useState<number | null>(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 (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
|
|
@ -195,7 +206,7 @@ export function PromptsContent() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{prompts.length === 0 && !showForm && (
|
||||
{list.length === 0 && !showForm && (
|
||||
<div className="rounded-lg border border-dashed border-border/60 p-8 text-center">
|
||||
<Sparkles className="mx-auto size-8 text-muted-foreground/40" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">No prompts yet</p>
|
||||
|
|
@ -205,9 +216,9 @@ export function PromptsContent() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{prompts.length > 0 && (
|
||||
{list.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{prompts.map((prompt) => (
|
||||
{list.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className="group flex items-start gap-3 rounded-lg border border-border/60 bg-card p-4"
|
||||
|
|
@ -247,17 +258,7 @@ export function PromptsContent() {
|
|||
<button
|
||||
type="button"
|
||||
title={prompt.is_public ? "Make private" : "Share with community"}
|
||||
onClick={async () => {
|
||||
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 ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue