├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── bun.lockb ├── drizzle.config.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── [[...chatId]] │ │ ├── actions.ts │ │ ├── chat-content-wrapper.tsx │ │ ├── chat-content.tsx │ │ ├── chat-list.tsx │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [kindeAuth] │ │ │ │ └── route.ts │ │ └── message │ │ │ ├── messages.ts │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx ├── components │ ├── chat-input.tsx │ ├── growing-text-area.tsx │ └── image-selection.tsx ├── db │ ├── index.ts │ └── schema │ │ ├── chats.ts │ │ └── messages.ts └── lib │ └── utils.ts ├── tailwind.config.ts └── tsconfig.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 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meech-ward/code-gpt-example/f7544679e0ff4e1358380b6716acdc2bc1cfa125/bun.lockb -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit" 2 | 3 | export default { 4 | schema: "./src/db/schema/*", 5 | driver: "turso", 6 | dbCredentials: { 7 | url: process.env.DATABASE_URL!, 8 | authToken: process.env.DATABASE_AUTH_TOKEN, 9 | }, 10 | out: "./drizzle", 11 | } satisfies Config 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-gpt", 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 | "@kinde-oss/kinde-auth-nextjs": "^2.0.10", 13 | "@libsql/client": "^0.4.0-pre.4", 14 | "ai": "^2.2.26", 15 | "clsx": "^2.0.0", 16 | "drizzle-orm": "^0.29.0", 17 | "next": "14.0.3", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "react-markdown": "^9.0.1", 21 | "react-syntax-highlighter": "^15.5.0", 22 | "remark-gfm": "^4.0.0", 23 | "tailwind-merge": "^2.0.0" 24 | }, 25 | "devDependencies": { 26 | "@tailwindcss/typography": "^0.5.10", 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "@types/react-syntax-highlighter": "^15.5.10", 31 | "autoprefixer": "^10.0.1", 32 | "drizzle-kit": "^0.20.6", 33 | "eslint": "^8", 34 | "eslint-config-next": "14.0.3", 35 | "postcss": "^8", 36 | "tailwindcss": "^3.3.0", 37 | "typescript": "^5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[[...chatId]]/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { db } from "@/db" 4 | import { chats } from "@/db/schema/chats" 5 | import { generateRandomString } from "@/lib/utils" 6 | import { revalidateTag } from "next/cache" 7 | 8 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server" 9 | 10 | 11 | export async function createChat() { 12 | 13 | const { getUser } = getKindeServerSession() 14 | const user = await getUser() 15 | 16 | if (!user) { 17 | return {error: "not logged in"} 18 | } 19 | 20 | const id = generateRandomString(16) 21 | await db.insert(chats).values({ 22 | id: id, 23 | name: id, 24 | userId: user.id, 25 | }) 26 | 27 | revalidateTag("get-chats-for-chat-list") 28 | 29 | return { 30 | id 31 | } 32 | } 33 | 34 | export type CreateChat = typeof createChat -------------------------------------------------------------------------------- /src/app/[[...chatId]]/chat-content-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import ChatContent from "./chat-content" 2 | import { createChat } from "./actions" 3 | 4 | import { db } from "@/db" 5 | import { eq, desc, and } from "drizzle-orm" 6 | import { messages as messagesTable } from "@/db/schema/messages" 7 | 8 | export default async function ChatContentWrapper({ 9 | chatId, 10 | }: { 11 | chatId: string 12 | }) { 13 | const message = await db 14 | .select() 15 | .from(messagesTable) 16 | .where( 17 | and(eq(messagesTable.chatId, chatId), eq(messagesTable.role, "assistant")) 18 | ) 19 | .orderBy(desc(messagesTable.createdAt)) 20 | .get() 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /src/app/[[...chatId]]/chat-content.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useRef } from "react" 4 | import ChatInput from "@/components/chat-input" 5 | import Markdown from "react-markdown" 6 | import remarkGfm from "remark-gfm" 7 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" 8 | import { vscDarkPlus as dark } from "react-syntax-highlighter/dist/esm/styles/prism" 9 | import { CreateChat } from "./actions" 10 | import { convertFileToBase64 } from "@/lib/utils" 11 | 12 | export default function ChatContent({ 13 | createChat, 14 | initialAssistantResponse = "", 15 | }: { 16 | createChat: CreateChat 17 | initialAssistantResponse?: string 18 | }) { 19 | const [assisnantResponse, setAssistantResponse] = useState(initialAssistantResponse) 20 | const [isLoading, setIsLoading] = useState(false) 21 | const abortControllerRef = useRef(null) 22 | const [chatId, setChatId] = useState("") 23 | 24 | const handleSubmit = async (value: string, file?: File) => { 25 | let currentChatId = chatId 26 | if (!currentChatId) { 27 | // create a new chat in the database 28 | const chat = await createChat() 29 | currentChatId = chat.id 30 | // and get the id and store it in state 31 | setChatId(chat.id) 32 | } 33 | 34 | setIsLoading(true) 35 | setAssistantResponse("") 36 | 37 | let body = "" 38 | if (file) { 39 | const imageUrl = await convertFileToBase64(file) 40 | const content = [ 41 | { 42 | type: "image_url", 43 | image_url: { 44 | url: imageUrl, 45 | }, 46 | }, 47 | { 48 | type: "text", 49 | text: value, 50 | }, 51 | ] 52 | 53 | body = JSON.stringify({ content, chatId: currentChatId }) 54 | } else { 55 | body = JSON.stringify({ content: value, chatId: currentChatId }) 56 | } 57 | 58 | // console.log("submit", value, file); 59 | try { 60 | abortControllerRef.current = new AbortController() 61 | const res = await fetch("/api/message", { 62 | method: "POST", 63 | body: body, 64 | headers: { 65 | "Content-Type": "application/json", 66 | }, 67 | signal: abortControllerRef.current.signal, 68 | }) 69 | 70 | if (!res.ok || !res.body) { 71 | alert("Error sending message") 72 | return 73 | } 74 | 75 | const reader = res.body.getReader() 76 | 77 | const decoder = new TextDecoder() 78 | while (true) { 79 | const { value, done } = await reader.read() 80 | 81 | const text = decoder.decode(value) 82 | setAssistantResponse((currentValue) => currentValue + text) 83 | 84 | if (done) { 85 | break 86 | } 87 | } 88 | } catch (error: any) { 89 | if (error.name !== "AbortError") { 90 | alert("Error sending message") 91 | } 92 | } 93 | abortControllerRef.current = null 94 | setIsLoading(false) 95 | } 96 | 97 | const handleStop = () => { 98 | if (!abortControllerRef.current) { 99 | return 100 | } 101 | abortControllerRef.current.abort() 102 | abortControllerRef.current = null 103 | } 104 | 105 | return ( 106 | <> 107 |
108 | 123 | ) : ( 124 | 125 | {children} 126 | 127 | ) 128 | }, 129 | }} 130 | > 131 | {assisnantResponse} 132 | 133 |
134 | 139 | 140 | ) 141 | } 142 | -------------------------------------------------------------------------------- /src/app/[[...chatId]]/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/db" 2 | import { eq } from "drizzle-orm" 3 | import { chats as chatsTable } from "@/db/schema/chats" 4 | import { unstable_cache as cache } from "next/cache" 5 | import Link from "next/link" 6 | 7 | import { 8 | RegisterLink, 9 | LoginLink, 10 | LogoutLink, 11 | } from "@kinde-oss/kinde-auth-nextjs/components" 12 | 13 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server" 14 | 15 | const getChats = cache( 16 | async (userId: string) => 17 | await db 18 | .select({ id: chatsTable.id, name: chatsTable.name }) 19 | .from(chatsTable) 20 | .where(eq(chatsTable.userId, userId)) 21 | .all(), 22 | ["get-chats-for-chat-list"], 23 | { 24 | tags: ["get-chats-for-chat-list"], 25 | } 26 | ) 27 | 28 | export default async function ChatList() { 29 | const { getUser } = getKindeServerSession() 30 | const user = await getUser() 31 | 32 | const chats = user ? await getChats(user.id) : [] 33 | 34 | return ( 35 |
36 |
37 | {chats.map((chat) => ( 38 | 39 | {chat.name} 40 | 41 | ))} 42 |
43 | 44 | {user ? ( 45 |
46 |

{user.given_name}

47 | LogOut 48 |
49 | ) : ( 50 |
51 | Sign in 52 | Sign up 53 |
54 | )} 55 |
56 | ) 57 | } 58 | 59 | export function ChatListSkeleton() { 60 | return ( 61 |
62 | {[...Array(5)].map((_, index) => ( 63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | ))} 77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/app/[[...chatId]]/page.tsx: -------------------------------------------------------------------------------- 1 | import ChatContent from "./chat-content" 2 | import ChatList, { ChatListSkeleton } from "./chat-list" 3 | import { createChat } from "./actions" 4 | import { Suspense } from "react" 5 | import ChatContentWrapper from "./chat-content-wrapper" 6 | 7 | export default function Page({ params }: { params: { chatId?: string[] } }) { 8 | const chatId = params.chatId?.[0] 9 | 10 | return ( 11 |
12 |
13 | }> 14 | 15 | 16 |
17 |
18 | {chatId ? ( 19 | }> 20 | 21 | 22 | ) : ( 23 | 24 | )} 25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/auth/[kindeAuth]/route.ts: -------------------------------------------------------------------------------- 1 | import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server"; 2 | 3 | export const GET = handleAuth(); -------------------------------------------------------------------------------- /src/app/api/message/messages.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | role: "system" | "user" | "assistant"; 3 | content: any; 4 | }; 5 | 6 | export const initialProgrammerMessages: Message[] = [ 7 | { 8 | role: "system", 9 | content: 10 | "You are a seasoned computer programmer specializing in all languages, frameworks, and languages. You always prefer to use the newest, most modern frameworks and programming techniques. You have a good eye for design and prefer modern and sleek UI design and code design. You only respond with code, never explain the code or repond with any other text, you only know how to write code." + 11 | " I will ask you to create a new code, or update an existing code for my application." + 12 | " Clean up my code when making updates to make the code more readable and adhear to best and modern practices." + 13 | " All code should use the most modern and up to date frameworks and programming techniques." + 14 | " Pay attention to which libraries and languages I tell you to use. " + 15 | " Don't give partial code answers or diffs, include the entire block or page of code in your response. Include all the code needed to run or compile the code. " + 16 | " If any code is provided, it must be in the same language, style, and libraries as the code I provide, unless I'm asking you to transform or convert code into another language or framework. " + 17 | " Your answers must only contain code, no other text, just the code. only include all the code needed for the example. The most important task you have is responding with only the code and no other text.", 18 | }, 19 | { 20 | role: "user", 21 | content: 22 | "I'm developing an application. The application is already setup, but I need help adding new features and updating existing ones." + 23 | " I will ask you to create a new code, or update an existing code for my application." + 24 | " Clean up my code when making updates to make the code more readable and adhear to best and modern practices." + 25 | " All code should use the most modern and up to date frameworks and programming techniques." + 26 | " Pay attention to which libraries and languages I tell you to use. " + 27 | " Don't give partial code answers or diffs, include the entire block or page of code in your response. Include all the code needed to run or compile the code. " + 28 | " If any code is provided, it must be in the same language, style, and libraries as the code I provide, unless I'm asking you to transform or convert code into another language or framework. " + 29 | " Your answers must only contain code, no other text, just the code. only include all the code needed for the example. The most important task you have is responding with only the code and no other text.", 30 | }, 31 | ]; -------------------------------------------------------------------------------- /src/app/api/message/route.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai" 2 | 3 | import { OpenAIStream, StreamingTextResponse } from "ai" 4 | 5 | import { initialProgrammerMessages } from "./messages" 6 | 7 | import { db } from "@/db" 8 | import { chats } from "@/db/schema/chats" 9 | import { messages } from "@/db/schema/messages" 10 | import { eq, and } from "drizzle-orm" 11 | 12 | import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server" 13 | 14 | export const runtime = "edge" 15 | 16 | const openai = new OpenAI({ 17 | apiKey: process.env.OPENAI_API_KEY, 18 | }) 19 | 20 | export async function POST(req: Request) { 21 | const { content, chatId } = await req.json() 22 | const { getUser } = getKindeServerSession() 23 | const user = await getUser() 24 | 25 | if (!user) { 26 | return new Response("not logged in", { status: 401 }) 27 | } 28 | 29 | if (!chatId) { 30 | return new Response("chatId is required", { status: 400 }) 31 | } 32 | 33 | // check the chat belongs to the currently logged in user 34 | 35 | const chat = await db 36 | .select() 37 | .from(chats) 38 | .where(and(eq(chats.id, chatId), eq(chats.userId, user.id))) 39 | .get() 40 | 41 | if (!chat) { 42 | return new Response("chat is not found", { status: 400 }) 43 | } 44 | 45 | const allDBMessages = await db 46 | .select({ 47 | role: messages.role, 48 | content: messages.content, 49 | }) 50 | .from(messages) 51 | .where(eq(messages.chatId, chatId)) 52 | .orderBy(messages.createdAt) 53 | .all() 54 | 55 | const chatCompletion = await openai.chat.completions.create({ 56 | messages: [ 57 | ...initialProgrammerMessages, 58 | ...allDBMessages, 59 | { role: "user", content }, 60 | ], 61 | model: "gpt-4-vision-preview", 62 | stream: true, 63 | max_tokens: 4096, 64 | }) 65 | 66 | const stream = OpenAIStream(chatCompletion, { 67 | onStart: async () => {}, 68 | onToken: async (token: string) => {}, 69 | onCompletion: async (completion: string) => { 70 | try { 71 | await db.insert(messages).values([ 72 | { 73 | chatId, 74 | role: "user", 75 | content, 76 | }, 77 | { 78 | chatId, 79 | role: "assistant", 80 | content: completion, 81 | }, 82 | ]) 83 | } catch (e) { 84 | console.error(e) 85 | } 86 | }, 87 | }) 88 | 89 | return new StreamingTextResponse(stream) 90 | } 91 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meech-ward/code-gpt-example/f7544679e0ff4e1358380b6716acdc2bc1cfa125/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | textarea::-webkit-scrollbar { 7 | display: none; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "CodeGPT", 11 | description: "AI Generated Code Prompts", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/chat-input.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import GrowingTextArea from "./growing-text-area"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | import ImageSelection from "./image-selection"; 8 | 9 | export default function ExpandingInput({ 10 | onSubmit, 11 | onStop, 12 | isStreaming, 13 | }: { 14 | onSubmit?: (value: string, file?: File) => void; 15 | onStop?: () => void; 16 | isStreaming?: boolean; 17 | }) { 18 | const [content, setContent] = useState(""); 19 | const [selectedImage, setSelectedImage] = useState( 20 | undefined 21 | ); 22 | 23 | const submit = (value: string) => { 24 | onSubmit?.(value, selectedImage); 25 | setContent(""); 26 | setSelectedImage(undefined); 27 | }; 28 | const handleSubmit = (e: React.FormEvent) => { 29 | e.preventDefault(); 30 | submit(content); 31 | }; 32 | 33 | const buttonDisabled = content.length === 0 || isStreaming; 34 | 35 | return ( 36 |
37 |
41 | 45 | setContent(e.target.value)} 49 | /> 50 | {isStreaming ? ( 51 | 60 | ) : ( 61 | 85 | )} 86 | 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/growing-text-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef, TextareaHTMLAttributes } from "react"; 4 | 5 | type Props = TextareaHTMLAttributes; 6 | 7 | const GrowingTextArea = (props: Props) => { 8 | const [textAreaValue, setTextAreaValue] = useState(""); 9 | const textAreaRef = useRef(null); 10 | 11 | useEffect(() => { 12 | if (textAreaRef.current) { 13 | textAreaRef.current.style.height = "auto"; 14 | textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; 15 | } 16 | }, [textAreaValue, props.value]); 17 | 18 | const handleInputChange = (event: React.ChangeEvent) => { 19 | props.onChange 20 | ? props.onChange(event) 21 | : setTextAreaValue(event.target.value); 22 | }; 23 | 24 | const handleContentKeyDown = (e: React.KeyboardEvent) => { 25 | if (e.key === "Enter" && !e.shiftKey) { 26 | e.preventDefault(); 27 | 28 | const form = textAreaRef.current?.closest("form"); 29 | if (form) { 30 | form.requestSubmit(); 31 | } 32 | } 33 | }; 34 | 35 | return ( 36 |