├── my-app ├── app │ ├── favicon.ico │ ├── page.tsx │ ├── api │ │ └── chat │ │ │ └── route.ts │ ├── layout.tsx │ ├── assistant.tsx │ └── globals.css ├── postcss.config.mjs ├── next.config.ts ├── lib │ └── utils.ts ├── next-env.d.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── breadcrumb.tsx │ │ ├── sheet.tsx │ │ └── sidebar.tsx │ ├── assistant-ui │ │ ├── tooltip-icon-button.tsx │ │ ├── tool-fallback.tsx │ │ ├── thread-list.tsx │ │ ├── markdown-text.tsx │ │ └── thread.tsx │ └── app-sidebar.tsx ├── eslint.config.mjs ├── components.json ├── README.md ├── hooks │ └── use-mobile.ts ├── tsconfig.json └── package.json └── README.md /my-app/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gaurav-creater317/Prompt-Mate/HEAD/my-app/app/favicon.ico -------------------------------------------------------------------------------- /my-app/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /my-app/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Assistant } from "./assistant"; 2 | 3 | export default function Home() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /my-app/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /my-app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /my-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /my-app/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /my-app/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { google } from "@ai-sdk/google"; 2 | import { streamText } from "ai"; 3 | 4 | export const maxDuration = 30; 5 | 6 | export async function POST(req: Request) { 7 | const { messages } = await req.json(); 8 | const result = streamText({ 9 | model: google("gemini-2.0-flash"), 10 | messages, 11 | }); 12 | return result.toDataStreamResponse(); 13 | } -------------------------------------------------------------------------------- /my-app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /my-app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /my-app/README.md: -------------------------------------------------------------------------------- 1 | This is the [assistant-ui](https://github.com/Yonom/assistant-ui) starter project. 2 | 3 | ## Getting Started 4 | 5 | First, add your OpenAI API key to `.env.local` file: 6 | 7 | ``` 8 | OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 9 | ``` 10 | 11 | Then, run the development server: 12 | 13 | ```bash 14 | npm run dev 15 | # or 16 | yarn dev 17 | # or 18 | pnpm dev 19 | # or 20 | bun dev 21 | ``` 22 | 23 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 24 | 25 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 26 | -------------------------------------------------------------------------------- /my-app/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /my-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /my-app/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /my-app/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "assistant-ui Starter App", 17 | description: "Generated by create-assistant-ui", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /my-app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /my-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assistant-ui-starter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ai-sdk/google": "^1.2.22", 13 | "@ai-sdk/openai": "^1.3.22", 14 | "@assistant-ui/react": "^0.10.9", 15 | "@assistant-ui/react-ai-sdk": "^0.10.14", 16 | "@assistant-ui/react-markdown": "^0.10.3", 17 | "@radix-ui/react-dialog": "^1.1.14", 18 | "@radix-ui/react-separator": "^1.1.7", 19 | "@radix-ui/react-slot": "^1.2.3", 20 | "@radix-ui/react-tooltip": "^1.2.7", 21 | "ai": "^4.3.16", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "lucide-react": "^0.511.0", 25 | "next": "15.3.2", 26 | "react": "^19.1.0", 27 | "react-dom": "^19.1.0", 28 | "remark-gfm": "^4.0.1", 29 | "tailwind-merge": "^3.3.0", 30 | "tw-animate-css": "^1.3.0" 31 | }, 32 | "devDependencies": { 33 | "@eslint/eslintrc": "^3", 34 | "@tailwindcss/postcss": "^4", 35 | "@types/node": "^22", 36 | "@types/react": "^19", 37 | "@types/react-dom": "^19", 38 | "eslint": "^9", 39 | "eslint-config-next": "15.3.2", 40 | "tailwindcss": "^4", 41 | "typescript": "^5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /my-app/components/assistant-ui/tooltip-icon-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ComponentPropsWithoutRef, forwardRef } from "react"; 4 | 5 | import { 6 | Tooltip, 7 | TooltipContent, 8 | TooltipProvider, 9 | TooltipTrigger, 10 | } from "@/components/ui/tooltip"; 11 | import { Button } from "@/components/ui/button"; 12 | import { cn } from "@/lib/utils"; 13 | 14 | export type TooltipIconButtonProps = ComponentPropsWithoutRef & { 15 | tooltip: string; 16 | side?: "top" | "bottom" | "left" | "right"; 17 | }; 18 | 19 | export const TooltipIconButton = forwardRef< 20 | HTMLButtonElement, 21 | TooltipIconButtonProps 22 | >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => { 23 | return ( 24 | 25 | 26 | 27 | 37 | 38 | {tooltip} 39 | 40 | 41 | ); 42 | }); 43 | 44 | TooltipIconButton.displayName = "TooltipIconButton"; 45 | -------------------------------------------------------------------------------- /my-app/components/assistant-ui/tool-fallback.tsx: -------------------------------------------------------------------------------- 1 | import { ToolCallContentPartComponent } from "@assistant-ui/react"; 2 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; 3 | import { useState } from "react"; 4 | import { Button } from "../ui/button"; 5 | 6 | export const ToolFallback: ToolCallContentPartComponent = ({ 7 | toolName, 8 | argsText, 9 | result, 10 | }) => { 11 | const [isCollapsed, setIsCollapsed] = useState(true); 12 | return ( 13 |
14 |
15 | 16 |

17 | Used tool: {toolName} 18 |

19 |
20 | 23 |
24 | {!isCollapsed && ( 25 |
26 |
27 |
{argsText}
28 |
29 | {result !== undefined && ( 30 |
31 |

Result:

32 |
33 |                 {typeof result === "string"
34 |                   ? result
35 |                   : JSON.stringify(result, null, 2)}
36 |               
37 |
38 | )} 39 |
40 | )} 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /my-app/app/assistant.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AssistantRuntimeProvider } from "@assistant-ui/react"; 4 | import { useChatRuntime } from "@assistant-ui/react-ai-sdk"; 5 | import { Thread } from "@/components/assistant-ui/thread"; 6 | import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; 7 | import { AppSidebar } from "@/components/app-sidebar"; 8 | import { Separator } from "@/components/ui/separator"; 9 | import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; 10 | 11 | export const Assistant = () => { 12 | const runtime = useChatRuntime({ 13 | api: "/api/chat", 14 | }); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | Build Your Own ChatGPT UX 29 | 30 | 31 | 32 | 33 | 34 | Starter Template 35 | 36 | 37 | 38 | 39 |
40 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /my-app/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return ; 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 62 | -------------------------------------------------------------------------------- /my-app/components/assistant-ui/thread-list.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import { 3 | ThreadListItemPrimitive, 4 | ThreadListPrimitive, 5 | } from "@assistant-ui/react"; 6 | import { ArchiveIcon, PlusIcon } from "lucide-react"; 7 | 8 | import { Button } from "@/components/ui/button"; 9 | import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; 10 | 11 | export const ThreadList: FC = () => { 12 | return ( 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const ThreadListNew: FC = () => { 21 | return ( 22 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | const ThreadListItems: FC = () => { 32 | return ; 33 | }; 34 | 35 | const ThreadListItem: FC = () => { 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }; 45 | 46 | const ThreadListItemTitle: FC = () => { 47 | return ( 48 |

49 | 50 |

51 | ); 52 | }; 53 | 54 | const ThreadListItemArchive: FC = () => { 55 | return ( 56 | 57 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prompt Mate - Your Personal AI Conversation Campaign 2 | 3 | # Education Track: Build Apps with Google AI Studio 4 | 5 | # 🚀 Prompt Mate – Your Personal AI Conversation Companion using Next.js & Gemini API 💬 6 | 7 | Hey DEV community! 👋 8 | 9 | I'm thrilled to introduce my latest project – Prompt Mate – an AI-powered chat interface built using Next.js and Google Cloud’s Gemini API via Google AI Studio. 10 | 11 | # 💡 What is Prompt Mate? 12 | Prompt Mate is a Next.js web application that enables users to: 13 | 14 | 🧠 Chat with an AI assistant powered by Google’s Gemini model. 15 | ✍️ Send custom prompts and get intelligent, real-time responses. 16 | 📱 Experience a responsive and clean UI across devices. 17 | 🔐 Interact securely with Google Cloud-hosted functions. 18 | Whether you're a developer, student, or AI enthusiast, Prompt Mate is designed to demonstrate the power of LLMs in a full-stack web application. 19 | 20 | # 🛠️ Tech Stack 21 | ⚛️ Next.js – Full-stack React framework. 22 | ☁️ Google Cloud Gemini API – Used via API key. 23 | 🧪 Google AI Studio – For prompt testing and optimization . 24 | 🌐 Tailwind CSS – For styling the UI. 25 | 🔐 .env.local – Securing API keys in environment variables. 26 | 🔁 API Routes – To safely call Gemini from the server side. 27 | 🔧 Key Features 28 | ✅ Fully responsive chat interface. 29 | 🧠 Gemini AI-generated responses. 30 | 🔒 API key protected backend route in Next.js. 31 | 🌙 Dark mode ready (optional toggle). 32 | 🧼 Clean code structure and modular components. 33 | 📸 34 | 35 | 36 | # 🔗 Live Demo & Code 37 | 🔍 Live Preview: Visit Prompt Mate 38 | 39 | 40 | # 🧠 What I Learned 41 | Seamlessly integrating Gemini API with Next.js serverless routes 42 | Protecting API keys using .env.local and getServerSideProps 43 | Building clean and intuitive UIs using Tailwind CSS 44 | Testing and refining prompts with Google AI Studio 45 | 46 | # 🎯 Final Thoughts 47 | Working on Prompt Mate gave me a practical dive into generative AI development and deploying it on a modern full-stack framework. 48 | 49 | It’s amazing how quickly you can build something powerful with the right APIs and tools. 50 | 51 | I’d love your feedback and thoughts! 🌟 52 | 53 | Feel free to fork, remix, or collaborate with me on future versions. 54 | # ✨ Also feel free to star this repository if you like it! ✨ 55 | -------------------------------------------------------------------------------- /my-app/components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Github, MessagesSquare } from "lucide-react" 3 | import Link from "next/link" 4 | import { 5 | Sidebar, 6 | SidebarContent, 7 | SidebarFooter, 8 | SidebarHeader, 9 | SidebarMenu, 10 | SidebarMenuButton, 11 | SidebarMenuItem, 12 | SidebarRail, 13 | } from "@/components/ui/sidebar" 14 | import { ThreadList } from "./assistant-ui/thread-list" 15 | 16 | export function AppSidebar({ ...props }: React.ComponentProps) { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 | assistant-ui 29 |
30 | 31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 |
49 |
50 | GitHub 51 | View Source 52 |
53 | 54 |
55 | 56 |
57 |
58 |
59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /my-app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /my-app/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { 8 | return