├── .env.example
├── .eslintrc.cjs
├── LICENSE
├── README.md
├── api
├── completions.js
└── counter.js
├── index.html
├── package.json
├── public
└── images
│ └── chatgpt-logo.svg
├── server.js
├── src
├── App.jsx
├── index.css
└── main.jsx
├── vite.config.js
└── vite.env.d.ts
/.env.example:
--------------------------------------------------------------------------------
1 | # Copy all variables to .env file and change the appropriate variables with your values
2 | OPENAI_API_KEY="moBeModified"
3 | GPT_MODEL_NAME="moBeModified"
4 | PORT=8000
5 | VITE_API_URL="http://localhost:8000"
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | "eslint:recommended",
5 | "plugin:react/recommended",
6 | "plugin:react/jsx-runtime",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
10 | settings: { react: { version: "18.2" } },
11 | plugins: ["react-refresh", "dotenv"],
12 | rules: {
13 | "react-refresh/only-export-components": "warn",
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Volodymyr Gerun
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React ChatGPT Clone
2 |
3 | The ChatGPT clone uses chat completion v1 API with your gpt model and express.js to run a server requesting client-side requests.
4 |
5 | ## Screenshots
6 |
7 |
8 |
9 | ## Installation
10 |
11 | Copy all variables from `.env.example` to new created `.env` file and change the appropriate variables with your values. You can get all necessary data at [Platform OpenAI](https://platform.openai.com/api-keys).
12 |
13 | Install dependencies.
14 |
15 | ```bash
16 | npm i
17 | ```
18 |
19 | Start backend server with nodemon.
20 |
21 | ```bash
22 | npm run dev:back
23 | ```
24 |
25 | Start frontend server with vite.
26 |
27 | ```bash
28 | npm run dev:front
29 | ```
30 |
31 | ## License
32 |
33 | [MIT License](LICENSE)
34 |
--------------------------------------------------------------------------------
/api/completions.js:
--------------------------------------------------------------------------------
1 | import { Ratelimit } from '@upstash/ratelimit';
2 | import { Redis } from '@upstash/redis';
3 | import { Resend } from 'resend';
4 |
5 | const resend = new Resend(process.env.RESEND_API_KEY);
6 |
7 | const ratelimit = new Ratelimit({
8 | redis: Redis.fromEnv(),
9 | limiter: Ratelimit.slidingWindow(1, '1 d'),
10 | analytics: true,
11 | /**
12 | * Optional prefix for the keys used in redis. This is useful if you want to share a redis
13 | * instance with other applications and want to avoid key collisions. The default prefix is
14 | * "@upstash/ratelimit"
15 | */
16 | prefix: '@gpt-clone/ratelimit',
17 | });
18 |
19 | // export default async function handler(req, res) {
20 | // try {
21 | // if (req.headers.authorization !== process.env.AUTH_TOKEN) {
22 | // return res.status(401).send('Unauthorized');
23 | // }
24 |
25 | // const ip =
26 | // req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
27 |
28 | // const { success } = await ratelimit.limit(ip);
29 |
30 | // if (!success) {
31 | // return res
32 | // .status(429)
33 | // .send('You have reached the maximum number of requests per hour.');
34 | // }
35 |
36 | // if (process.env.IS_RESEND_ENABLE === 'true') {
37 | // resend.emails.send({
38 | // from: 'react-chatgpt-clone@resend.dev',
39 | // to: process.env.RESEND_EMAIL,
40 | // subject: 'User prompt',
41 | // html: `
User ${ip} sent ${req.body.message} prompt.
`, 42 | // }); 43 | // } 44 | 45 | // const response = await fetch('https://api.openai.com/v1/chat/completions', { 46 | // method: 'POST', 47 | // headers: { 48 | // Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 49 | // 'Content-Type': 'application/json', 50 | // }, 51 | // body: JSON.stringify({ 52 | // model: process.env.GPT_MODEL_NAME, 53 | // messages: [ 54 | // { 55 | // role: 'user', 56 | // content: req.body.message, 57 | // }, 58 | // ], 59 | // }), 60 | // }); 61 | 62 | // return res.send(await response.json()); 63 | // } catch (error) { 64 | // return res.status(500).send(error.message || error.toString()); 65 | // } 66 | // } 67 | -------------------------------------------------------------------------------- /api/counter.js: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis"; 2 | 3 | const redis = Redis.fromEnv(); 4 | 5 | export default async function handler(req, res) { 6 | const count = await redis.incr("counter"); 7 | return res.status(200).json({ count }); 8 | } 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |User ${ip} sent ${req.body.message} prompt.
`, 37 | }); 38 | } 39 | 40 | const options = { 41 | method: "POST", 42 | headers: { 43 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 44 | "Content-Type": "application/json", 45 | }, 46 | body: JSON.stringify({ 47 | model: process.env.GPT_MODEL_NAME, 48 | messages: [ 49 | { 50 | role: "user", 51 | content: req.body.message, 52 | }, 53 | ], 54 | }), 55 | }; 56 | 57 | try { 58 | const response = await fetch( 59 | "https://api.openai.com/v1/chat/completions", 60 | options 61 | ); 62 | 63 | const data = await response.json(); 64 | 65 | res.send(data); 66 | } catch (e) { 67 | console.error(e); 68 | res.status(500).send(e.message); 69 | } 70 | }); 71 | 72 | app.listen(process.env.PORT, () => { 73 | console.log( 74 | `Server is running on http://localhost:${process.env.PORT}/api/completions` 75 | ); 76 | }); 77 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | useEffect, 4 | useRef, 5 | useCallback, 6 | useLayoutEffect, 7 | } from 'react'; 8 | import { BiPlus, BiUser, BiSend, BiSolidUserCircle } from 'react-icons/bi'; 9 | import { MdOutlineArrowLeft, MdOutlineArrowRight } from 'react-icons/md'; 10 | 11 | function App() { 12 | const [text, setText] = useState(''); 13 | const [message, setMessage] = useState(null); 14 | const [previousChats, setPreviousChats] = useState([]); 15 | const [localChats, setLocalChats] = useState([]); 16 | const [currentTitle, setCurrentTitle] = useState(null); 17 | const [isResponseLoading, setIsResponseLoading] = useState(false); 18 | const [errorText, setErrorText] = useState(''); 19 | const [isShowSidebar, setIsShowSidebar] = useState(false); 20 | const scrollToLastItem = useRef(null); 21 | 22 | const createNewChat = () => { 23 | setMessage(null); 24 | setText(''); 25 | setCurrentTitle(null); 26 | }; 27 | 28 | const backToHistoryPrompt = (uniqueTitle) => { 29 | setCurrentTitle(uniqueTitle); 30 | setMessage(null); 31 | setText(''); 32 | }; 33 | 34 | const toggleSidebar = useCallback(() => { 35 | setIsShowSidebar((prev) => !prev); 36 | }, []); 37 | 38 | const submitHandler = async (e) => { 39 | e.preventDefault(); 40 | return setErrorText('My billing plan is gone because of many requests.'); 41 | if (!text) return; 42 | 43 | setIsResponseLoading(true); 44 | setErrorText(''); 45 | 46 | const options = { 47 | method: 'POST', 48 | headers: { 49 | 'Content-Type': 'application/json', 50 | 'Authorization': import.meta.env.VITE_AUTH_TOKEN, 51 | }, 52 | body: JSON.stringify({ 53 | message: text, 54 | }), 55 | }; 56 | 57 | try { 58 | const response = await fetch( 59 | `${import.meta.env.VITE_API_URL}/api/completions`, 60 | options 61 | ); 62 | 63 | if (response.status === 429) { 64 | return setErrorText('Too many requests, please try again later.'); 65 | } 66 | 67 | const data = await response.json(); 68 | 69 | if (data.error) { 70 | setErrorText(data.error.message); 71 | setText(''); 72 | } else { 73 | setErrorText(false); 74 | } 75 | 76 | if (!data.error) { 77 | setErrorText(''); 78 | setMessage(data.choices[0].message); 79 | setTimeout(() => { 80 | scrollToLastItem.current?.lastElementChild?.scrollIntoView({ 81 | behavior: 'smooth', 82 | }); 83 | }, 1); 84 | setTimeout(() => { 85 | setText(''); 86 | }, 2); 87 | } 88 | } catch (e) { 89 | setErrorText(e.message); 90 | console.error(e); 91 | } finally { 92 | setIsResponseLoading(false); 93 | } 94 | }; 95 | 96 | useLayoutEffect(() => { 97 | const handleResize = () => { 98 | setIsShowSidebar(window.innerWidth <= 640); 99 | }; 100 | handleResize(); 101 | 102 | window.addEventListener('resize', handleResize); 103 | 104 | return () => { 105 | window.removeEventListener('resize', handleResize); 106 | }; 107 | }, []); 108 | 109 | useEffect(() => { 110 | const storedChats = localStorage.getItem('previousChats'); 111 | 112 | if (storedChats) { 113 | setLocalChats(JSON.parse(storedChats)); 114 | } 115 | }, []); 116 | 117 | useEffect(() => { 118 | if (!currentTitle && text && message) { 119 | setCurrentTitle(text); 120 | } 121 | 122 | if (currentTitle && text && message) { 123 | const newChat = { 124 | title: currentTitle, 125 | role: 'user', 126 | content: text, 127 | }; 128 | 129 | const responseMessage = { 130 | title: currentTitle, 131 | role: message.role, 132 | content: message.content, 133 | }; 134 | 135 | setPreviousChats((prevChats) => [...prevChats, newChat, responseMessage]); 136 | setLocalChats((prevChats) => [...prevChats, newChat, responseMessage]); 137 | 138 | const updatedChats = [...localChats, newChat, responseMessage]; 139 | localStorage.setItem('previousChats', JSON.stringify(updatedChats)); 140 | } 141 | }, [message, currentTitle]); 142 | 143 | const currentChat = (localChats || previousChats).filter( 144 | (prevChat) => prevChat.title === currentTitle 145 | ); 146 | 147 | const uniqueTitles = Array.from( 148 | new Set(previousChats.map((prevChat) => prevChat.title).reverse()) 149 | ); 150 | 151 | const localUniqueTitles = Array.from( 152 | new Set(localChats.map((prevChat) => prevChat.title).reverse()) 153 | ).filter((title) => !uniqueTitles.includes(title)); 154 | 155 | return ( 156 | <> 157 |Ongoing
167 |Previous
192 |Upgrade plan
219 |User
223 |You
271 |{chatMsg.content}
272 |ChatGPT
276 |{chatMsg.content}
277 |{errorText}
} 286 | {errorText && ( 287 |288 | *You can clone the repository and use your paid OpenAI API key 289 | to make this work. 290 |
291 | )} 292 | 307 |308 | ChatGPT can make mistakes. Consider checking important 309 | information. 310 |
311 |