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..c120173 --- /dev/null +++ b/docs/blueprint.md @@ -0,0 +1,22 @@ +# **App Name**: ContentFlow + +## Core Features: + +- User Authentication: Login and authentication using email or social accounts (Firebase/Auth.js). +- Briefing Form: Briefing form for users to input their content requirements; an AI chat with OpenRouter will come later. +- Content Plan Generation: AI-powered tool to generate a content plan based on the user's brief via a webhook that connects to n8n and GPT. +- Content Plan Editor: Display the AI-generated content plan for user review and edits. +- Content Generation: AI tool to generate content based on the approved content plan via n8n. +- Content Editor: Content preview and editor for the generated content, so the user can approve and modify. +- Content Export: Export content plan and generated content as a PDF or ZIP file. +- Storage: Firestore is used to store all brief data, plans, and generated content in a structured, user-specific hierarchy. + +## Style Guidelines: + +- Primary color: Deep indigo (#3F51B5), a professional and calming color to reflect the AI's ability to focus. +- Background color: Very light grey (#F0F2F5), a neutral backdrop that keeps the interface feeling airy. +- Accent color: Electric purple (#BF5AF2), adding a zing that evokes the spark of creativity. +- Body and headline font: 'Inter' sans-serif for a modern, neutral look; suitable for both headlines and body text. +- Code font: 'Source Code Pro' for any code snippets displayed. +- Simple and modern icons from Headless UI and ShadCN to maintain a clean interface. +- Subtle transitions and progress bars to provide feedback during AI operations and content loading. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c67c9cc..2c5f76f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "dotenv": "^16.5.0", "embla-carousel-react": "^8.6.0", "firebase": "^11.9.1", + "framer-motion": "^11.3.12", "genkit": "^1.13.0", "lucide-react": "^0.475.0", "next": "15.3.3", @@ -5958,6 +5959,33 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -7330,6 +7358,21 @@ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index 575d1cc..de7ad28 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dotenv": "^16.5.0", "embla-carousel-react": "^8.6.0", "firebase": "^11.9.1", + "framer-motion": "^11.3.12", "genkit": "^1.13.0", "lucide-react": "^0.475.0", "next": "15.3.3", diff --git a/src/ai/dev.ts b/src/ai/dev.ts index 51e556a..379a0a8 100644 --- a/src/ai/dev.ts +++ b/src/ai/dev.ts @@ -1 +1,5 @@ -// Flows will be imported for their side effects in this file. +import { config } from 'dotenv'; +config(); + +import '@/ai/flows/generate-content.ts'; +import '@/ai/flows/generate-content-plan.ts'; \ No newline at end of file diff --git a/src/ai/flows/generate-content-plan.ts b/src/ai/flows/generate-content-plan.ts new file mode 100644 index 0000000..e9c432a --- /dev/null +++ b/src/ai/flows/generate-content-plan.ts @@ -0,0 +1,47 @@ +'use server'; + +/** + * @fileOverview AI flow to generate a content plan based on user brief. + * + * - generateContentPlan - The main function to generate a content plan. + * - GenerateContentPlanInput - The input type for the generateContentPlan function. + * - GenerateContentPlanOutput - The output type for the generateContentPlan function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const GenerateContentPlanInputSchema = z.object({ + brief: z.string().describe('The content brief provided by the user.'), +}); +export type GenerateContentPlanInput = z.infer; + +const GenerateContentPlanOutputSchema = z.object({ + contentPlan: z.string().describe('The generated content plan.'), +}); +export type GenerateContentPlanOutput = z.infer; + +export async function generateContentPlan(input: GenerateContentPlanInput): Promise { + return generateContentPlanFlow(input); +} + +const generateContentPlanPrompt = ai.definePrompt({ + name: 'generateContentPlanPrompt', + input: {schema: GenerateContentPlanInputSchema}, + output: {schema: GenerateContentPlanOutputSchema}, + prompt: `You are an AI content planning assistant. Generate a detailed content plan based on the user's brief. + +Brief: {{{brief}}}`, +}); + +const generateContentPlanFlow = ai.defineFlow( + { + name: 'generateContentPlanFlow', + inputSchema: GenerateContentPlanInputSchema, + outputSchema: GenerateContentPlanOutputSchema, + }, + async input => { + const {output} = await generateContentPlanPrompt(input); + return output!; + } +); diff --git a/src/ai/flows/generate-content.ts b/src/ai/flows/generate-content.ts new file mode 100644 index 0000000..385529a --- /dev/null +++ b/src/ai/flows/generate-content.ts @@ -0,0 +1,49 @@ +'use server'; + +/** + * @fileOverview Generates content based on an approved content plan. + * + * - generateContent - A function that generates content from a content plan. + * - GenerateContentInput - The input type for the generateContent function. + * - GenerateContentOutput - The return type for the generateContent function. + */ + +import {ai} from '@/ai/genkit'; +import {z} from 'genkit'; + +const GenerateContentInputSchema = z.object({ + contentPlan: z + .string() + .describe('The approved content plan to generate content from.'), +}); +export type GenerateContentInput = z.infer; + +const GenerateContentOutputSchema = z.object({ + generatedContent: z + .string() + .describe('The content generated based on the content plan.'), +}); +export type GenerateContentOutput = z.infer; + +export async function generateContent(input: GenerateContentInput): Promise { + return generateContentFlow(input); +} + +const prompt = ai.definePrompt({ + name: 'generateContentPrompt', + input: {schema: GenerateContentInputSchema}, + output: {schema: GenerateContentOutputSchema}, + prompt: `You are an AI content generator. Generate content based on the following content plan:\n\nContent Plan: {{{contentPlan}}}`, +}); + +const generateContentFlow = ai.defineFlow( + { + name: 'generateContentFlow', + inputSchema: GenerateContentInputSchema, + outputSchema: GenerateContentOutputSchema, + }, + async input => { + const {output} = await prompt(input); + return output!; + } +); diff --git a/src/app/dashboard/create/components/brief-form.tsx b/src/app/dashboard/create/components/brief-form.tsx new file mode 100644 index 0000000..5af52d6 --- /dev/null +++ b/src/app/dashboard/create/components/brief-form.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; +import { BotMessageSquare } from 'lucide-react'; + +const briefSchema = z.object({ + title: z.string().min(3, 'Title must be at least 3 characters long.'), + goal: z.string().min(10, 'Content goal must be at least 10 characters long.'), + audience: z.string().min(3, 'Target audience must be at least 3 characters long.'), + points: z.string().min(10, 'Please provide some key points.'), + tone: z.string().min(3, 'Tone of voice must be at least 3 characters long.'), + keywords: z.string().optional(), +}); + +export type BriefData = z.infer; + +interface BriefFormProps { + onSubmit: (data: BriefData) => void; + isLoading: boolean; +} + +export function BriefForm({ onSubmit, isLoading }: BriefFormProps) { + const form = useForm({ + resolver: zodResolver(briefSchema), + defaultValues: { + title: '', + goal: '', + audience: '', + points: '', + tone: '', + keywords: '', + }, + }); + + return ( + + + Content Brief + Fill out the details below to give the AI context for your content. + + +
+ + ( + + Project Title + + + + + + )} + /> + ( + + What is the primary goal of this content? + + + + + + )} + /> + ( + + Who is the target audience? + + + + + + )} + /> + ( + + Key Talking Points + +