PromptVerse/src/components/create-prompt-dialog.tsx
2025-08-09 15:38:19 +00:00

168 lines
5.9 KiB
TypeScript

'use client';
import { useEffect, useRef, useState, useTransition } from 'react';
import { useFormState, useFormStatus } from 'react-dom';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { createPrompt, suggestPromptTags } from '@/lib/actions';
import { useToast } from '@/hooks/use-toast';
import { PlusCircle, Loader2, Sparkles, XIcon } from 'lucide-react';
import { Badge } from './ui/badge';
const initialState = {
message: '',
errors: {},
success: false,
};
function SubmitButton() {
const { pending } = useFormStatus();
return (
<Button type="submit" disabled={pending}>
{pending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Create Prompt
</Button>
);
}
export function CreatePromptDialog() {
const [open, setOpen] = useState(false);
const [state, formAction] = useFormState(createPrompt, initialState);
const { toast } = useToast();
const formRef = useRef<HTMLFormElement>(null);
const [promptContent, setPromptContent] = useState('');
const [tags, setTags] = useState<string[]>([]);
const [tagInput, setTagInput] = useState('');
const [isSuggesting, startSuggestionTransition] = useTransition();
const handleSuggestTags = async () => {
startSuggestionTransition(async () => {
const result = await suggestPromptTags(promptContent);
if (result.error) {
toast({ title: 'Suggestion Failed', description: result.error, variant: 'destructive' });
} else if (result.tags) {
setTags((prev) => [...new Set([...prev, ...result.tags!])]);
toast({ title: 'Tags Suggested!', description: 'AI-powered tags have been added.' });
}
});
};
const handleTagInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && tagInput.trim()) {
e.preventDefault();
setTags((prev) => [...new Set([...prev, tagInput.trim().toLowerCase()])]);
setTagInput('');
}
};
const removeTag = (tagToRemove: string) => {
setTags((prev) => prev.filter((tag) => tag !== tagToRemove));
};
useEffect(() => {
if (state.success) {
toast({
title: 'Success!',
description: state.message,
});
setOpen(false);
formRef.current?.reset();
setTags([]);
setPromptContent('');
} else if (state.message && !state.success && Object.keys(state.errors ?? {}).length > 0) {
toast({
title: 'Error',
description: state.message,
variant: 'destructive',
});
}
}, [state, toast]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
New Prompt
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[625px]">
<DialogHeader>
<DialogTitle>Create a new prompt</DialogTitle>
<DialogDescription>
Craft your next masterpiece. Add notes and tags to keep it organized.
</DialogDescription>
</DialogHeader>
<form ref={formRef} action={formAction} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Title</Label>
<Input id="title" name="title" placeholder="e.g., Blog Post Ideas" required />
{state.errors?.title && <p className="text-sm text-destructive">{state.errors.title[0]}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="content">Prompt</Label>
<Textarea
id="content"
name="content"
placeholder="Your prompt text here..."
className="min-h-[120px]"
onChange={(e) => setPromptContent(e.target.value)}
required
/>
{state.errors?.content && <p className="text-sm text-destructive">{state.errors.content[0]}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="notes">Notes (Optional)</Label>
<Textarea
id="notes"
name="notes"
placeholder="Add context or notes about this prompt..."
/>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<Label htmlFor="tags">Tags</Label>
<Button type="button" size="sm" variant="ghost" onClick={handleSuggestTags} disabled={isSuggesting || promptContent.length < 20}>
{isSuggesting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Sparkles className="mr-2 h-4 w-4" />}
Suggest
</Button>
</div>
<div className="flex flex-wrap gap-2 p-2 border rounded-md min-h-[40px] bg-muted/50">
{tags.map(tag => (
<Badge key={tag} variant="secondary">
{tag}
<button type="button" onClick={() => removeTag(tag)} className="ml-1 rounded-full hover:bg-destructive/80 p-0.5">
<XIcon className="h-3 w-3" />
</button>
</Badge>
))}
</div>
<Input id="tags-input" value={tagInput} onChange={(e) => setTagInput(e.target.value)} onKeyDown={handleTagInputKeyDown} placeholder="Type a tag and press Enter" />
<input type="hidden" name="tags" value={JSON.stringify(tags)} />
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">Cancel</Button>
</DialogClose>
<SubmitButton />
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}