add community prompts tab and public toggle in prompt form

This commit is contained in:
CREDO23 2026-03-30 19:41:14 +02:00
parent 16884963a4
commit 1238efaf99
3 changed files with 154 additions and 15 deletions

View file

@ -0,0 +1,104 @@
"use client";
import { Copy, Globe, Sparkles } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
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 [copyingId, setCopyingId] = 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 promptsApiService.copy(id);
toast.success("Prompt added to your collection");
} catch {
toast.error("Failed to copy prompt");
} finally {
setCopyingId(null);
}
}, []);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Spinner className="size-6" />
</div>
);
}
return (
<div className="space-y-6 min-w-0 overflow-hidden">
<p className="text-sm text-muted-foreground">
Prompts shared by other users. Add any to your collection with one click.
</p>
{prompts.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>
<p className="text-xs text-muted-foreground/60">
Share your own prompts from the My Prompts tab
</p>
</div>
)}
{prompts.length > 0 && (
<div className="space-y-2">
{prompts.map((prompt) => (
<div
key={prompt.id}
className="group flex items-start gap-3 rounded-lg border border-border/60 bg-card p-4"
>
<div className="mt-0.5 shrink-0 text-muted-foreground">
<Sparkles className="size-4" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{prompt.name}</span>
<span className="rounded-full border px-2 py-0.5 text-[10px] text-muted-foreground">
{prompt.mode}
</span>
</div>
<p className="mt-1 text-xs text-muted-foreground line-clamp-2">{prompt.prompt}</p>
{prompt.author_name && (
<p className="mt-1.5 text-[11px] text-muted-foreground/60">
by {prompt.author_name}
</p>
)}
</div>
<Button
variant="outline"
size="sm"
className="shrink-0 gap-1.5"
disabled={copyingId === prompt.id}
onClick={() => handleCopy(prompt.id)}
>
{copyingId === prompt.id ? (
<Spinner className="size-3" />
) : (
<Copy className="size-3" />
)}
Add to mine
</Button>
</div>
))}
</div>
)}
</div>
);
}

View file

@ -1,22 +1,24 @@
"use client";
import { PenLine, Plus, Sparkles, Trash2 } from "lucide-react";
import { Globe, PenLine, Plus, Sparkles, Trash2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import type { PromptRead } from "@/contracts/types/prompts.types";
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;
prompt: string;
mode: "transform" | "explore";
is_public: boolean;
}
const EMPTY_FORM: PromptFormData = { name: "", prompt: "", mode: "transform" };
const EMPTY_FORM: PromptFormData = { name: "", prompt: "", mode: "transform", is_public: false };
export function PromptsContent() {
const [prompts, setPrompts] = useState<PromptRead[]>([]);
@ -66,6 +68,7 @@ export function PromptsContent() {
name: prompt.name,
prompt: prompt.prompt,
mode: prompt.mode as "transform" | "explore",
is_public: prompt.is_public,
});
setEditingId(prompt.id);
setShowForm(true);
@ -99,7 +102,9 @@ export function PromptsContent() {
<div className="space-y-6 min-w-0 overflow-hidden">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Create prompt templates triggered with <kbd className="rounded border bg-muted px-1.5 py-0.5 text-xs font-mono">/</kbd> in the chat composer.
Create prompt templates triggered with{" "}
<kbd className="rounded border bg-muted px-1.5 py-0.5 text-xs font-mono">/</kbd> in the
chat composer.
</p>
{!showForm && (
<Button
@ -144,7 +149,11 @@ export function PromptsContent() {
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none resize-none focus:ring-1 focus:ring-ring"
/>
<p className="text-xs text-muted-foreground">
Use <code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">{"{selection}"}</code> to insert the input text. If omitted, the text is appended automatically.
Use{" "}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-[11px]">
{"{selection}"}
</code>{" "}
to insert the input text. If omitted, the text is appended automatically.
</p>
</div>
@ -153,7 +162,9 @@ export function PromptsContent() {
<select
id="prompt-mode"
value={formData.mode}
onChange={(e) => setFormData((p) => ({ ...p, mode: e.target.value as "transform" | "explore" }))}
onChange={(e) =>
setFormData((p) => ({ ...p, mode: e.target.value as "transform" | "explore" }))
}
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring"
>
<option value="transform">Transform rewrites or modifies your text</option>
@ -161,14 +172,25 @@ export function PromptsContent() {
</select>
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel
</Button>
<Button size="sm" onClick={handleSave} disabled={isSaving}>
{isSaving ? <Spinner className="size-3.5" /> : editingId ? "Update" : "Create"}
</Button>
</div>
<div className="flex items-center gap-2">
<Switch
id="prompt-public"
checked={formData.is_public}
onCheckedChange={(checked) => setFormData((p) => ({ ...p, is_public: checked }))}
/>
<Label htmlFor="prompt-public" className="text-sm font-normal">
Share with community
</Label>
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel
</Button>
<Button size="sm" onClick={handleSave} disabled={isSaving}>
{isSaving ? <Spinner className="size-3.5" /> : editingId ? "Update" : "Create"}
</Button>
</div>
</div>
)}
@ -198,6 +220,12 @@ export function PromptsContent() {
<span className="rounded-full border px-2 py-0.5 text-[10px] text-muted-foreground">
{prompt.mode}
</span>
{prompt.is_public && (
<span className="flex items-center gap-1 rounded-full border border-primary/20 bg-primary/5 px-2 py-0.5 text-[10px] text-primary">
<Globe className="size-2.5" />
Public
</span>
)}
</div>
<p className="mt-1 text-xs text-muted-foreground line-clamp-2">{prompt.prompt}</p>
</div>