├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components ├── AnimateChats.js ├── ClientSide.js └── Icon.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ └── chat.js └── index.js ├── postcss.config.js ├── public ├── favicon.ico ├── image │ ├── ss-chat.png │ └── ss-chats.png ├── next.svg ├── thirteen.svg └── vercel.svg ├── store └── ChatStore.js ├── styles └── globals.css └── tailwind.config.js /.env.example: -------------------------------------------------------------------------------- 1 | // change this to .env 2 | API_KEY = your_apikey_from_openai -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NextJS Chat OpenAI 2 | 3 | ### Requirements 4 | 5 | - Node.js 14+ and npm 6 | 7 | ### Getting started 8 | 9 | Run the following command on your local environment: 10 | 11 | ```shell 12 | git clone https://github.com/hadi-16/nextjs-chat-openai.git your-project-name 13 | cd your-project-name 14 | npm install 15 | ``` 16 | 17 | ## How to run app 18 | 19 | 1. Get api key from OpenAI [https://beta.openai.com](https://beta.openai.com). 20 | 2. Edit env.example to .env 21 | 3. Enter api key to .env 22 | 4. Run the development server : 23 | 24 | ```bash 25 | npm run dev 26 | # or 27 | yarn dev 28 | ``` 29 | 30 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the app. 31 | 32 | ```shell 33 | structure folder app 34 | 35 | ├── components # Components folder 36 | ├── pages # Next JS Pages 37 | ├── public # Public assets folder 38 | ├── store # Store folder 39 | ├── styles # Next JS Style 40 | ├── README.md # README file 41 | ├── tailwind.config.js # Tailwind CSS configuration 42 | ├── .env # Next JS environment variables 43 | ``` 44 | 45 | ### Features 46 | 47 | - ⚡ [Next.js](https://nextjs.org) for Fullstack Framework Javascript 48 | - 🤖 Integrate Chatbot API with [OpenAI](https://openai.com) 49 | - 💎 Integrate Styling with [Tailwind CSS](https://tailwindcss.com) 50 | - ⚙️ State Management with [Zustand](https://www.npmjs.com/package/zustand) 51 | - 🔦 Animation Chats with [AutoAnimate](https://auto-animate.formkit.com) 52 | 53 | ### Demo app 54 | Open [https://chat.hadi.pw](https://chat.hadi.pw) the url in your browser for the demo application. 55 | 56 | ### Screenshoot app 57 | 58 | | Screenshot Chat | Screenshot Chats | 59 | | --- | --- | 60 | | ![SS Chat](https://github.com/hadi-16/nextjs-chat-openai/blob/main/public/image/ss-chat.png?raw=true "ss nextjs chat openai") | ![SS Chats](https://github.com/hadi-16/nextjs-chat-openai/blob/main/public/image/ss-chats.png?raw=true "ss nextjs chat openai") | 61 | -------------------------------------------------------------------------------- /components/AnimateChats.js: -------------------------------------------------------------------------------- 1 | import { useAutoAnimate } from "@formkit/auto-animate/react"; 2 | 3 | export default function AnimateChats({ children }) { 4 | const [ref] = useAutoAnimate(); 5 | return
{children}
; 6 | } 7 | -------------------------------------------------------------------------------- /components/ClientSide.js: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | 3 | const ClientSide = ({ children }) => <>{children}; 4 | 5 | export default dynamic(() => Promise.resolve(ClientSide), { 6 | ssr: false, 7 | }); 8 | -------------------------------------------------------------------------------- /components/Icon.js: -------------------------------------------------------------------------------- 1 | const IconHand = ({ loading }) => { 2 | return ( 3 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | const IconRobot = () => { 16 | return ( 17 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | const IconTrash = () => { 28 | return ( 29 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | const IconProfile = () => { 40 | return ( 41 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | const IconLoadingHourglass = () => { 52 | return ( 53 | 58 | 59 | 60 | ); 61 | }; 62 | 63 | const IconSend = () => { 64 | return ( 65 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | const IconLoadingSend = () => { 76 | return ( 77 | 82 | 83 | 84 | ); 85 | }; 86 | 87 | export { 88 | IconHand, 89 | IconRobot, 90 | IconTrash, 91 | IconProfile, 92 | IconLoadingHourglass, 93 | IconSend, 94 | IconLoadingSend, 95 | }; 96 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-chat-openai", 3 | "version": "1.0.0", 4 | "private": false, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@formkit/auto-animate": "^1.0.0-beta.6", 13 | "@headlessui/react": "^1.7.13", 14 | "@next/font": "^13.2.4", 15 | "axios": "^1.3.4", 16 | "dayjs": "^1.11.7", 17 | "eslint": "^8.36.0", 18 | "eslint-config-next": "^13.2.4", 19 | "next": "^13.2.4", 20 | "openai": "^3.2.1", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-hot-toast": "^2.4.0", 24 | "zustand": "^4.3.6" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "^10.4.14", 28 | "postcss": "^8.4.21", 29 | "tailwindcss": "^3.2.7" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | export default function App({ Component, pageProps }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/chat.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | const { Configuration, OpenAIApi } = require("openai"); 3 | 4 | export default async function handler(req, res) { 5 | // if method not post send this 6 | if (req.method !== "POST") { 7 | res.status(405).send({ message: "Only POST requests allowed!!" }); 8 | return; 9 | } 10 | // if method post then do it 11 | if (req.method === "POST") { 12 | // req body with name is chat 13 | const chat = req.body.chat; 14 | // if there is a chat 15 | if (chat) { 16 | // configuration api openai 17 | const configuration = new Configuration({ 18 | apiKey: process.env.API_KEY, 19 | }); 20 | const openai = new OpenAIApi(configuration); 21 | const response = await openai.createCompletion({ 22 | model: "text-davinci-003", 23 | prompt: chat, 24 | temperature: 0, 25 | max_tokens: 500, 26 | }); 27 | // if get data from openai send json 28 | if (response.data?.choices) { 29 | res.json(response.data.choices[0].text); 30 | // if can't get data from openai send error 31 | } else { 32 | res.status(500).send("Oops, Something went wrong!!"); 33 | } 34 | // if no chat send error not found 35 | } else { 36 | res.status(404).send("Please, write your chat!!"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { Fragment, useEffect, useRef, useState } from "react"; 3 | import { ChatStore } from "../store/ChatStore"; 4 | import dayjs from "dayjs"; 5 | var relativeTime = require("dayjs/plugin/relativeTime"); 6 | dayjs.extend(relativeTime); 7 | import { 8 | IconHand, 9 | IconLoadingHourglass, 10 | IconLoadingSend, 11 | IconProfile, 12 | IconRobot, 13 | IconSend, 14 | IconTrash, 15 | } from "../components/Icon"; 16 | import ClientSide from "../components/ClientSide"; 17 | import AnimateChats from "../components/AnimateChats"; 18 | import { Dialog } from "@headlessui/react"; 19 | import { Toaster } from "react-hot-toast"; 20 | 21 | export default function PageHome() { 22 | // store chats 23 | const { chats, chat, addChat, loading, removeAllChat, removeOneChat } = 24 | ChatStore((state) => state); 25 | 26 | // state text 27 | const [text, setText] = useState(""); 28 | 29 | // handler form submit 30 | const handlerSubmitChat = (event) => { 31 | event.preventDefault(); 32 | // if text greater than 0 and less than 300 character do it 33 | if (text.length > 0 && text.length <= 300) { 34 | // store text to addChat store 35 | addChat(text); 36 | // set text to default 37 | setText(""); 38 | } 39 | }; 40 | 41 | // format date 42 | const formatDate = (date) => { 43 | return dayjs().to(dayjs(date)); 44 | }; 45 | 46 | // ref 47 | const chatRef = useRef(null); 48 | const loadingRef = useRef(null); 49 | 50 | useEffect(() => { 51 | // if there is a new chat scroll to them 52 | if (chats && chatRef?.current) { 53 | chatRef?.current?.scrollIntoView({ behavior: "smooth" }); 54 | } 55 | }, [chats, chatRef]); 56 | 57 | useEffect(() => { 58 | // if there is a loading scroll to them 59 | if (loading && loadingRef?.current) { 60 | loadingRef?.current?.scrollIntoView({ behavior: "smooth" }); 61 | } 62 | }, [loading, loadingRef]); 63 | 64 | // state modal remove all 65 | const [modalRemoveAll, setModalRemoveAll] = useState(false); 66 | 67 | // state modal remove one 68 | const [modalRemoveOne, setModalRemoveOne] = useState(); 69 | 70 | return ( 71 | // client side it means client side rendering 72 | 73 | 74 | 75 | NextJS Chat OpenAI 76 | 77 |
78 |
79 | {/* header */} 80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | 93 |
94 |
95 | Bot OpenAI 96 |
97 | 98 | {loading ? "Typing..." : "Online"} 99 | 100 |
101 |
102 | 103 | {chats.length > 1 && ( 104 |
105 | 108 | 109 | setModalRemoveAll(false)}> 114 |
115 |
116 |
117 | 118 | 121 | Are you sure ? 122 | 123 | 124 | Are you sure delete all chat ? 125 | 126 |
127 | 133 | 141 |
142 |
143 |
144 |
145 |
146 |
147 | )} 148 |
149 |
150 | 151 | {/* chats */} 152 |
153 | <> 154 | {chats?.length === 0 && ( 155 |
158 |
159 |

No message here...

160 |

Send a message or tap the greeting icon below

161 |
162 | 168 |
169 |
170 |
171 | )} 172 | 173 | {chats?.length > 0 && 174 | chats?.map((item, index) => ( 175 | 176 |
177 |
178 |
179 |

180 | {item.chat} 181 |

182 |
183 | 184 | {formatDate(item.date)} 185 | 186 |
187 | 188 |
189 | 194 |
195 | 196 | setModalRemoveOne()}> 201 |
202 |
203 |
204 | 205 | 208 | Are you sure ? 209 | 210 | 211 | Are you sure delete chat {item.chat} ? 212 | 213 |
214 | 220 | 228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 | 237 |
238 |
239 |
240 |

241 | {item.answer} 242 |

243 |
244 | 245 | {formatDate(item.date)} 246 | 247 |
248 |
249 |
250 | ))} 251 |
252 | {chats?.length > 0 && chat?.chat && ( 253 |
254 |
255 |
256 |

{chat.chat}

257 |
258 | 259 | {formatDate(chat.date)} 260 | 261 |
262 | 263 |
264 | 267 |
268 |
269 | )} 270 | {loading && ( 271 |
274 | 275 |
276 | )} 277 | 278 |
279 | 280 | {/* input chat */} 281 |
282 |
283 |
284 | setText(e.target.value)} 290 | /> 291 | 296 |
297 |
298 |
299 |
300 |
301 |
302 | ); 303 | } 304 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadi-16/nextjs-chat-openai/68bf909a312276052b8c7bc53e7f26bf1517b4a2/public/favicon.ico -------------------------------------------------------------------------------- /public/image/ss-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadi-16/nextjs-chat-openai/68bf909a312276052b8c7bc53e7f26bf1517b4a2/public/image/ss-chat.png -------------------------------------------------------------------------------- /public/image/ss-chats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hadi-16/nextjs-chat-openai/68bf909a312276052b8c7bc53e7f26bf1517b4a2/public/image/ss-chats.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /store/ChatStore.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { toast } from "react-hot-toast"; 3 | import { create } from "zustand"; 4 | import { createJSONStorage, persist } from "zustand/middleware"; 5 | 6 | // initial chat store 7 | export const ChatStore = create( 8 | persist( 9 | (set, get) => ({ 10 | // default array chats 11 | chats: [], 12 | // latest chat 13 | chat: {}, 14 | // default loading false 15 | loading: false, 16 | // store chat to api 17 | addChat: async (inputChat) => { 18 | try { 19 | // set new latest chat 20 | set(() => ({ chat: { chat: inputChat, date: new Date() } })); 21 | // set loading true 22 | set(() => ({ loading: true })); 23 | // post data input to api 24 | const { data } = await axios.post("/api/chat", { 25 | chat: inputChat, 26 | }); 27 | // data chat object 28 | const dataChat = { 29 | // input chat 30 | chat: inputChat, 31 | // answer from api 32 | answer: data, 33 | // current date 34 | date: new Date(), 35 | }; 36 | // set default latest chat 37 | set(() => ({ chat: {} })); 38 | // set new data chat from api to new array 39 | set((state) => ({ 40 | chats: [...state.chats, dataChat], 41 | loading: false, 42 | })); 43 | } catch (err) { 44 | // toast error 45 | toast.error( 46 | err.response && err.response.data.message 47 | ? err.response.data.message 48 | : err.message 49 | ); 50 | // console.log(err); 51 | set(() => ({ chat: {} })); 52 | set(() => ({ loading: false })); 53 | } 54 | }, 55 | // remove one chat 56 | removeOneChat: (item) => { 57 | // toast success 58 | toast.success(`Success delete ${item.chat}`); 59 | // remove one chat by index 60 | set((state) => ({ 61 | chats: state.chats.filter((x) => x !== item), 62 | })); 63 | }, 64 | // remove all chats 65 | removeAllChat: () => { 66 | // toast success 67 | toast.success(`Success delete all chats`); 68 | set({ chats: [] }); 69 | }, 70 | }), 71 | // set local storage 72 | { 73 | name: "next-openai-chats", 74 | storage: createJSONStorage(() => localStorage), 75 | } 76 | ) 77 | ); 78 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | scroll-behavior: smooth; 7 | } 8 | 9 | .input-chat:focus ~ .change-color { 10 | @apply fill-blue-600; 11 | } 12 | 13 | .loadingIcon { 14 | animation: shake 1.5s; 15 | animation-iteration-count: infinite; 16 | } 17 | 18 | @keyframes shake { 19 | 0% { 20 | transform: rotate(0deg); 21 | } 22 | 25% { 23 | transform: rotate(25deg); 24 | } 25 | 50% { 26 | transform: rotate(-25deg); 27 | } 28 | 75% { 29 | transform: rotate(25deg); 30 | } 31 | 100% { 32 | transform: rotate(-25deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | --------------------------------------------------------------------------------