├── .eslintrc.json
├── .env.example
├── public
├── favicon.ico
├── image
│ ├── ss-chat.png
│ └── ss-chats.png
├── vercel.svg
├── thirteen.svg
└── next.svg
├── postcss.config.js
├── next.config.js
├── pages
├── _app.js
├── _document.js
├── api
│ └── chat.js
└── index.js
├── components
├── ClientSide.js
├── AnimateChats.js
└── Icon.js
├── tailwind.config.js
├── .gitignore
├── styles
└── globals.css
├── package.json
├── README.md
└── store
└── ChatStore.js
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | // change this to .env
2 | API_KEY = your_apikey_from_openai
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadi-16/nextjs-chat-openai/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/image/ss-chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadi-16/nextjs-chat-openai/HEAD/public/image/ss-chat.png
--------------------------------------------------------------------------------
/public/image/ss-chats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hadi-16/nextjs-chat-openai/HEAD/public/image/ss-chats.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/globals.css'
2 |
3 | export default function App({ Component, pageProps }) {
4 | return
5 | }
6 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
8 |
9 |
10 |
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | |  |  |
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/Icon.js:
--------------------------------------------------------------------------------
1 | const IconHand = ({ loading }) => {
2 | return (
3 |
12 | );
13 | };
14 |
15 | const IconRobot = () => {
16 | return (
17 |
24 | );
25 | };
26 |
27 | const IconTrash = () => {
28 | return (
29 |
36 | );
37 | };
38 |
39 | const IconProfile = () => {
40 | return (
41 |
48 | );
49 | };
50 |
51 | const IconLoadingHourglass = () => {
52 | return (
53 |
60 | );
61 | };
62 |
63 | const IconSend = () => {
64 | return (
65 |
72 | );
73 | };
74 |
75 | const IconLoadingSend = () => {
76 | return (
77 |
84 | );
85 | };
86 |
87 | export {
88 | IconHand,
89 | IconRobot,
90 | IconTrash,
91 | IconProfile,
92 | IconLoadingHourglass,
93 | IconSend,
94 | IconLoadingSend,
95 | };
96 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
299 |
300 |
301 |
302 | );
303 | }
304 |
--------------------------------------------------------------------------------