├── public ├── vercelLogo.png ├── vercel.svg └── write.svg ├── postcss.config.js ├── next.config.js ├── @ ├── lib │ └── utils.ts └── components │ └── ui │ └── tabs.tsx ├── pages ├── _app.tsx ├── _document.tsx ├── api │ ├── claude.ts │ └── openai.ts └── index.tsx ├── components ├── SetupMarkdown.tsx ├── LoadingDots.tsx ├── Header.tsx ├── GitHub.tsx ├── CodeEditor.tsx ├── Footer.tsx ├── DropDown.tsx └── mdx-components.tsx ├── components.json ├── .gitignore ├── tsconfig.json ├── .vscode └── launch.json ├── LICENSE ├── styles ├── loading-dots.module.css └── globals.css ├── README.md ├── package.json ├── tailwind.config.js ├── utils └── OpenAIStream.ts └── .env.example /public/vercelLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XamHans/formulateflow/HEAD/public/vercelLogo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /@/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | import type { AppProps } from "next/app"; 3 | import "../styles/globals.css"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /components/SetupMarkdown.tsx: -------------------------------------------------------------------------------- 1 | import matter from "gray-matter"; 2 | import Markdown from "markdown-to-jsx"; 3 | 4 | export default function SetupMarkdown() { 5 | const mdx = matter("**TEST**"); 6 | console.log(mdx); 7 | return ( 8 |
9 |
10 | {mdx.content} 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/loading-dots.module.css'; 2 | 3 | const LoadingDots = ({ 4 | color = '#000', 5 | style = 'small', 6 | }: { 7 | color: string; 8 | style: string; 9 | }) => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default LoadingDots; 20 | 21 | LoadingDots.defaultProps = { 22 | style: 'small', 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | #other stuff 9 | cli-script 10 | codegen-crew 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | .pnpm-debug.log* 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | .env 42 | 43 | # idea 44 | .idea 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | ".next/types/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Github from "./GitHub"; 3 | 4 | export default function Header() { 5 | return ( 6 |
7 | 8 |

9 | FormulateFlow 10 |

11 | 12 | 18 | 19 |

Star on GitHub

20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node", 19 | "request": "launch", 20 | "program": "${workspaceFolder}/node_modules/.bin/next", 21 | "runtimeArgs": ["--inspect"], 22 | "skipFiles": ["/**"], 23 | "serverReadyAction": { 24 | "action": "debugWithEdge", 25 | "killOnServerStop": true, 26 | "pattern": "- Local:.+(https?://.+)", 27 | "uriFormat": "%s", 28 | "webRoot": "${workspaceFolder}" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /components/GitHub.tsx: -------------------------------------------------------------------------------- 1 | export default function Github({ className }: { className?: string }) { 2 | return ( 3 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2023 Hassan El Mghari 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /styles/loading-dots.module.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .loading .spacer { 7 | margin-right: 2px; 8 | } 9 | 10 | .loading span { 11 | animation-name: blink; 12 | animation-duration: 1.4s; 13 | animation-iteration-count: infinite; 14 | animation-fill-mode: both; 15 | width: 5px; 16 | height: 5px; 17 | border-radius: 50%; 18 | display: inline-block; 19 | margin: 0 1px; 20 | } 21 | 22 | .loading span:nth-of-type(2) { 23 | animation-delay: 0.2s; 24 | } 25 | 26 | .loading span:nth-of-type(3) { 27 | animation-delay: 0.4s; 28 | } 29 | 30 | .loading2 { 31 | display: inline-flex; 32 | align-items: center; 33 | } 34 | 35 | .loading2 .spacer { 36 | margin-right: 2px; 37 | } 38 | 39 | .loading2 span { 40 | animation-name: blink; 41 | animation-duration: 1.4s; 42 | animation-iteration-count: infinite; 43 | animation-fill-mode: both; 44 | width: 4px; 45 | height: 4px; 46 | border-radius: 50%; 47 | display: inline-block; 48 | margin: 0 1px; 49 | } 50 | 51 | .loading2 span:nth-of-type(2) { 52 | animation-delay: 0.2s; 53 | } 54 | 55 | .loading2 span:nth-of-type(3) { 56 | animation-delay: 0.4s; 57 | } 58 | 59 | @keyframes blink { 60 | 0% { 61 | opacity: 0.2; 62 | } 63 | 20% { 64 | opacity: 1; 65 | } 66 | 100% { 67 | opacity: 0.2; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 25 | 29 | 33 | 34 | 35 |
36 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | export default MyDocument; 44 | -------------------------------------------------------------------------------- /public/write.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiStepForm Generator - FormulateFlow 2 | 3 | ## Video Showcase: 4 | https://drive.google.com/file/d/1npAQ-kwl1h6pQrwQlZE3tJawE4BjafF3/view 5 | 6 | Ever wrestle with clunky multi-step forms in React? ‍ 7 | 8 | I know I did. That's why I built this open-source project: to streamline the process and make creating multi-step forms in Nextjs applications a breeze. 9 | 10 | I have fine-tuned Claude to understand the specific needs of multi-step forms within this tech stack. 11 | 12 | ## Tech Stack 13 | 14 | - **React Hook Form Integration**: Utilize the power of React Hook Form for handling form states and validations. 15 | - **Zustand for State Management**: Manage global state across different form steps effortlessly with Zustand. 16 | - **Dynamic Form Schema**: Define your form structure and validation using Zod. 17 | - **Styling with Shadcn**: Ensure your forms are not only functional but also visually appealing with Shadcn. 18 | 19 | ## Getting Started 20 | 21 | To get started with the MultiStepForm Generator, follow these steps: 22 | 23 | ### Installation 24 | 25 | Clone the repository: 26 | 27 | ```bash 28 | git clone https://github.com/XamHans/formulateflow 29 | cd formulateflow 30 | ``` 31 | 32 | Rename the .env.example to .env and place your claude api key 33 | 34 | ``` 35 | CLAUDE_API_KEY="" 36 | 37 | ``` 38 | 39 | ## Acknowledgments 40 | 41 | Special thanks to the [twitterbio](https://github.com/Nutlope/twitterbio) project which served as an inspiration and starting point for our MultiStepForm Generator. 42 | 43 | ``` 44 | 45 | ``` 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "@anthropic-ai/sdk": "^0.20.9", 10 | "@headlessui/react": "^1.7.17", 11 | "@headlessui/tailwindcss": "^0.1.2", 12 | "@heroicons/react": "^2.0.13", 13 | "@mdx-js/loader": "^3.0.1", 14 | "@mdx-js/react": "^3.0.1", 15 | "@monaco-editor/react": "^4.6.0", 16 | "@next/mdx": "^14.2.3", 17 | "@radix-ui/react-tabs": "^1.0.4", 18 | "@tailwindcss/forms": "^0.5.3", 19 | "@types/mdx": "^2.0.13", 20 | "@uiw/react-textarea-code-editor": "^3.0.2", 21 | "@vercel/analytics": "^0.1.8", 22 | "axios": "^1.6.8", 23 | "class-variance-authority": "^0.7.0", 24 | "clsx": "^2.1.1", 25 | "eventsource-parser": "^0.0.5", 26 | "gray-matter": "^4.0.3", 27 | "lucide-react": "^0.378.0", 28 | "markdown-to-jsx": "^7.4.7", 29 | "next": "^14.0.4", 30 | "next-mdx-remote": "^4.4.1", 31 | "openai": "^4.43.0", 32 | "react": "18.2.0", 33 | "react-codemirror2": "^8.0.0", 34 | "react-dom": "18.2.0", 35 | "react-hook-form": "^7.42.0", 36 | "react-hot-toast": "^2.4.0", 37 | "react-use-measure": "^2.1.1", 38 | "tailwind-merge": "^2.3.0", 39 | "tailwindcss-animate": "^1.0.7", 40 | "together-ai": "^0.5.2" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "18.11.3", 44 | "@types/react": "18.0.21", 45 | "@types/react-dom": "18.0.6", 46 | "autoprefixer": "^10.4.12", 47 | "postcss": "^8.4.18", 48 | "tailwindcss": "^3.2.4", 49 | "typescript": "4.9.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "@monaco-editor/react"; 2 | import React, { useEffect, useState } from "react"; 3 | import { 4 | Tabs, 5 | TabsContent, 6 | TabsList, 7 | TabsTrigger, 8 | } from "../@/components/ui/tabs"; 9 | 10 | export interface EditorInstance { 11 | id: number; 12 | label: string; 13 | content: string; 14 | } 15 | 16 | export interface MonacoEditorWithTabsProps { 17 | generatedFormCode: EditorInstance[]; 18 | } 19 | 20 | const MonacoEditorWithTabs: React.FC = ({ 21 | generatedFormCode, 22 | }) => { 23 | const [editorInstances, setEditorInstances] = useState([]); 24 | 25 | useEffect(() => { 26 | setEditorInstances(generatedFormCode); 27 | }, [generatedFormCode]); 28 | 29 | return ( 30 |
31 | 32 | 33 | {editorInstances?.map((instance) => ( 34 | 35 | {instance.label} 36 | 37 | ))} 38 | 39 | {editorInstances?.map((instance, index) => ( 40 | 45 | 51 | 52 | ))} 53 | 54 |
55 | ); 56 | }; 57 | 58 | export default MonacoEditorWithTabs; 59 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | 77 | 78 | } -------------------------------------------------------------------------------- /@/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "../../lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsContent, TabsList, TabsTrigger }; 56 | -------------------------------------------------------------------------------- /pages/api/claude.ts: -------------------------------------------------------------------------------- 1 | import Anthropic from "@anthropic-ai/sdk"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | // if (!process.env.OPENAI_API_KEY) { 5 | // throw new Error("Missing env var from OpenAI"); 6 | // } 7 | 8 | // export const config = { 9 | // runtime: "edge", 10 | // }; 11 | 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | const payload = req.body; 17 | const { userInput, userClaudeApiKey } = payload; 18 | 19 | if (!userInput) { 20 | return new Response("No userInput in the request", { status: 400 }); 21 | } 22 | 23 | // if (!userClaudeApiKey) { 24 | // userClaudeApiKey 25 | // return new Response("No userClaudeApiKey in the request", { status: 400 }); 26 | // } 27 | 28 | const generatedCode = await generateFormCode(userInput, userClaudeApiKey); 29 | 30 | return res.status(200).json(generatedCode); 31 | } 32 | 33 | const generateFormCode = async ( 34 | userInput: string, 35 | userClaudeApiKey: string 36 | ) => { 37 | const anthropic = new Anthropic({ 38 | apiKey: process.env.CLAUDE_API_KEY! ?? userClaudeApiKey, 39 | }); 40 | const msg = await anthropic.messages.create({ 41 | model: "claude-3-sonnet-20240229", 42 | max_tokens: 4000, 43 | temperature: 0, 44 | system: process.env.SYSTEM_PROMPT, 45 | messages: [ 46 | { 47 | role: "user", 48 | content: [ 49 | { 50 | type: "text", 51 | text: userInput, 52 | }, 53 | ], 54 | }, 55 | ], 56 | }); 57 | const responseMarkdown = msg.content[0].text; 58 | return convertResponseFromClaudeToJSON(responseMarkdown); 59 | }; 60 | 61 | const convertResponseFromClaudeToJSON = (responseMarkdown: string) => { 62 | try { 63 | const jsonStart = responseMarkdown.indexOf("```json\n") + 8; // Find the start of the JSON data 64 | const jsonEnd = responseMarkdown.lastIndexOf("```"); // Find the end of the JSON data 65 | const jsonString = responseMarkdown.slice(jsonStart, jsonEnd); // Extract the JSON data 66 | 67 | const responseJson = JSON.parse(jsonString); 68 | return responseJson; 69 | } catch (error) { 70 | throw new Error("Failed to parse JSON from Claude", error as any); 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | module.exports = { 4 | darkMode: ["class"], 5 | content: [ 6 | './@/**/*.{ts,tsx}', 7 | 8 | './pages/**/*.{ts,tsx}', 9 | './components/**/*.{ts,tsx}', 10 | './app/**/*.{ts,tsx}', 11 | './src/**/*.{ts,tsx}', 12 | ], 13 | prefix: "", 14 | theme: { 15 | container: { 16 | center: true, 17 | padding: "2rem", 18 | screens: { 19 | "2xl": "1400px", 20 | }, 21 | }, 22 | extend: { 23 | colors: { 24 | border: "hsl(var(--border))", 25 | input: "hsl(var(--input))", 26 | ring: "hsl(var(--ring))", 27 | background: "hsl(var(--background))", 28 | foreground: "hsl(var(--foreground))", 29 | primary: { 30 | DEFAULT: "hsl(var(--primary))", 31 | foreground: "hsl(var(--primary-foreground))", 32 | }, 33 | secondary: { 34 | DEFAULT: "hsl(var(--secondary))", 35 | foreground: "hsl(var(--secondary-foreground))", 36 | }, 37 | destructive: { 38 | DEFAULT: "hsl(var(--destructive))", 39 | foreground: "hsl(var(--destructive-foreground))", 40 | }, 41 | muted: { 42 | DEFAULT: "hsl(var(--muted))", 43 | foreground: "hsl(var(--muted-foreground))", 44 | }, 45 | accent: { 46 | DEFAULT: "hsl(var(--accent))", 47 | foreground: "hsl(var(--accent-foreground))", 48 | }, 49 | popover: { 50 | DEFAULT: "hsl(var(--popover))", 51 | foreground: "hsl(var(--popover-foreground))", 52 | }, 53 | card: { 54 | DEFAULT: "hsl(var(--card))", 55 | foreground: "hsl(var(--card-foreground))", 56 | }, 57 | }, 58 | borderRadius: { 59 | lg: "var(--radius)", 60 | md: "calc(var(--radius) - 2px)", 61 | sm: "calc(var(--radius) - 4px)", 62 | }, 63 | keyframes: { 64 | "accordion-down": { 65 | from: { height: "0" }, 66 | to: { height: "var(--radix-accordion-content-height)" }, 67 | }, 68 | "accordion-up": { 69 | from: { height: "var(--radix-accordion-content-height)" }, 70 | to: { height: "0" }, 71 | }, 72 | }, 73 | animation: { 74 | "accordion-down": "accordion-down 0.2s ease-out", 75 | "accordion-up": "accordion-up 0.2s ease-out", 76 | }, 77 | }, 78 | }, 79 | plugins: [require("tailwindcss-animate")], 80 | } -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 |
7 | 14 |
15 |
16 | 21 | 27 | 28 | 29 | 30 | 35 | 41 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /pages/api/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | // if (!process.env.OPENAI_API_KEY) { 4 | // throw new Error("Missing env var from OpenAI"); 5 | // } 6 | 7 | // export const config = { 8 | // runtime: "edge", 9 | // }; 10 | 11 | const openai = new OpenAI({ 12 | apiKey: process.env.OPENAI_API_KEY!, 13 | }); 14 | 15 | let thread = { 16 | id: "", 17 | }; 18 | 19 | let run: any; 20 | 21 | // export default async function handler( 22 | // req: NextApiRequest, 23 | // res: NextApiResponse 24 | // ) { 25 | // const payload = req.body; 26 | // const { userInput } = payload; 27 | 28 | // if (!userInput) { 29 | // return new Response("No userInput in the request", { status: 400 }); 30 | // } 31 | 32 | // run = createRunPerUser(userInput); 33 | 34 | // const generatedCode = await generateFormCode(); 35 | 36 | // return res.status(200).json({ generatedCode }); 37 | // } 38 | 39 | // // Function to create a new thread 40 | // const createRunPerUser = async (userInput: string) => { 41 | // if (!thread?.id) { 42 | // thread = await openai.beta.threads.create(); 43 | // } 44 | 45 | // await openai.beta.threads.messages.create(thread.id, { 46 | // role: "user", 47 | // content: userInput, 48 | // }); 49 | 50 | // run = await openai.beta.threads.runs.create(thread.id, { 51 | // assistant_id: process.env.ASSISTANT_ID!, 52 | // }); 53 | // }; 54 | 55 | // const checkRun = async () => { 56 | // if (!run) { 57 | // throw new Error("No run found, therefore no check run can be done"); 58 | // } 59 | // return new Promise((resolve, reject) => { 60 | // const interval = setInterval(async () => { 61 | // const retrieveRun = await openai.beta.threads.runs.retrieve( 62 | // thread.id, 63 | // run.id 64 | // ); 65 | 66 | // console.log("Run status: ", retrieveRun.status); 67 | 68 | // if (retrieveRun.status === "completed") { 69 | // console.log("Run completed: ", retrieveRun); 70 | 71 | // clearInterval(interval); 72 | // resolve(retrieveRun); 73 | // } 74 | // }, 3000); 75 | // }); 76 | // }; 77 | 78 | // const generateFormCode = async () => { 79 | // await checkRun(); 80 | 81 | // const messages = await openai.beta.threads.messages.list(thread.id); 82 | 83 | // const answers = (messages.data ?? []) 84 | // .filter((m) => m?.role === "assistant") 85 | // .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); 86 | 87 | // if (!answers || answers.length === 0) { 88 | // return "No answer found"; 89 | // } 90 | 91 | // return answers[0].content[0].text.value; 92 | // }; 93 | -------------------------------------------------------------------------------- /components/DropDown.tsx: -------------------------------------------------------------------------------- 1 | import { Menu, Transition } from '@headlessui/react'; 2 | import { 3 | CheckIcon, 4 | ChevronDownIcon, 5 | ChevronUpIcon, 6 | } from '@heroicons/react/20/solid'; 7 | import { Fragment } from 'react'; 8 | 9 | function classNames(...classes: string[]) { 10 | return classes.filter(Boolean).join(' '); 11 | } 12 | 13 | export type VibeType = 'Professional' | 'Casual' | 'Funny'; 14 | 15 | interface DropDownProps { 16 | vibe: VibeType; 17 | setVibe: (vibe: VibeType) => void; 18 | } 19 | 20 | let vibes: VibeType[] = ['Professional', 'Casual', 'Funny']; 21 | 22 | export default function DropDown({ vibe, setVibe }: DropDownProps) { 23 | return ( 24 | 25 |
26 | 27 | {vibe} 28 | 37 |
38 | 39 | 48 | 52 |
53 | {vibes.map((vibeItem) => ( 54 | 55 | {({ active }) => ( 56 | 69 | )} 70 | 71 | ))} 72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /utils/OpenAIStream.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createParser, 3 | ParsedEvent, 4 | ReconnectInterval, 5 | } from "eventsource-parser"; 6 | 7 | export type ChatGPTAgent = "user" | "system"; 8 | 9 | export interface ChatGPTMessage { 10 | role: ChatGPTAgent; 11 | content: string; 12 | } 13 | 14 | export interface OpenAIStreamPayload { 15 | model: string; 16 | messages: ChatGPTMessage[]; 17 | temperature: number; 18 | top_p: number; 19 | frequency_penalty: number; 20 | presence_penalty: number; 21 | max_tokens: number; 22 | stream: boolean; 23 | n: number; 24 | } 25 | 26 | export async function OpenAIStream(payload: OpenAIStreamPayload) { 27 | const encoder = new TextEncoder(); 28 | const decoder = new TextDecoder(); 29 | 30 | const res = await fetch("https://api.openai.com/v1/chat/completions", { 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}`, 34 | }, 35 | method: "POST", 36 | body: JSON.stringify(payload), 37 | }); 38 | 39 | const readableStream = new ReadableStream({ 40 | async start(controller) { 41 | // callback 42 | const onParse = (event: ParsedEvent | ReconnectInterval) => { 43 | if (event.type === "event") { 44 | const data = event.data; 45 | controller.enqueue(encoder.encode(data)); 46 | } 47 | } 48 | 49 | // optimistic error handling 50 | if (res.status !== 200) { 51 | const data = { 52 | status: res.status, 53 | statusText: res.statusText, 54 | body: await res.text(), 55 | } 56 | console.log(`Error: recieved non-200 status code, ${JSON.stringify(data)}`); 57 | controller.close(); 58 | return 59 | } 60 | 61 | // stream response (SSE) from OpenAI may be fragmented into multiple chunks 62 | // this ensures we properly read chunks and invoke an event for each SSE event stream 63 | const parser = createParser(onParse); 64 | // https://web.dev/streams/#asynchronous-iteration 65 | for await (const chunk of res.body as any) { 66 | parser.feed(decoder.decode(chunk)); 67 | } 68 | }, 69 | }); 70 | 71 | let counter = 0; 72 | const transformStream = new TransformStream({ 73 | async transform(chunk, controller) { 74 | const data = decoder.decode(chunk); 75 | // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream 76 | if (data === "[DONE]") { 77 | controller.terminate(); 78 | return; 79 | } 80 | try { 81 | const json = JSON.parse(data); 82 | const text = json.choices[0].delta?.content || ""; 83 | if (counter < 2 && (text.match(/\n/) || []).length) { 84 | // this is a prefix character (i.e., "\n\n"), do nothing 85 | return; 86 | } 87 | // stream transformed JSON resposne as SSE 88 | const payload = {text: text}; 89 | // https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format 90 | controller.enqueue( 91 | encoder.encode(`data: ${JSON.stringify(payload)}\n\n`) 92 | ); 93 | counter++; 94 | } catch (e) { 95 | // maybe parse error 96 | controller.error(e); 97 | } 98 | }, 99 | }); 100 | 101 | return readableStream.pipeThrough(transformStream); 102 | } 103 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | 2 | CLAUDE_API_KEY="" 3 | SYSTEM_PROMPT= "You are an experienced web developer, you are proficient in the following technologies:\n\n- **Next.js 14** with App Router\n- **Shadcn Components** library\n- Server actions for asynchronous functions on the server\n- **Zod** and **React Hook Form** for handling forms and input validation\n- **TailwindCSS** for styling\n- **Typescript** for type safety\n\nYour expertise extends to developing complex form wizards. Your task is to construct a wrapper component that encompasses all sub-orchestrates of the wizard form. Each new page within the wizard should be encapsulated within a standalone component. To facilitate state sharing between pages, utilize the **Zustand** store and ensure consumption on each page. Make sure that inside the store you store the current step of the wizard steps, also you don\'t need to define for every attribute a key, instead make it generic like this\n````\nexport const useOnboardingStore = create((set, get) => ({\n data: {},\n setData: (key, value) => set((state) => ({ data: { ...state.data, [key]: value } })),\n currentStep: 0,\n setCurrentStep: (step) => set({ currentStep: step }),\n \n}));\n```\n Make sure that you import the setCurrentStep from useOnboardingStore and
from @/components/ui/form. To define the data capture requirements for each page, create a Zod schema accordingly. It\'s imperative to exclusively utilize **Shadcn components** throughout the implementation. Where you then just use the key to set the value like this\n``` \n//usage inside of form submit of wizard page \n const onFormSubmit = async (data: FormData) => { setData(`personalInfo,` data); setCurrentStep(currentStep + 1); } \n```\n\n | Create the wizard pages like this: 'use client'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { zodResolver } from '@hookform/resolvers/zod'; import React from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import OnboardingNavigation from './navigation-btns'; import { useOnboardingStore } from './store'; const schema = z.object({ email: z.string().email('Invalid email address'), profession: z.string().min(1, 'Profession is required'), }); type FormData = z.infer; const WizardPage2: React.FC = () => { const { setData, currentStep, setCurrentStep } = useOnboardingStore(); const form = useForm({ resolver: zodResolver(schema), defaultValues: { email: ', profession: ', }, }); const onFormSubmit = async (data: FormData) => { setData('professionalInfo', data); setCurrentStep(currentStep + 1); }; return ( ( Email Enter your email address. )} /> ( Profession Enter your profession or job title. )} /> ); }; export default WizardPage2; | Make sure you retrun everthing in a json format like this so i can parse it : \n[\n {\n id: 0,\n label: store.ts,\n content:\n your typescript content for store,\n },\n {\n id: 1,\n label: WizardWrapper.tsx,\n content:\n your tsx content for wizard wrapper,\n },\n {\n id: 2,\n label: navigation-btns.tsx,\n content:\n your tsx content for navigation buttons,\n },\n {\n id: 3,\n label: WizardPage.tsx,\n content:\n your tsx content for first form page,\n },\n {\n id: 4,\n label: WizardPage2.tsx,\n content: your tsx format for second form page.,\n },\n {\n ... if more pages needed like object before but increasing id\n },\n ]); Create a component called navigation-buttons.tsx that contains code like this: \n import React from react; import Button from ~/core/ui/Button; import { useOnboardingStore } from ../store; const OnboardingNavigation: React.FC = ({}) => { const { setCurrentStep, currentStep } = useOnboardingStore(); return (
); }; export default OnboardingNavigation; Import that component in every wizard page to handle the navigation so you dont need to use it inside the wrapper only consume the current step and show the page accordingly. Make sure you import the shadcn components from @/components/ui/\, like this: import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from @/components/ui/form; import { Input } from @/components/ui/input; Make all components to client components by adding: 'use client'; in the very first line above all the imports like this: 1) 'use client'; 2) import { zodResolver } from '@hookform/resolvers/zod'; 3) ... and so on. DO NOT output any additional text outside of the JSON, do only return JSON that is valid and I can parse back;" -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Head from "next/head"; 3 | import Image from "next/image"; 4 | import { useState } from "react"; 5 | import MonacoEditorWithTabs, { EditorInstance } from "../components/CodeEditor"; 6 | import Footer from "../components/Footer"; 7 | import Header from "../components/Header"; 8 | import LoadingDots from "../components/LoadingDots"; 9 | 10 | const Home: NextPage = () => { 11 | const [loading, setLoading] = useState(false); 12 | const [generatedFormCode, setGeneratedFormCode] = useState( 13 | [] 14 | ); 15 | 16 | const [userInput, setUserInput] = useState( 17 | "Create me a form that captures users firstname, lastname and email. On the second page capture the email and profession." 18 | ); 19 | 20 | const [userClaudeApiKey, setUserClaudeApiKey] = useState(""); 21 | 22 | const generateFormCode = async (e: any) => { 23 | e.preventDefault(); 24 | 25 | setLoading(true); 26 | 27 | const response = await fetch("/api/claude", { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | }, 32 | body: JSON.stringify({ 33 | userInput: userInput, 34 | userClaudeApiKey: userClaudeApiKey, 35 | }), 36 | }); 37 | 38 | if (!response.ok) { 39 | console.error(response); 40 | throw new Error(response.statusText); 41 | } 42 | 43 | const generatedCode = await response.json(); 44 | setGeneratedFormCode(generatedCode); 45 | setLoading(false); 46 | }; 47 | 48 | return ( 49 |
50 | 51 | Form Wizard Generator 52 | 53 | 54 | 55 |
56 |
57 |

58 | nextjs14 + shadcn + zod + zustand + react-hook-form 59 |

60 |

61 | Generate your Multi-Page Form form in seconds 62 |

63 | 64 |
65 |
66 | 1 icon 73 |

74 | Drop in your form requirements and let the AI do the heavy 75 | lifting. Be as detailed as possible how many pages you want and 76 | what information a page should capture. 77 |

78 |
79 |