├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── drizzle.config.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg ├── s1.png ├── s2.png ├── s3.png ├── s4.png ├── s5.png └── vercel.svg ├── src ├── app │ ├── (admin) │ │ ├── layout.tsx │ │ ├── results │ │ │ ├── FormPicker.tsx │ │ │ ├── Res.tsx │ │ │ ├── ResultsDisplay.tsx │ │ │ └── page.tsx │ │ └── view-forms │ │ │ └── page.tsx │ ├── actions │ │ ├── generateForm.ts │ │ ├── getSubmissons.ts │ │ ├── getUserForms.ts │ │ ├── mutateForm.ts │ │ ├── navigateToForm.ts │ │ └── theme.ts │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── form │ │ │ └── new │ │ │ └── route.ts │ ├── favicon.ico │ ├── form-generator │ │ └── index.tsx │ ├── forms │ │ ├── Form.tsx │ │ ├── FormField.tsx │ │ ├── FormList.tsx │ │ ├── FormPublishSucces.tsx │ │ ├── [formId] │ │ │ ├── page.tsx │ │ │ └── success │ │ │ │ └── page.tsx │ │ ├── edit │ │ │ └── [formId] │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── globals.css │ ├── landing-page │ │ └── index.tsx │ ├── layout.tsx │ ├── page.tsx │ └── utils │ │ └── cn.ts ├── auth.ts ├── components │ ├── Step.tsx │ ├── icons.tsx │ ├── navigation │ │ └── navbar.tsx │ └── ui │ │ ├── ThemeChange.tsx │ │ ├── alert.tsx │ │ ├── background-beams.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ ├── header.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── sticky-scroll-reveal.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── db │ ├── index.ts │ └── schema.ts ├── lib │ └── utils.ts └── types │ ├── form-types.d.ts │ └── nav-types.d.ts ├── tailwind.config.ts ├── tsconfig.json └── versel.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Form Builder 2 | 3 | This is an AI-powered form builder application built using Next.js, Next-auth, Shadcn UI, Gemini AI API, Drizzle, PostgreSQL, and TypeScript. With this application, users can easily create customized forms by providing prompts, and the forms can be published for others to fill out. The admin of the form can access and view all responses submitted. 4 | 5 | ## Features 6 | 7 | - **AI-Powered Form Creation**: Users can create forms by simply providing prompts, and the AI generates the necessary form fields based on the prompts. 8 | - **Authentication**: Next-auth is integrated for secure authentication, allowing users to sign up, sign in, and manage their accounts. 9 | - **Responsive Design:** Shadcn UI ensures that the application is responsive and looks great across various devices and screen sizes. 10 | - **Admin Dashboard**: Admin users have access to a dashboard where they can view all responses submitted to their forms. 11 | - **Persistent Data Storage**: PostgreSQL is used as the database to store form configurations, user information, and form responses securely. 12 | - **Theme Customization**: The app offers six different themes for users to choose from, allowing them to customize the appearance of their app interface. 13 | - **Type Safety**: TypeScript is employed throughout the project to provide type safety and enhance code maintainability. 14 | 15 | ### Technologies Used 16 | 17 | - **Next.js**: A React framework for building server-side rendered (SSR) and statically generated web applications. 18 | - **Next-auth**: A complete open-source authentication solution for Next.js applications. 19 | - **Shadcn UI**: A UI framework for building beautiful, responsive web interfaces. 20 | - **Gemini AI API**: An API for integrating artificial intelligence capabilities into applications. 21 | - **Drizzle ORM**: ORM used for object-relational mapping, simplifying database interactions and management. 22 | - **PostgreSQ**L: A powerful, open-source relational database system. 23 | - **TypeScript**: A statically typed superset of JavaScript that provides type safety and enhance code maintainability. 24 | 25 | ### Get Started 26 | 1. **Clone the repository:** 27 | 28 | ```bash 29 | git clone https://github.com/harshxraj/ai-form-builder.git 30 | ``` 31 | 32 | 2. **Install Dependencies:** 33 | 34 | ```bash 35 | cd your_repo 36 | npm intall 37 | ``` 38 | 39 | 3. **Set Up Environment Variables:** 40 | 41 | ```bash 42 | GEMINI_API_KEY= 43 | GOOGLE_CLIENT_ID= 44 | GOOGLE_CLIENT_SECRET= 45 | AUTH_SECRET= 46 | DATABASE_URL= 47 | ``` 48 | 49 | ## Preview 50 | https://github.com/harshxraj/ai-form-builder/assets/128404446/bc419920-edcb-4aa6-baa2-79fc2e4c360b 51 | 52 | ## Dashboard 53 | ![Screenshot 2024-03-17 181850](https://github.com/harshxraj/ai-form-builder/assets/128404446/5b384e5a-6cc6-4120-945c-b6471a9717f0) 54 | 55 | 56 | ## Results Page 57 | ![Screenshot 2024-03-17 183821](https://github.com/harshxraj/ai-form-builder/assets/128404446/14e8492c-07e4-4a8d-a82e-18c5df1c0d86) 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | 3 | export default { 4 | schema: "./src/db/schema.ts", 5 | out: "./drizzle", 6 | driver: "pg", 7 | dbCredentials: { 8 | connectionString: 9 | process.env.DATABASE_URL || 10 | "postgres://postgres:postgres@localhost:5432/postgres", 11 | }, 12 | } satisfies Config; 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "lh3.googleusercontent.com", 8 | port: "", 9 | pathname: "/a/**", 10 | }, 11 | ], 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-form-builder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@auth/drizzle-adapter": "^0.7.0", 13 | "@google/generative-ai": "^0.2.1", 14 | "@hookform/resolvers": "^3.3.4", 15 | "@nextui-org/react": "^2.2.9", 16 | "@radix-ui/react-dialog": "^1.0.5", 17 | "@radix-ui/react-icons": "^1.3.0", 18 | "@radix-ui/react-label": "^2.0.2", 19 | "@radix-ui/react-menubar": "^1.0.4", 20 | "@radix-ui/react-radio-group": "^1.1.3", 21 | "@radix-ui/react-select": "^2.0.0", 22 | "@radix-ui/react-slot": "^1.0.2", 23 | "@radix-ui/react-switch": "^1.0.3", 24 | "@radix-ui/react-tabs": "^1.0.4", 25 | "@radix-ui/react-toast": "^1.1.5", 26 | "@radix-ui/react-tooltip": "^1.0.7", 27 | "add": "^2.0.6", 28 | "axios": "^1.6.7", 29 | "class-variance-authority": "^0.7.0", 30 | "clsx": "^2.1.0", 31 | "drizzle-orm": "^0.29.4", 32 | "framer-motion": "^11.0.8", 33 | "lucide-react": "^0.339.0", 34 | "menubar": "^9.4.0", 35 | "next": "14.0.4", 36 | "next-auth": "^5.0.0-beta.13", 37 | "npx": "^10.2.2", 38 | "pg": "^8.11.3", 39 | "postgres": "^3.4.3", 40 | "react": "^18", 41 | "react-dom": "^18", 42 | "react-hook-form": "^7.50.1", 43 | "shadcn-ui": "^0.8.0", 44 | "tailwind-merge": "^2.2.1", 45 | "tailwindcss-animate": "^1.0.7", 46 | "uuid": "^9.0.1", 47 | "zod": "^3.22.4" 48 | }, 49 | "devDependencies": { 50 | "@types/node": "^20", 51 | "@types/react": "^18", 52 | "@types/react-dom": "^18", 53 | "@types/uuid": "^9.0.8", 54 | "autoprefixer": "^10.0.1", 55 | "drizzle-kit": "^0.20.14", 56 | "eslint": "^8", 57 | "eslint-config-next": "14.0.4", 58 | "postcss": "^8", 59 | "tailwindcss": "^3.3.0", 60 | "typescript": "^5" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/s1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshxraj/ai-form-builder/ab614ea87ebc8f541522cf8acc6dd8b1a7a7a96d/public/s1.png -------------------------------------------------------------------------------- /public/s2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshxraj/ai-form-builder/ab614ea87ebc8f541522cf8acc6dd8b1a7a7a96d/public/s2.png -------------------------------------------------------------------------------- /public/s3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshxraj/ai-form-builder/ab614ea87ebc8f541522cf8acc6dd8b1a7a7a96d/public/s3.png -------------------------------------------------------------------------------- /public/s4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshxraj/ai-form-builder/ab614ea87ebc8f541522cf8acc6dd8b1a7a7a96d/public/s4.png -------------------------------------------------------------------------------- /public/s5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshxraj/ai-form-builder/ab614ea87ebc8f541522cf8acc6dd8b1a7a7a96d/public/s5.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Header from "@/components/ui/header"; 2 | import DashboardNav from "@/components/navigation/navbar"; 3 | import { SessionProvider } from "next-auth/react"; 4 | import FormGenerator from "../form-generator"; 5 | import { SidebarNavItem } from "@/types/nav-types"; 6 | 7 | export default function AdminLayout({ 8 | children, 9 | }: { 10 | children: React.ReactNode; 11 | }) { 12 | const dashboardConfig: { 13 | sidebarNav: SidebarNavItem[]; 14 | } = { 15 | sidebarNav: [ 16 | { 17 | title: "My Forms", 18 | href: "/view-forms", 19 | icon: "library", 20 | }, 21 | { 22 | title: "Results", 23 | href: "/results", 24 | icon: "list", 25 | }, 26 | { 27 | title: "Analytics", 28 | href: "/analytics", 29 | icon: "lineChart", 30 | }, 31 | { 32 | title: "Charts", 33 | href: "/charts", 34 | icon: "pieChart", 35 | }, 36 | { 37 | title: "Settings", 38 | href: "/settings", 39 | icon: "settings", 40 | }, 41 | ], 42 | }; 43 | return ( 44 |
45 |
46 |
47 | 50 |
51 |
52 |

Dashboard

53 | 54 | 55 | 56 |
57 |
58 | {children} 59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/app/(admin)/results/FormPicker.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { use, useCallback, useEffect } from "react"; 3 | import { 4 | Select, 5 | SelectContent, 6 | SelectGroup, 7 | SelectItem, 8 | SelectTrigger, 9 | SelectValue, 10 | } from "@/components/ui/select"; 11 | import { 12 | forms, 13 | answers, 14 | formSubmissions, 15 | questions, 16 | fieldOptions, 17 | } from "@/db/schema"; 18 | import { Label } from "@/components/ui/label"; 19 | import { useSearchParams, useRouter, usePathname } from "next/navigation"; 20 | import { InferSelectModel } from "drizzle-orm"; 21 | import { getSubmissions } from "@/app/actions/getSubmissons"; 22 | import { cpSync } from "fs"; 23 | 24 | type SelectProps = { 25 | value: number; 26 | label?: string | null; 27 | }; 28 | type FieldOption = InferSelectModel; 29 | 30 | type Answer = InferSelectModel & { 31 | fieldOption?: FieldOption | null; 32 | }; 33 | 34 | type Question = InferSelectModel & { 35 | fieldOptions: FieldOption[]; 36 | }; 37 | 38 | type FormSubmission = InferSelectModel & { 39 | answers: Answer[]; 40 | }; 41 | 42 | export type Form = 43 | | (InferSelectModel & { 44 | questions: Question[]; 45 | submissions: FormSubmission[]; 46 | }) 47 | | undefined; 48 | 49 | interface TableProps { 50 | data: FormSubmission[]; 51 | columns: Question[]; 52 | } 53 | type FormsPickerProps = { 54 | options: Array; 55 | setData: React.Dispatch>; 56 | setCols: React.Dispatch>; 57 | setRows: React.Dispatch>; 58 | }; 59 | 60 | const FormsPicker = (props: FormsPickerProps) => { 61 | const { options, setData, setCols, setRows } = props; 62 | 63 | const searchParams = useSearchParams(); 64 | const router = useRouter(); 65 | const pathname = usePathname(); 66 | 67 | const formId = searchParams.get("formId") || options[0].value.toString(); 68 | 69 | const createQueryString = useCallback( 70 | (name: string, value: string) => { 71 | console.log("searchParams", searchParams); 72 | const params = new URLSearchParams(searchParams.toString()); 73 | params.set(name, value); 74 | 75 | return params.toString(); 76 | }, 77 | [searchParams] 78 | ); 79 | 80 | // useEffect(() => { 81 | // const fetchData = async () => { 82 | // try { 83 | // const res = await getSubmissions(51); // Call your API function here 84 | // if (res) { 85 | // const transformedData: TableProps = { 86 | // data: res.submissions, 87 | // columns: res.questions, 88 | // }; 89 | // setCols(res.questions); 90 | // setRows(res.submissions); 91 | // setData(transformedData); 92 | // } 93 | // } catch (error) { 94 | // console.error("Error fetching data:", error); 95 | // } 96 | // }; 97 | 98 | // fetchData(); 99 | // }, []); 100 | const fetchData = async (formId: number) => { 101 | try { 102 | const res = await getSubmissions(formId); 103 | if (res) { 104 | const transformedData: TableProps = { 105 | data: res.submissions, 106 | columns: res.questions, 107 | }; 108 | setCols(res.questions); 109 | setRows(res.submissions); 110 | setData(transformedData); 111 | } 112 | console.log("respnose", res); 113 | } catch (error) { 114 | console.error("Error fetching data:", error); 115 | } 116 | }; 117 | useEffect(() => { 118 | fetchData(Number(formId)); 119 | }, [formId]); // Include formId in the dependency array 120 | 121 | return ( 122 |
123 | 124 | 144 |
145 | ); 146 | }; 147 | 148 | export default FormsPicker; 149 | -------------------------------------------------------------------------------- /src/app/(admin)/results/Res.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | Table, 4 | TableHeader, 5 | TableColumn, 6 | TableBody, 7 | TableRow, 8 | TableCell, 9 | } from "@nextui-org/react"; 10 | import { 11 | Tooltip, 12 | TooltipContent, 13 | TooltipProvider, 14 | TooltipTrigger, 15 | } from "@/components/ui/tooltip"; 16 | import ResultsDisplay from "./ResultsDisplay"; 17 | import { db } from "@/db"; 18 | import { eq } from "drizzle-orm"; 19 | import { getSubmissions } from "@/app/actions/getSubmissons"; 20 | import { InferSelectModel } from "drizzle-orm"; 21 | import { 22 | forms, 23 | answers, 24 | formSubmissions, 25 | questions, 26 | fieldOptions, 27 | } from "@/db/schema"; 28 | import { Columns } from "lucide-react"; 29 | 30 | type FieldOption = InferSelectModel; 31 | 32 | type Answer = InferSelectModel & { 33 | fieldOption?: FieldOption | null; 34 | }; 35 | 36 | type Question = InferSelectModel & { 37 | fieldOptions: FieldOption[]; 38 | }; 39 | 40 | type FormSubmission = InferSelectModel & { 41 | answers: Answer[]; 42 | }; 43 | 44 | export type Form = 45 | | (InferSelectModel & { 46 | questions: Question[]; 47 | submissions: FormSubmission[]; 48 | }) 49 | | undefined; 50 | 51 | interface TableProps { 52 | data: FormSubmission[]; 53 | columns: Question[]; 54 | } 55 | interface ResProps { 56 | data: TableProps | null; 57 | cols: Question[] | null; 58 | rows: FormSubmission[] | null; 59 | } 60 | const Res: React.FC = ({ data, cols, rows }) => { 61 | return ( 62 |
63 | {data && cols && ( 64 | 65 | 66 | {(column) => ( 67 | 68 | 69 | 70 | 71 |
79 | {column.text} 80 |
81 |
82 | 83 |

{column.text}

84 |
85 |
86 |
87 |
88 | )} 89 |
90 | 91 | {(rows || []).map((row, index) => ( 92 | 93 | {row.answers.map((answer, idx) => ( 94 | 95 |
96 | {answer.value == null 97 | ? answer?.fieldOption?.text 98 | : answer.value} 99 |
100 |
101 | ))} 102 |
103 | ))} 104 |
105 |
106 | )} 107 |
108 | ); 109 | }; 110 | 111 | export default Res; 112 | -------------------------------------------------------------------------------- /src/app/(admin)/results/ResultsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { db } from "@/db"; 3 | import { eq } from "drizzle-orm"; 4 | import { forms } from "@/db/schema"; 5 | 6 | type Props = { 7 | formId: number; 8 | }; 9 | 10 | const ResultsDisplay = async ({ formId }: Props) => { 11 | const form = await db.query.forms.findFirst({ 12 | where: eq(forms.id, formId), 13 | with: { 14 | questions: { 15 | with: { 16 | fieldOptions: true, 17 | }, 18 | }, 19 | submissions: { 20 | with: { 21 | answers: { 22 | with: { 23 | fieldOption: true, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }); 30 | 31 | if (!form) return null; 32 | if (!form.submissions) return

No submissions on this form yet!

; 33 | console.log("form", form); 34 | return
; 35 | }; 36 | 37 | export default ResultsDisplay; 38 | -------------------------------------------------------------------------------- /src/app/(admin)/results/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import { NextUIProvider } from "@nextui-org/react"; 5 | import Res from "./Res"; 6 | import { getUserForms } from "@/app/actions/getUserForms"; 7 | import FormsPicker from "./FormPicker"; 8 | import { InferSelectModel } from "drizzle-orm"; 9 | import { 10 | forms, 11 | answers, 12 | formSubmissions, 13 | questions, 14 | fieldOptions, 15 | } from "@/db/schema"; 16 | 17 | type Props = {}; 18 | type FieldOption = InferSelectModel; 19 | 20 | type Answer = InferSelectModel & { 21 | fieldOption?: FieldOption | null; 22 | }; 23 | 24 | type Question = InferSelectModel & { 25 | fieldOptions: FieldOption[]; 26 | }; 27 | 28 | type FormSubmission = InferSelectModel & { 29 | answers: Answer[]; 30 | }; 31 | 32 | export type Form = 33 | | (InferSelectModel & { 34 | questions: Question[]; 35 | submissions: FormSubmission[]; 36 | }) 37 | | undefined; 38 | 39 | interface TableProps { 40 | data: FormSubmission[]; 41 | columns: Question[]; 42 | } 43 | 44 | const Page = (props: Props) => { 45 | const [selectOptions, setSelectOptions] = useState< 46 | { label: string; value: number }[] 47 | >([]); 48 | const [data, setData] = useState(null); 49 | const [cols, setCols] = useState(null); 50 | const [rows, setRows] = useState(null); 51 | 52 | useEffect(() => { 53 | const fetchForms = async () => { 54 | try { 55 | const userForms: Array> = 56 | await getUserForms(); 57 | const options: { label: string; value: number }[] = userForms 58 | .filter((form) => form.name !== null) // Filter out forms with null names 59 | .map((form) => ({ 60 | label: form.name!, 61 | value: form.id, 62 | })); 63 | setSelectOptions(options); 64 | console.log("userForms", userForms); 65 | } catch (err) { 66 | console.log(err); 67 | } 68 | }; 69 | 70 | fetchForms(); 71 | }, []); 72 | 73 | return ( 74 | 75 | {selectOptions.length > 0 && ( 76 | 82 | )} 83 | 84 | 85 | ); 86 | }; 87 | 88 | export default Page; 89 | -------------------------------------------------------------------------------- /src/app/(admin)/view-forms/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FormList from "@/app/forms/FormList"; 3 | import { getUserForms } from "@/app/actions/getUserForms"; 4 | import { InferSelectModel } from "drizzle-orm"; 5 | import { forms as dbForms } from "@/db/schema"; 6 | 7 | type Props = {}; 8 | 9 | const page = async (props: Props) => { 10 | const forms: InferSelectModel[] = await getUserForms(); 11 | return ( 12 |
13 |

My Forms

14 | 15 |
16 | ); 17 | }; 18 | 19 | export default page; 20 | -------------------------------------------------------------------------------- /src/app/actions/generateForm.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import { z } from "zod"; 5 | import { saveForm } from "./mutateForm"; 6 | import { v4 as uuidv4 } from "uuid"; 7 | 8 | import { GoogleGenerativeAI } from "@google/generative-ai"; 9 | const API_KEY = process.env.GEMINI_API_KEY || ""; 10 | const genAI = new GoogleGenerativeAI(API_KEY); 11 | const model = genAI.getGenerativeModel({ model: "gemini-pro" }); 12 | 13 | export async function generateForm( 14 | prevState: { 15 | message: string; 16 | }, 17 | formData: FormData 18 | ) { 19 | const schema = z.object({ 20 | description: z.string().min(1), 21 | }); 22 | const parse = schema.safeParse({ 23 | description: formData.get("description"), 24 | }); 25 | 26 | if (!parse.success) { 27 | console.log(parse.error); 28 | return { 29 | message: "Failed to parse data", 30 | }; 31 | } 32 | 33 | const data = parse.data; 34 | console.log(data); 35 | 36 | try { 37 | console.log("indside"); 38 | const prompt = `${data.description} Based on the description, generate a survey object with 3 fields: name(string) for the form, description(string) of the form and a questions array where every element has 2 fields: text and the fieldType and fieldType can be of these options RadioGroup, Select, Input, Textarea, Switch; and return it in json format. For RadioGroup, and Select types also return fieldOptions array with text and value fields. And more importantly, questions should be only 2 in number. For example, for RadioGroup, and Select types, the field options array can be [{text: 'Yes', value: 'yes'}, {text: 'No', value: 'no'}] and for Input, Textarea, and Switch types, the field options array can be empty. For example, for Input, Textarea, and Switch types, the field options array can be []`; 39 | const result = await model.generateContent(prompt); 40 | const response = await result.response; 41 | const text = response.text(); 42 | const jsonString = text.replace(/^```json\s*([\s\S]*)\s*```$/g, "$1"); 43 | 44 | const responseObject = JSON.parse(jsonString); 45 | console.log(responseObject, "dsf"); 46 | 47 | const dbFormId = await saveForm({ 48 | user_prompt: data.description, 49 | name: responseObject.name, 50 | description: responseObject.description, 51 | questions: responseObject.questions, 52 | }); 53 | 54 | console.log("getting form id", dbFormId); 55 | 56 | revalidatePath("/"); 57 | return { 58 | message: "success", 59 | data: { formId: dbFormId }, 60 | }; 61 | } catch (err) { 62 | console.log(err); 63 | return { 64 | message: "Failed to create form", 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/actions/getSubmissons.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/db"; 4 | import { eq } from "drizzle-orm"; 5 | import { forms } from "@/db/schema"; 6 | 7 | export const getSubmissions = async (formId: number) => { 8 | const form = await db.query.forms.findFirst({ 9 | where: eq(forms.id, formId), 10 | with: { 11 | questions: { 12 | with: { 13 | fieldOptions: true, 14 | }, 15 | }, 16 | submissions: { 17 | with: { 18 | answers: { 19 | with: { 20 | fieldOption: true, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }, 26 | }); 27 | return form; 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/actions/getUserForms.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/db"; 4 | import { eq } from "drizzle-orm"; 5 | import { forms } from "@/db/schema"; 6 | import { auth } from "@/auth"; 7 | 8 | export async function getUserForms() { 9 | const session = await auth(); 10 | const userId = session?.user?.id; 11 | if (!userId) { 12 | return []; 13 | } 14 | const userForms = await db.query.forms.findMany({ 15 | where: eq(forms.userId, userId), 16 | }); 17 | return userForms; 18 | } 19 | 20 | export async function getCurrentForm(formID: string) { 21 | const formData = await db.query.forms.findFirst({ 22 | where: eq(forms.formID, formID), 23 | with: { 24 | questions: true, 25 | }, 26 | }); 27 | return formData; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/actions/mutateForm.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { db } from "@/db"; 4 | import { forms, questions as dbQuestions, fieldOptions } from "@/db/schema"; 5 | import { auth } from "@/auth"; 6 | import { InferInsertModel, eq } from "drizzle-orm"; 7 | import { v4 as uuidv4 } from "uuid"; 8 | import { getCurrentForm } from "./getUserForms"; 9 | import { GoogleGenerativeAI } from "@google/generative-ai"; 10 | const API_KEY = process.env.GEMINI_API_KEY || ""; 11 | 12 | const genAI = new GoogleGenerativeAI(API_KEY); 13 | const model = genAI.getGenerativeModel({ model: "gemini-pro" }); 14 | 15 | type Form = InferInsertModel; 16 | type Question = InferInsertModel; 17 | type FieldOption = InferInsertModel; 18 | 19 | interface SaveFormData extends Form { 20 | questions: Array; 21 | user_prompt: string; 22 | } 23 | 24 | export async function saveForm(data: SaveFormData) { 25 | const { name, description, questions, user_prompt } = data; 26 | const session = await auth(); 27 | const userId = session?.user?.id; 28 | 29 | const newForm = await db 30 | .insert(forms) 31 | .values({ 32 | formID: uuidv4(), 33 | name, 34 | description, 35 | userId, 36 | user_prompt, 37 | published: false, 38 | }) 39 | .returning({ insertedId: forms.id, newFormCreated: forms.formID }); 40 | const formId = newForm[0].insertedId; 41 | // console.log("formId created", formId); 42 | // console.log("formId created", newForm[0].newFormCreated); 43 | 44 | const newQuestions = data.questions.map((question) => { 45 | return { 46 | text: question.text, 47 | fieldType: question.fieldType, 48 | fieldOptions: question.fieldOptions, 49 | formId, 50 | }; 51 | }); 52 | 53 | await db.transaction(async (tx) => { 54 | for (const question of newQuestions) { 55 | const [{ questionId }] = await tx 56 | .insert(dbQuestions) 57 | .values(question) 58 | .returning({ questionId: dbQuestions.id }); 59 | if (question.fieldOptions && question.fieldOptions.length > 0) { 60 | await tx.insert(fieldOptions).values( 61 | question.fieldOptions.map((option) => ({ 62 | text: option.text, 63 | value: option.value, 64 | questionId, 65 | })) 66 | ); 67 | } 68 | } 69 | }); 70 | 71 | return newForm[0].newFormCreated; 72 | } 73 | 74 | export async function publishForm(formId: string) { 75 | await db 76 | .update(forms) 77 | .set({ published: true }) 78 | .where(eq(forms.formID, formId)); 79 | } 80 | 81 | export async function deleteForm(formId: string) { 82 | await db.delete(forms).where(eq(forms.formID, formId)); 83 | } 84 | 85 | export async function addMoreQuestion( 86 | prompt: string, 87 | id: number, 88 | formID: string, 89 | allQuestions: string 90 | ) { 91 | try { 92 | console.log("indside"); 93 | const user_prompt = `${prompt} Based on the description, generate a survey object with 1 field of "questions" array where every element has 2 fields: text and the fieldType and fieldType can be of these options RadioGroup, Select, Input, Textarea, Switch; and return it in json format. For RadioGroup, and Select types also return fieldOptions array with text and value fields. And questions should 2 in quantity and more importantly the questions does not include these question ${allQuestions}. For example, for RadioGroup, and Select types, the field options array can be [{text: 'Yes', value: 'yes'}, {text: 'No', value: 'no'}] and for Input, Textarea, and Switch types, the field options array can be empty. For example, for Input, Textarea, and Switch types, the field options array can be []`; 94 | const result = await model.generateContent(user_prompt); 95 | const response = await result.response; 96 | const text = response.text(); 97 | const jsonString = text.replace(/^```json\s*([\s\S]*)\s*```$/g, "$1"); 98 | 99 | const responseObject = JSON.parse(jsonString); 100 | console.log("ADDITION QUESTIOS", responseObject); 101 | 102 | const currentForm = await getCurrentForm(formID || ""); 103 | 104 | const newQuestions = responseObject.questions.map((question: any) => { 105 | return { 106 | text: question.text, 107 | fieldType: question.fieldType, 108 | fieldOptions: question.fieldOptions, 109 | formId: id, 110 | }; 111 | }); 112 | 113 | const updatedQuestions = [ 114 | ...(currentForm?.questions || []), 115 | ...newQuestions, 116 | ]; 117 | 118 | await db.transaction(async (tx) => { 119 | for (const question of newQuestions) { 120 | console.log("questionssssss", question); 121 | const [{ questionId }] = await tx 122 | .insert(dbQuestions) 123 | .values(question) 124 | .returning({ questionId: dbQuestions.id }); 125 | if (question.fieldOptions && question.fieldOptions.length > 0) { 126 | await tx.insert(fieldOptions).values( 127 | question.fieldOptions.map((option: any) => ({ 128 | text: option.text, 129 | value: option.value, 130 | questionId, 131 | })) 132 | ); 133 | } 134 | } 135 | }); 136 | 137 | return updatedQuestions; 138 | } catch (err) { 139 | console.log(err); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/app/actions/navigateToForm.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | 5 | export async function navigate(id: string) { 6 | redirect(`/forms/edit/${id}`); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/actions/theme.ts: -------------------------------------------------------------------------------- 1 | export const slateTheme = { 2 | background: "222.2 84% 4.9%", 3 | foreground: "210 40% 98%", 4 | card: "222.2 84% 4.9%", 5 | "card-foreground": "210 40% 98%", 6 | popover: "222.2 84% 4.9%", 7 | "popover-foreground": "210 40% 98%", 8 | primary: "210 40% 98%", 9 | "primary-foreground": "222.2 47.4% 11.2%", 10 | secondary: "217.2 32.6% 17.5%", 11 | "secondary-foreground": "210 40% 98%", 12 | muted: "217.2 32.6% 17.5%", 13 | "muted-foreground": "215 20.2% 65.1%", 14 | accent: "217.2 32.6% 17.5%", 15 | "accent-foreground": "210 40% 98%", 16 | destructive: "0 62.8% 30.6%", 17 | "destructive-foreground": "210 40% 98%", 18 | border: "217.2 32.6% 17.5%", 19 | input: "217.2 32.6% 17.5%", 20 | ring: "212.7 26.8% 83.9", 21 | radius: "0.5rem", 22 | }; 23 | 24 | export const violetTheme = { 25 | background: "224 71.4% 4.1%", 26 | foreground: "210 20% 98%", 27 | card: "224 71.4% 4.1%", 28 | "card-foreground": "210 20% 98%", 29 | popover: "224 71.4% 4.1%", 30 | "popover-foreground": "210 20% 98%", 31 | primary: "263.4 70% 50.4%", 32 | "primary-foreground": "210 20% 98%", 33 | secondary: "215 27.9% 16.9%", 34 | "secondary-foreground": "210 20% 98%", 35 | muted: "215 27.9% 16.9%", 36 | "muted-foreground": "217.9 10.6% 64.9%", 37 | accent: "215 27.9% 16.9%", 38 | "accent-foreground": "210 20% 98%", 39 | destructive: "0 62.8% 30.6%", 40 | "destructive-foreground": "210 20% 98%", 41 | border: "215 27.9% 16.9%", 42 | input: "215 27.9% 16.9%", 43 | ring: "263.4 70% 50.4%", 44 | radius: "0.5rem", 45 | }; 46 | 47 | export const redTheme = { 48 | background: "0 0% 3.9%", 49 | foreground: "0 0% 98%", 50 | card: "0 0% 3.9%", 51 | "card-foreground": "0 0% 98%", 52 | popover: "0 0% 3.9%", 53 | "popover-foreground": "0 0% 98%", 54 | primary: "0 72.2% 50.6%", 55 | "primary-foreground": "0 85.7% 97.3%", 56 | secondary: "0 0% 14.9%", 57 | "secondary-foreground": "0 0% 98%", 58 | muted: "0 0% 14.9%", 59 | "muted-foreground": "0 0% 63.9%", 60 | accent: "0 0% 14.9%", 61 | "accent-foreground": "0 0% 98%", 62 | destructive: "0 62.8% 30.6%", 63 | "destructive-foreground": "0 0% 98%", 64 | border: "0 0% 14.9%", 65 | input: "0 0% 14.9%", 66 | ring: "0 72.2% 50.6%", 67 | radius: "0.5rem", 68 | }; 69 | 70 | export const blueTheme = { 71 | background: "222.2 84% 4.9%", 72 | foreground: "210 40% 98%", 73 | card: "222.2 84% 4.9%", 74 | "card-foreground": "210 40% 98%", 75 | popover: "222.2 84% 4.9%", 76 | "popover-foreground": "210 40% 98%", 77 | primary: "217.2 91.2% 59.8%", 78 | "primary-foreground": "222.2 47.4% 11.2%", 79 | secondary: "217.2 32.6% 17.5%", 80 | "secondary-foreground": "210 40% 98%", 81 | muted: "217.2 32.6% 17.5%", 82 | "muted-foreground": "215 20.2% 65.1%", 83 | accent: "217.2 32.6% 17.5%", 84 | "accent-foreground": "210 40% 98%", 85 | destructive: "0 62.8% 30.6%", 86 | "destructive-foreground": "210 40% 98%", 87 | border: "217.2 32.6% 17.5%", 88 | input: "217.2 32.6% 17.5%", 89 | ring: "224.3 76.3% 48%", 90 | radius: "0.5rem", 91 | }; 92 | 93 | export const orangeTheme = { 94 | background: "20 14.3% 4.1%", 95 | foreground: "60 9.1% 97.8%", 96 | card: "20 14.3% 4.1%", 97 | "card-foreground": "60 9.1% 97.8%", 98 | popover: "20 14.3% 4.1%", 99 | "popover-foreground": "60 9.1% 97.8%", 100 | primary: "20.5 90.2% 48.2%", 101 | "primary-foreground": "60 9.1% 97.8%", 102 | secondary: "12 6.5% 15.1%", 103 | "secondary-foreground": "60 9.1% 97.8%", 104 | muted: "12 6.5% 15.1%", 105 | "muted-foreground": "24 5.4% 63.9%", 106 | accent: "12 6.5% 15.1%", 107 | "accent-foreground": "60 9.1% 97.8%", 108 | destructive: "0 72.2% 50.6%", 109 | "destructive-foreground": "60 9.1% 97.8%", 110 | border: "12 6.5% 15.1%", 111 | input: "12 6.5% 15.1%", 112 | ring: "20.5 90.2% 48.2%", 113 | radius: "0.5rem", 114 | }; 115 | 116 | export const neutralTheme = { 117 | background: "0 0% 3.9%", 118 | foreground: "0 0% 98%", 119 | card: "0 0% 3.9%", 120 | "card-foreground": "0 0% 98%", 121 | popover: "0 0% 3.9%", 122 | "popover-foreground": "0 0% 98%", 123 | primary: "0 0% 98%", 124 | "primary-foreground": "0 0% 9%", 125 | secondary: "0 0% 14.9%", 126 | "secondary-foreground": "0 0% 98%", 127 | muted: "0 0% 14.9%", 128 | "muted-foreground": "0 0% 63.9%", 129 | accent: "0 0% 14.9%", 130 | "accent-foreground": "0 0% 98%", 131 | destructive: "0 62.8% 30.6%", 132 | "destructive-foreground": "0 0% 98%", 133 | border: "0 0% 14.9%", 134 | input: "0 0% 14.9%", 135 | ring: "0 0% 83.1%", 136 | radius: "0.5rem", 137 | }; 138 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@/auth"; 2 | -------------------------------------------------------------------------------- /src/app/api/form/new/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/db"; 2 | import { forms, formSubmissions, answers as dbAnswers } from "@/db/schema"; 3 | 4 | export async function POST(request: Request): Promise { 5 | const data = await request.json(); 6 | 7 | const newFormSubmission = await db 8 | .insert(formSubmissions) 9 | .values({ 10 | formId: data.formId, 11 | }) 12 | .returning({ 13 | insertedId: formSubmissions.id, 14 | }); 15 | const [{ insertedId }] = newFormSubmission; 16 | 17 | await db.transaction(async (tx) => { 18 | for (const answer of data.answers) { 19 | const [{ answerId }] = await tx 20 | .insert(dbAnswers) 21 | .values({ 22 | formSubmissionId: insertedId, 23 | ...answer, 24 | }) 25 | .returning({ 26 | answerId: dbAnswers.id, 27 | }); 28 | } 29 | }); 30 | 31 | return Response.json({ formSubmissionsId: insertedId }, { status: 200 }); 32 | } 33 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harshxraj/ai-form-builder/ab614ea87ebc8f541522cf8acc6dd8b1a7a7a96d/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/form-generator/index.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect } from "react"; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | DialogFooter, 12 | } from "@/components/ui/dialog"; 13 | import { Button } from "@/components/ui/button"; 14 | import { Textarea } from "@/components/ui/textarea"; 15 | import { generateForm } from "@/actions/generateForm"; 16 | import { useFormState, useFormStatus } from "react-dom"; 17 | import { navigate } from "@/actions/navigateToForm"; 18 | 19 | import { useSession, signIn } from "next-auth/react"; 20 | 21 | type Props = {}; 22 | 23 | const initialState: { 24 | message: string; 25 | data?: any; 26 | } = { 27 | message: "", 28 | }; 29 | 30 | export function SubmitButton() { 31 | const { pending } = useFormStatus(); 32 | return ( 33 | 36 | ); 37 | } 38 | 39 | const FormGenerator = (props: Props) => { 40 | const [state, formAction] = useFormState(generateForm, initialState); 41 | const [open, setOpen] = useState(false); 42 | const session = useSession(); 43 | 44 | useEffect(() => { 45 | console.log("State", state); 46 | if (state?.message == "success") { 47 | setOpen(false); 48 | navigate(state.data.formId); 49 | // navigate(state.data.formId, "Additional value i am passing"); 50 | } 51 | console.log(state?.data); 52 | }, [state?.message]); 53 | 54 | const onFormCreate = () => { 55 | if (session.data?.user) { 56 | setOpen(true); 57 | } else { 58 | signIn(); 59 | } 60 | }; 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | Create New Form 68 | 69 |
70 |