diff --git a/.idx/icon.png b/.idx/icon.png new file mode 100644 index 0000000..3c5444e Binary files /dev/null and b/.idx/icon.png differ diff --git a/.modified b/.modified new file mode 100644 index 0000000..e69de29 diff --git a/docs/blueprint.md b/docs/blueprint.md new file mode 100644 index 0000000..7049df0 --- /dev/null +++ b/docs/blueprint.md @@ -0,0 +1,19 @@ +# **App Name**: PromptVerse + +## Core Features: + +- Prompt Creation: Create and save new prompts with a user-friendly interface, offering options to format the prompt and add context notes. +- Prompt Organization: Organize prompts using tags and categories. Include hierarchical categories. +- Robust Search: Enable powerful search functionality to quickly find prompts by keywords, tags, or categories. +- One-Click Copy: Allow users to copy prompts to the clipboard with a single click for easy use in other applications. +- AI-Powered Tagging: Suggest relevant tags for the prompt using AI. The AI tool should extract important keywords from the prompt to improve searchability and organization. + +## Style Guidelines: + +- Primary color: Deep indigo (#4B0082) for a professional feel with a hint of creativity. +- Background color: Dark slate gray (#303030) to implement a sleek dark mode theme. +- Accent color: Electric purple (#BF40BF) for interactive elements and highlights to guide the user. +- Body and headline font: 'Inter' sans-serif for a modern, machined, objective, neutral look. +- Use minimalist icons that are easily recognizable on a dark background. Prefer sharp, geometric designs for a modern feel. +- Implement a grid-based layout, similar to Google Keep, for organizing prompts. Use subtle shadows to create a frosted glass effect (similar to Apple Vision Pro). Utilize a card-based interface for each prompt. +- Subtle animations on hover and click events. Smooth transitions between pages or sections. A brief 'copied to clipboard' confirmation that elegantly fades in and out when copying prompts. \ No newline at end of file diff --git a/src/ai/dev.ts b/src/ai/dev.ts index 51e556a..4f77a33 100644 --- a/src/ai/dev.ts +++ b/src/ai/dev.ts @@ -1 +1,4 @@ -// Flows will be imported for their side effects in this file. +import { config } from 'dotenv'; +config(); + +import '@/ai/flows/suggest-tags.ts'; \ No newline at end of file diff --git a/src/ai/flows/suggest-tags.ts b/src/ai/flows/suggest-tags.ts new file mode 100644 index 0000000..856bbb6 --- /dev/null +++ b/src/ai/flows/suggest-tags.ts @@ -0,0 +1,56 @@ +// This file uses server-side code. +'use server'; + +/** + * @fileOverview AI-powered tag suggestion for prompts. + * + * - suggestTags - A function that suggests relevant tags for a given prompt. + * - SuggestTagsInput - The input type for the suggestTags function. + * - SuggestTagsOutput - The return type for the suggestTags function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const SuggestTagsInputSchema = z.object({ + promptText: z + .string() + .describe('The text content of the prompt for which tags are to be suggested.'), +}); +export type SuggestTagsInput = z.infer; + +const SuggestTagsOutputSchema = z.object({ + tags: z + .array(z.string()) + .describe('An array of suggested tags for the given prompt.'), +}); +export type SuggestTagsOutput = z.infer; + +export async function suggestTags(input: SuggestTagsInput): Promise { + return suggestTagsFlow(input); +} + +const suggestTagsPrompt = ai.definePrompt({ + name: 'suggestTagsPrompt', + input: {schema: SuggestTagsInputSchema}, + output: {schema: SuggestTagsOutputSchema}, + prompt: `You are a prompt categorization expert. + + Given the following prompt, suggest 5 relevant tags that can be used to categorize and search for this prompt later. Return them as a simple array of strings. + + Prompt: {{{promptText}}} + + Tags:`, +}); + +const suggestTagsFlow = ai.defineFlow( + { + name: 'suggestTagsFlow', + inputSchema: SuggestTagsInputSchema, + outputSchema: SuggestTagsOutputSchema, + }, + async input => { + const {output} = await suggestTagsPrompt(input); + return output!; + } +); diff --git a/src/app/globals.css b/src/app/globals.css index a8144b6..e3cca7a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -14,67 +14,51 @@ body { --card-foreground: 0 0% 3.9%; --popover: 0 0% 100%; --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; + --primary: 300 50% 50%; --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; + --secondary: 275 100% 25.1%; + --secondary-foreground: 0 0% 98%; --muted: 0 0% 96.1%; --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; + --accent: 300 50% 60%; --accent-foreground: 0 0% 9%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 0 0% 89.8%; --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; + --ring: 300 50% 50%; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --radius: 0.5rem; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; } .dark { - --background: 0 0% 3.9%; + --background: 0 0% 18.8%; --foreground: 0 0% 98%; - --card: 0 0% 3.9%; + --card: 0 0% 22%; --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; + --popover: 0 0% 18.8%; --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; + --primary: 300 50% 50%; + --primary-foreground: 0 0% 98%; + --secondary: 275 100% 35%; --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; + --muted: 0 0% 25%; --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; + --accent: 300 50% 60%; --accent-foreground: 0 0% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; + --border: 0 0% 30%; + --input: 0 0% 25%; + --ring: 300 50% 60%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c81ce2d..4f5e03f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,9 +1,10 @@ import type {Metadata} from 'next'; import './globals.css'; +import { Toaster } from "@/components/ui/toaster"; export const metadata: Metadata = { - title: 'Firebase Studio App', - description: 'Generated by Firebase Studio', + title: 'PromptVerse', + description: 'An AI prompt management tool to create, organize, and share your best prompts.', }; export default function RootLayout({ @@ -12,13 +13,16 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + - - + + - {children} + + {children} + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 6ff5373..c9185ca 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,22 @@ -export default function Home() { - return <>; +import { Header } from '@/components/header'; +import { PromptList, PromptListSkeleton } from '@/components/prompt-list'; +import { Suspense } from 'react'; + +export default function Home({ + searchParams, +}: { + searchParams?: { q?: string }; +}) { + const query = searchParams?.q || ''; + + return ( +
+
+
+ }> + + +
+
+ ); } diff --git a/src/components/create-prompt-dialog.tsx b/src/components/create-prompt-dialog.tsx new file mode 100644 index 0000000..061130d --- /dev/null +++ b/src/components/create-prompt-dialog.tsx @@ -0,0 +1,168 @@ +'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 ( + + ); +} + +export function CreatePromptDialog() { + const [open, setOpen] = useState(false); + const [state, formAction] = useFormState(createPrompt, initialState); + const { toast } = useToast(); + const formRef = useRef(null); + + const [promptContent, setPromptContent] = useState(''); + const [tags, setTags] = useState([]); + 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) => { + 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 ( + + + + + + + Create a new prompt + + Craft your next masterpiece. Add notes and tags to keep it organized. + + +
+
+ + + {state.errors?.title &&

{state.errors.title[0]}

} +
+
+ +