168 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|