├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── assets
└── qr.jpg
├── components
├── Chat.js
├── Chat.module.scss
├── Config.js
├── DropDown.tsx
├── Footer.tsx
├── GitHub.tsx
├── Header.tsx
├── LoadingDots.tsx
├── Messages.js
├── Messages.module.css
├── PromptElement.js
├── PromptElement.module.css
├── Shapes.js
├── logo.svg
├── notice.js
└── share.js
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── _document.tsx
├── account.js
├── account.module.css
├── api
│ └── chat.ts
└── index.tsx
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── 1-black.png
├── 2-black.png
├── analytics.png
├── app.png
├── assets
│ ├── group-qr-8-13.jpeg
│ └── qr.jpg
├── favicon.ico
├── manifest.json
├── notice.json
├── og-image.png
├── qr-prom.png
├── qr.jpg
├── robots.txt
├── screenshot.png
├── share.jpg
├── vercel.svg
├── vercelLogo.png
└── writingIcon.png
├── styles
├── globals.css
└── loading-dots.module.css
├── tailwind.config.js
├── tsconfig.json
└── utils
├── ChatServiceBridge.js
├── ObjCacheWrap.js
├── OpenAIStream.ts
├── Others.js
├── Settings.js
└── client
└── ChatGPTClient.js
/.env.example:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=
2 |
--------------------------------------------------------------------------------
/.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 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 | .env
38 |
39 | # idea
40 | .idea
41 |
42 | .next
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 FrontendEngineeringTeam
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 | # FullStack-ChatGPT-WebApp
2 |
3 | [](https://github.com/frontend-engineering/chatgpt-webapp-fullstack)
4 | 
5 | 
6 |
7 |
8 | 这是一个 ChatGPT 聊天应用,包含网页端App和一个Node服务,可快速部署一套自用的完整智能聊天服务([点击体验](https://www.webinfra.cloud))
9 |
10 | ## 功能特点
11 |
12 | * 全栈应用:包括 **网页端App** 和 **服务端Node服务**,适合全链路功能的二次开发
13 | * 快速部署:项目迁移到了Nextjs架构,前后端一键部署,无须一行代码,小白也可以发布自己的ChatGPT服务了
14 | * 无须运维:利用vercel的免费额度,摆脱繁琐的运维工作
15 | * 无须翻墙:针对国内无法直接访问OpenAI的限制,本服务部署完成后,可直接访问,无须科学上网
16 | * 多端适配:适配手机端和PC端,更多客户端功能迭代中
17 | * 上下文记忆:问了保障问答质量,缓存了提问记录
18 | * stream响应:支持stream的响应方式,更好的问答体验
19 | * 流量控制:支持单用户的调用流量限制,防止恶意盗刷Token
20 | * 用户充值:支持开启用户收费功能,用户可充值购买调用额度
21 |
22 | >> 功能开发快速迭代中,使用或部署时如果遇到任何问题,请加页面最下方微信交流群反馈
23 |
24 | https://github.com/frontend-engineering/chatgpt-webapp-fullstack/assets/9939767/eaef68ce-e73b-4dd5-9277-6b0f4f201455
25 |
26 |
27 |
28 | ## 一键部署
29 |
30 | 自动部署服务到Vercel
31 |
32 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ffrontend-engineering%2Fchatgpt-webapp-fullstack&project-name=private-chatgpt-service&repository-name=chatgpt-webapp-fullstack&demo-title=Demo%20Page&demo-description=%E7%A4%BA%E4%BE%8B%E9%A1%B9%E7%9B%AE&demo-url=https%3A%2F%2Fwebinfra.cloud)
33 |
34 | ### 部署依赖
35 |
36 | 需要额外配置vercel Edge Config 和 Vercel KV 两个store
37 | * Edge Config 用来管理项目配置
38 | * KV 用于存储用户聊天上下文的数据缓存
39 |
40 | 其中,Edge Config的初始配置可以参考
41 | ```
42 | {
43 | "chatGptClient": {
44 | "openaiApiKey": "sk-你的openai api key",
45 | "reverseProxyUrl": "",
46 | "modelOptions": {
47 | "model": "gpt-3.5-turbo",
48 | "max_tokens": 1000
49 | },
50 | "proxy": "",
51 | "debug": false
52 | },
53 | "apiOptions": {
54 | "port": 3000,
55 | "host": "0.0.0.0",
56 | "debug": false,
57 | "clientToUse": "chatgpt",
58 | "perMessageClientOptionsWhitelist": {
59 | "validClientsToUse": ["bing", "chatgpt", "chatgpt-browser"],
60 | "chatgpt": [
61 | "promptPrefix",
62 | "userLabel",
63 | "chatGptLabel",
64 | "modelOptions.temperature"
65 | ]
66 | }
67 | },
68 | "cacheOptions": {}
69 | }
70 | ```
71 |
72 | ## 项目结构
73 |
74 | 项目迁移到了NextJS,开发参考 [Next.js文档](https://nextjs.org/docs)
75 |
76 | ## 项目体验
77 |
78 | 打开 [DEMO](https://www.webinfra.cloud)
79 | 或扫码
80 |
81 |
82 |
83 |
84 |
85 |
86 | ## 更多功能
87 | 更多功能正在开发中,如有需要可以私聊,或者贡献PR
88 |
89 | * 前端WebApp功能补全
90 | * BingAI等多模型支持
91 | * 其他
92 |
93 | ## 私有部署
94 | 如有部署方面的问题,可关注公众号 webinfra 需求帮助
95 | 
96 |
97 | 或加交流群
98 |
99 |
100 |
101 |
102 | ## License
103 |
104 | [MIT](./LICENSE)
105 |
--------------------------------------------------------------------------------
/assets/qr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/frontend-engineering/chatgpt-webapp-fullstack/b43cdad8ec6e5a0466b4d63c6eab6e819bc13dbd/assets/qr.jpg
--------------------------------------------------------------------------------
/components/Chat.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { Toast, Button, Modal, TextArea, SafeArea, NoticeBar, Tag } from 'antd-mobile'
4 | import { PlayOutline, UserSetOutline } from 'antd-mobile-icons'
5 | import Cashier from '@cashier/web';
6 | import classnames from 'classnames';
7 | import { callBridge } from '../utils/ChatServiceBridge';
8 | import Messages from './Messages';
9 | import { useLocalStorage } from '../utils/Others';
10 | import PromotSelect from './PromptElement.js';
11 | import styles from './Chat.module.scss';
12 | import { appId, appToken, enableAuth } from './Config.js';
13 |
14 | function ChatComponent(props) {
15 | const router = useRouter();
16 | const [loginState, setLoginState] = useState(null)
17 | const [question, setQuestion] = useState("");
18 | const [outMsgs, setOutMsgs] = useLocalStorage('chat-out-msgs', []);
19 | const [retMsgs, setRetMsgs] = useLocalStorage('chat-ret-msgs', []);
20 | const [msgId, setMsgId] = useState('');
21 | const [convId, setConvId] = useState('');
22 | const [typing, setTyping] = useState(false);
23 | const [answerTS, setAnswerTS] = useState(new Date().valueOf());
24 | const [answer, setAnswer] = useState();
25 | const [hasNotice, setHasNotice] = useState('');
26 | const [abortSignal, setAbortSignal] = useState(null);
27 | const [selectingPrompt, setSelectingPrompt] = useState(false);
28 | const [selectedPrompt, setSelectedPrompt] = useLocalStorage('chat-selected-prompt', null);
29 |
30 | const answerTSRef = useRef();
31 | answerTSRef.current = answerTS;
32 |
33 | const answerRef = useRef();
34 | answerRef.current = answer;
35 |
36 | const abortSignalRef = useRef();
37 | abortSignalRef.current = abortSignal;
38 |
39 | const messagesEndRef = useRef(null)
40 |
41 | const sdkInsta = useMemo(() => {
42 | if (!enableAuth) {
43 | console.log('skip auth');
44 | return null;
45 | }
46 | return new Cashier({
47 | // 应用 ID
48 | appId,
49 | appToken,
50 | // domain: 'http://localhost:3333',
51 | pageDomain: 'https://pay.freecharger.cn',
52 | mobile: true,
53 | inPage: true,
54 | root: '#sdk-root'
55 | });
56 | }, []);
57 |
58 | const getLoginState = useCallback(async () => {
59 | if (!sdkInsta) return;
60 | try {
61 | console.log('get login state...');
62 | let state = await sdkInsta.getUserInfo();
63 | console.log('get loginState resp: ', state)
64 | if (!state) {
65 | state = await sdkInsta.login()
66 | }
67 | setLoginState(state);
68 | } catch (error) {
69 | console.error('login state: ', error);
70 | Toast.show({
71 | content: `初始化失败 - ${error?.message}`
72 | })
73 | }
74 | }, [sdkInsta]);
75 |
76 | const getLoginTokens = useCallback(async () => {
77 | if (!sdkInsta) return;
78 | let tokens = await sdkInsta.getTokens();
79 | console.log('get tokens: ', tokens)
80 | return tokens;
81 | }, [sdkInsta]);
82 |
83 |
84 | const scrollToBottom = () => {
85 | messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
86 | console.log('scroll to bottom');
87 | }
88 |
89 | const genRandomMsgId = () => {
90 | return `msg-${new Date().valueOf()}-${Math.floor(Math.random() * 10)}`;
91 | }
92 |
93 | const onChoosePrompt = (val) => {
94 | setSelectedPrompt(val)
95 | Toast.show({
96 | content: `预设模式更新为 - ${val.label}`
97 | })
98 | }
99 |
100 | const inputQuestion = val => {
101 | setQuestion(val);
102 | }
103 |
104 | const onmessage = (msgObj) => {
105 | console.log('onmsg - ', msgObj)
106 | setAnswer(lastAns => (lastAns || '') + msgObj);
107 | scrollToBottom()
108 | }
109 | function nextTick() {
110 | return new Promise(resolve => setTimeout(resolve, 0));
111 | }
112 | const onopen = async () => {
113 | console.log('open', new Date(), new Date(answerTSRef.current));
114 | setAnswerTS(new Date().valueOf());
115 | await nextTick();
116 | console.log('opened', new Date(), new Date(answerTSRef.current));
117 | }
118 |
119 | const onclose = () => {
120 | console.log('closed ', answerRef.current, new Date())
121 | if (answerRef.current) {
122 | setRetMsgs([...retMsgs, { id: genRandomMsgId(), msg: answerRef.current, timestamp: answerTSRef.current }])
123 | setAnswer('')
124 | setTyping(false);
125 | setAnswerTS(new Date().valueOf());
126 | }
127 | }
128 | const onerror = (message) => {
129 | console.log("error: ", answerRef.current);
130 | if (answerRef.current) {
131 | if (answerRef.current) {
132 | setRetMsgs([...retMsgs, { id: genRandomMsgId(), msg: answerRef.current + '...', timestamp: answerTSRef.current }])
133 | }
134 | setAnswer('')
135 | setTyping(false);
136 | } else {
137 | console.log('chatGPT error msg: ', message);
138 | throw new Error('ChatGPT 用量饱和,请稍后重试')
139 | }
140 | }
141 |
142 | const directChat = async function (e) {
143 | e.preventDefault();
144 | if (!question) {
145 | Toast.show({
146 | content: '请输入有效问题',
147 | })
148 | return;
149 | }
150 |
151 | if (!loginState && enableAuth) {
152 | Toast.show({
153 | content: '请先登录'
154 | })
155 | return;
156 | }
157 |
158 | setQuestion('');
159 | setOutMsgs([...outMsgs, { id: genRandomMsgId(), msg: question, timestamp: new Date().valueOf() }])
160 |
161 | setAbortSignal(null);
162 | setTyping(true);
163 | // 向云服务发起调用
164 | try {
165 | const tokens = await getLoginTokens();
166 |
167 | const callRes = await callBridge({
168 | data: {
169 | uid: loginState?.id,
170 | at: tokens?.accessToken,
171 | message: question,
172 | parentMessageId: msgId,
173 | conversationId: convId,
174 | prompt: selectedPrompt?.prompt
175 | },
176 | onmessage,
177 | onopen,
178 | onclose,
179 | onerror,
180 | getSignal: (sig) => {
181 | setAbortSignal(sig);
182 | },
183 | debug: props.debug
184 | })
185 |
186 | console.log('client stream result: ', abortSignalRef.current, callRes, answerTSRef.current, new Date(answerTSRef.current));
187 | const { response, messageId, conversationId } = callRes || {}
188 |
189 | const curMsgId = messageId || genRandomMsgId()
190 | setMsgId(curMsgId);
191 |
192 | if (conversationId) {
193 | setConvId(conversationId);
194 | }
195 |
196 | setTyping(false);
197 | setRetMsgs([...retMsgs, { id: curMsgId, msg: response, timestamp: answerTSRef.current }])
198 | setAnswer('')
199 | setAnswerTS(Date.now())
200 |
201 | return callRes;
202 | } catch (error) {
203 | console.error('call service error: ', error);
204 | setRetMsgs([...retMsgs, { id: genRandomMsgId(), msg: error?.message || '在线人数太多啦,请稍后再试', timestamp: new Date().valueOf() }])
205 | setTyping(false);
206 | }
207 | }
208 |
209 | const gotoAccount = (e) => {
210 | e.preventDefault();
211 | console.log('goto account page ....')
212 | if (enableAuth) {
213 | router.push('/account')
214 | }
215 | }
216 |
217 | const deleteItem = (id, type) => {
218 | if (!id) {
219 | Toast.show({
220 | icon: 'fail',
221 | content: '该对话已过期',
222 | })
223 | return;
224 | }
225 | Modal.confirm({
226 | content: '确认删除该对话',
227 | confirmText: '删除',
228 | cancelText: '取消',
229 | onCancel: () => {
230 | console.log('close modal')
231 | Modal.clear()
232 | },
233 | onConfirm: () => {
234 | if (type === 'incoming') {
235 | setRetMsgs([...(retMsgs.filter(item => item.id !== id))])
236 | } else {
237 | setOutMsgs([...(outMsgs.filter(item => item.id !== id))])
238 | }
239 |
240 | Modal.clear();
241 | }
242 | })
243 | }
244 | const editItem = (txt) => {
245 | setQuestion(txt);
246 | }
247 |
248 |
249 | useEffect(() => {
250 | if (sdkInsta) {
251 | sdkInsta.init()
252 | .then((resp) => {
253 | if (resp) {
254 | // 加载到了操作结果数据,反馈登录/购买结果给用户
255 | console.log('found resp: ', resp);
256 | }
257 | return getLoginState()
258 | })
259 | }
260 | }, [sdkInsta, getLoginState]);
261 |
262 | useEffect(() => {
263 | setTimeout(() => {
264 | scrollToBottom();
265 | }, 300)
266 | }, []);
267 |
268 | const onCancelChat = (e) => {
269 | e.preventDefault();
270 | console.log('=============user===cancel============');
271 | abortSignalRef.current?.abort();
272 | // TODO: Send cancel feedback to server
273 | setTyping(false);
274 | }
275 |
276 | return (
277 |
278 |
279 |
280 |
281 |
{loginState?.name?.toUpperCase().slice(0, 2) || 'W'}
282 |
283 |
WebInfra
284 |
285 |
286 |
287 |
288 | { hasNotice ?
289 | { setHasNotice('') }} />
290 |
: null }
291 |
292 |
293 |
{ item && (item.type = 'incoming'); return item })}
295 | outMsgs={outMsgs.map(item => { item && (item.type = 'outgoing'); return item })}
296 | userInfo={loginState}
297 | onItemDeleted={deleteItem}
298 | onEdit={editItem} />
299 |
300 |
301 |
302 |
303 |
304 |
{ setSelectingPrompt(!selectingPrompt); }}>{ selectedPrompt?.label || '默认模式' }
305 | { selectingPrompt ?
{ setSelectingPrompt(false) }} /> : null }
306 |
307 |
308 | {/*
*/}
309 |
317 | {typing &&
318 |
319 |
320 | 取消
321 |
322 |
323 | }
324 | {typing ?
325 |
:
332 |
333 |
directChat(e)} >
334 |
335 |
336 |
337 | }
338 |
339 |
340 |
341 |
342 |
)
343 | }
344 |
345 | export default ChatComponent;
--------------------------------------------------------------------------------
/components/Chat.module.scss:
--------------------------------------------------------------------------------
1 | .top-bar .adm-space {
2 | height: 100%;
3 | width: 100%;
4 | }
5 | .adm-notice-bar {
6 | --height: 30px;
7 | }
8 | .adm-notice-bar.adm-notice-bar-alert {
9 | --background-color: #F9FBFF;
10 | --border-color: none;
11 | --text-color: #03915d;
12 | }
13 | .chat-container .adm-list-item,
14 | .chat-container .adm-list-item-content,
15 | .chat-container .adm-list-item-content-main {
16 | max-width: inherit;
17 | display: block;
18 | }
19 | .container {
20 | position: relative;
21 | margin: 0 auto;
22 | width: 100%;
23 | max-width: 620px;
24 | height: 100vh;
25 | text-align: left;
26 | background: #F9FBFF;
27 | }
28 |
29 | .messages {
30 | position: absolute;
31 | background: #F9FBFF;
32 | opacity: 0.5;
33 | width: 30%;
34 | height: 70%;
35 | top: 2.5%;
36 | left: 5%;
37 | border-radius: 10px 0 0 10px;
38 | box-shadow: -5px 5px 10px rgba(119, 119, 119, 0.5);
39 | }
40 |
41 | .chatbox {
42 | position: absolute;
43 | /* left: 35%; */
44 | height: 100%;
45 | width: 100%;
46 | border-radius: 10px;
47 | // box-shadow: 5px 5px 15px rgb(119 119 119 / 50%);
48 | display: flex;
49 | flex-direction: column;
50 | flex-wrap: nowrap;
51 | justify-content: space-between;
52 | align-items: stretch;
53 | max-width: inherit;
54 | }
55 |
56 | .top-bar {
57 | width: 100%;
58 | height: 60px;
59 | background: #F9FBFF;
60 | border-radius: 10px 10px 0 0;
61 | position: fixed;
62 | top: 0;
63 | z-index: 9;
64 | display: flex;
65 | justify-content: space-between;
66 | align-items: center;
67 | padding: 0 15px;
68 | box-sizing: border-box;
69 | max-width: inherit;
70 | }
71 |
72 | .bottom-bar {
73 | position: fixed;
74 | bottom: 0;
75 | width: 100%;
76 | /* height: 55px; */
77 | bottom: 0;
78 | background: #F9FBFF;
79 | border-radius: 0 0 10px 10px;
80 | z-index: 9;
81 | max-width: inherit;
82 | }
83 | .notice {
84 | position: fixed;
85 | top: 60px;
86 | height: 30px;
87 | width: 100%;
88 | z-index: 9;
89 | max-width: inherit;
90 | overflow: hidden;
91 | --height: 30px;
92 | }
93 |
94 | .avatar {
95 | width: 35px;
96 | height: 35px;
97 | background: linear-gradient(to bottom left, #79C7C5 20%, #A1E2D9 100%);
98 | border-radius: 50%;
99 | /* position: absolute; */
100 | top: 11px;
101 | left: 15px;
102 | overflow: hidden;
103 | }
104 |
105 | .avatar p {
106 | color: #F9FBFF;
107 | line-height: 35px;
108 | box-sizing: border-box;
109 | margin: 0;
110 | text-align: center;
111 | }
112 |
113 | .avatar {
114 | img, svg {
115 | height: 100%;
116 | width: auto;
117 | cursor: pointer;
118 | display: flex;
119 | justify-content: center;
120 | }
121 | }
122 |
123 | .name {
124 | /* position: absolute; */
125 | top: 22px;
126 | text-transform: uppercase;
127 | color: #777777;
128 | font-size: 0.8em;
129 | letter-spacing: 2px;
130 | font-weight: 500;
131 | left: 60px;
132 | flex: 1 1 auto;
133 | left: 60px;
134 | text-align: center;
135 | }
136 |
137 | .count {
138 | color: #777777;
139 | font-size: 0.8em;
140 | letter-spacing: 2px;
141 | font-weight: 500;
142 | display: flex;
143 | align-items: center;
144 | justify-content: center;
145 | flex: 1 1 auto;
146 | }
147 |
148 | .count span {
149 | color: #19c37d;
150 | font-weight: bolder;
151 | font-size: 0.9rem;
152 | }
153 |
154 | .menu {
155 | /* position: absolute; */
156 | right: 10px;
157 | top: 20px;
158 | width: 66px;
159 | height: 20px;
160 | cursor: pointer;
161 | display: flex;
162 | justify-content: space-between;
163 | flex-direction: row-reverse;
164 | svg {
165 | width: 20px;
166 | height: 20px;
167 | &:hover {
168 | transform: scale(1.1);
169 | transition: all 0.3s ease-in;
170 | }
171 | }
172 | }
173 |
174 |
175 | .icons {
176 | position: absolute;
177 | color: #A1E2D9;
178 | padding: 10px;
179 | top: 5px;
180 | right: 21px;
181 | cursor: pointer;
182 | }
183 |
184 | .icons .fas {
185 | padding: 5px;
186 | opacity: 0.8;
187 | }
188 |
189 | .icons .fas:hover {
190 | transform: scale(1.1);
191 | transition: all 0.3s ease-in;
192 | }
193 |
194 | .dots {
195 | width: 4px;
196 | height: 4px;
197 | border-radius: 50%;
198 | background-color: #79C7C5;
199 | box-shadow: 0px 7px 0px #79C7C5, 0px 14px 0px #79C7C5;
200 | margin: 0 auto;
201 | }
202 |
203 | .middle {
204 | background: #F9FBFF;
205 | width: 100%;
206 | opacity: 1;
207 | margin-top: 60px;
208 | padding-bottom: 86px;
209 | flex: 1 1 auto;
210 | max-width: inherit;
211 | }
212 | .chat-container {
213 | display: flex;
214 | flex-direction: column;
215 | padding: 12px;
216 | --align-items: center;
217 | max-width: inherit;
218 | }
219 |
220 | .chat-bottom-line {
221 | height: 0;
222 | border-top: 1px solid #80808047;
223 | margin: 2px 0 4px;
224 | }
225 |
226 | /* .incoming {
227 | position: absolute;
228 | width: 50%;
229 | height: 100%;
230 | padding: 20px;
231 | } */
232 |
233 |
234 | .chat {
235 | width: 100%;
236 | display: flex;
237 | justify-content: space-between;
238 | padding: 8px 12px;
239 | box-sizing: border-box;
240 | position: relative;
241 | }
242 |
243 | .left {
244 | left: 0px;
245 | }
246 |
247 | // input {
248 | // padding: 7px;
249 | // width: 74%;
250 | // left: 5%;
251 | // border: 0;
252 | // top: 13px;
253 | // background: #F9FBFF;
254 | // color: #79C7C5;
255 | // flex: 1 1 auto;
256 | // box-sizing: border-box;
257 | // font-size: 16px;
258 | // }
259 |
260 | // input::placeholder {
261 | // color: #A1E2D9;
262 | // }
263 |
264 | // input:focus {
265 | // color: #777777;
266 | // outline: 0;
267 | // }
268 | .button-container {
269 | position: relative;
270 | }
271 |
272 | .button-container .button {
273 | border: 0;
274 | font-size: 1em;
275 | color: #A1E2D9;
276 | opacity: 0.8;
277 | cursor: pointer;
278 | outline: 0;
279 | flex: 0 0 auto;
280 | background-color: #ffffff;
281 | box-shadow: 0 0 1px 1px #a1e2d9b8;
282 | }
283 |
284 | .button-container .button:hover {
285 | transform: scale(1.1);
286 | transition: all 0.3s ease-in-out;
287 | color: #79C7C5;
288 | }
289 |
290 | .cancel-container {
291 | position: absolute;
292 | display: flex;
293 | justify-content: center;
294 | align-items: center;
295 | width: 100%;
296 | height: 100%;
297 | top: 0;
298 | left: 0;
299 | max-width: 100%;
300 | max-height: 100%;
301 | background: #f8f8f8;
302 | }
303 |
304 | .cancel-container .cancel {
305 | background: #b2b2b2;
306 | border: none;
307 | width: 100px;
308 | box-shadow: 0 0 3px 2px #9e9e9e75;
309 | opacity: 0.9;
310 | font-size: .875rem;
311 | line-height: 1.25rem;
312 | padding: 0.5rem 0.75rem;
313 | display: flex;
314 | justify-content: space-evenly;
315 | align-items: center;
316 | border-radius: 4px;
317 | color: rgb(18, 150, 219);
318 | }
319 |
320 | @keyframes bounce {
321 | 30% {
322 | transform: translateY(-2px);
323 | }
324 |
325 | 60% {
326 | transform: translateY(0px);
327 | }
328 |
329 | 80% {
330 | transform: translateY(2px);
331 | }
332 |
333 | 100% {
334 | transform: translateY(0px);
335 | opacity: 0.5;
336 | }
337 | }
338 |
339 | .prompt-container {
340 | border-bottom: 1px solid #a9a9a938;
341 | padding-top: 4px;
342 | margin: 0 12px;
343 | display: flex;
344 | align-items: center;
345 | }
346 |
347 | .prompt-container .prompt-tag {
348 | --background-color: #79C7C5 !important;
349 | --border-color: #79C7C5 !important;
350 | box-shadow: 0 0 3px 1px #9e9e9e75;
351 | margin-right: 10px;
352 | max-width: 120px;
353 | overflow: hidden;
354 | text-overflow: ellipsis;
355 | white-space: nowrap;
356 | --adm-font-size-3: 14px;
357 | }
358 |
359 |
360 | .typing {
361 | position: absolute;
362 | right: 10px;
363 | top: 0px;
364 | }
365 |
366 | .typing .bubble {
367 | background: #eaeaea;
368 | padding: 8px 13px 9px 13px;
369 | }
370 |
371 | .ellipsis {
372 | width: 5px;
373 | height: 5px;
374 | display: inline-block;
375 | background: #b7b7b7;
376 | border-radius: 50%;
377 | animation: bounce 1.3s linear infinite;
378 | }
379 |
380 | .one {
381 | animation-delay: 0.6s;
382 | }
383 |
384 | .two {
385 | animation-delay: 0.5s;
386 | }
387 |
388 | .three {
389 | animation-delay: 0.8s;
390 | }
391 |
--------------------------------------------------------------------------------
/components/Config.js:
--------------------------------------------------------------------------------
1 | export const appId = process.env.NEXT_PUBLIC_APP_ID;
2 | export const appToken = process.env.NEXT_PUBLIC_APP_TOKEN;
3 | export const enableAuth = process.env.NEXT_PUBLIC_ENABLE_AUTH === 'WebInfra';
--------------------------------------------------------------------------------
/components/DropDown.tsx:
--------------------------------------------------------------------------------
1 | import { Menu, Transition } from "@headlessui/react";
2 | import {
3 | CheckIcon,
4 | ChevronDownIcon,
5 | ChevronUpIcon,
6 | } from "@heroicons/react/20/solid";
7 | import { Fragment } from "react";
8 |
9 | function classNames(...classes: string[]) {
10 | return classes.filter(Boolean).join(" ");
11 | }
12 |
13 | export type VibeType = "Professional" | "Casual" | "Funny";
14 |
15 | interface DropDownProps {
16 | vibe: VibeType;
17 | setVibe: (vibe: VibeType) => void;
18 | }
19 |
20 | let vibes: VibeType[] = ["Professional", "Casual", "Funny"];
21 |
22 | export default function DropDown({ vibe, setVibe }: DropDownProps) {
23 | return (
24 |
25 |
26 |
27 | {vibe}
28 |
32 |
36 |
37 |
38 |
39 |
48 |
52 |
53 | {vibes.map((vibeItem) => (
54 |
55 | {({ active }) => (
56 | setVibe(vibeItem)}
58 | className={classNames(
59 | active ? "bg-gray-100 text-gray-900" : "text-gray-700",
60 | vibe === vibeItem ? "bg-gray-200" : "",
61 | "px-4 py-2 text-sm w-full text-left flex items-center space-x-2 justify-between"
62 | )}
63 | >
64 | {vibeItem}
65 | {vibe === vibeItem ? (
66 |
67 | ) : null}
68 |
69 | )}
70 |
71 | ))}
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function Footer() {
4 | return (
5 |
6 |
26 |
27 |
32 |
36 |
37 |
38 |
39 |
44 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/components/GitHub.tsx:
--------------------------------------------------------------------------------
1 | export default function Github({ className }: { className?: string }) {
2 | return (
3 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | export default function Header() {
5 | return (
6 |
7 |
8 |
15 |
16 | twitterBio.com
17 |
18 |
19 |
24 |
31 |
32 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/components/LoadingDots.tsx:
--------------------------------------------------------------------------------
1 | import styles from "../styles/loading-dots.module.css";
2 |
3 | const LoadingDots = ({
4 | color = "#000",
5 | style = "small",
6 | }: {
7 | color: string;
8 | style: string;
9 | }) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default LoadingDots;
20 |
21 | LoadingDots.defaultProps = {
22 | style: "small",
23 | };
24 |
--------------------------------------------------------------------------------
/components/Messages.js:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import dynamic from 'next/dynamic'
3 | import React, { useEffect, useState } from 'react';
4 | import { createRoot } from 'react-dom/client';
5 | import { flushSync } from 'react-dom';
6 | import classnames from 'classnames';
7 | import { List, Toast } from 'antd-mobile'
8 | import { UserOutline, EditSOutline } from 'antd-mobile-icons'
9 | import { fetch as fetchPolyfill } from 'whatwg-fetch'
10 | import ReactMarkdown from 'react-markdown'
11 | import { CopyBtn, DeleteBtn } from './Shapes';
12 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
13 | import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
14 | import styles from './Messages.module.css'
15 |
16 | const Messages = (props) => {
17 | console.log('msg got props: ', props);
18 | const { retMsgs, outMsgs, userInfo, onItemDeleted, onEdit } = props?.props || props || {};
19 | const [activeMsgId, setActiveMsgId] = useState(null);
20 |
21 | const toggleActiveItem = (id) => {
22 | return (e) => {
23 | e.preventDefault();
24 | if (!id) return;
25 | if (id === activeMsgId) {
26 | setActiveMsgId(null)
27 | } else {
28 | setActiveMsgId(id);
29 | }
30 | }
31 | }
32 |
33 | const rewriteItem = (text) => {
34 | return (e) => {
35 | e.preventDefault();
36 | e.stopPropagation();
37 | navigator.clipboard.writeText(text)
38 | onEdit && onEdit(text);
39 | }
40 | }
41 |
42 | const copyItem = (text, content = '内容已复制到剪贴板') => {
43 | return (e) => {
44 | e.preventDefault();
45 | e.stopPropagation();
46 | navigator.clipboard.writeText(text)
47 | Toast.show({
48 | icon: 'success',
49 | content: content,
50 | })
51 | }
52 | }
53 |
54 | const deleteItem = (id, type) => {
55 | return (e) => {
56 | e.preventDefault();
57 | e.stopPropagation();
58 | console.log('deleting item ', id)
59 | onItemDeleted && onItemDeleted(id, type);
60 | }
61 | }
62 |
63 | useEffect(() => {
64 | const { retMsgs, collecting } = props?.props || props || {};
65 | if (collecting) {
66 | return;
67 | }
68 | if (retMsgs) {
69 | const retMsgList = retMsgs.filter(item => !!item?.timestamp).sort((itemA, itemB) => (itemA?.timestamp - itemB.timestamp));
70 | setActiveMsgId(retMsgList[retMsgList.length - 1]?.id)
71 | }
72 | }, [props]);
73 |
74 | if ([...retMsgs, ...outMsgs].filter(item => !!item).length === 0) {
75 | return
76 | Hello!I'm a smart chat robot ^_^!
77 | This site is free to use and is only intended for learning and testing purposes.
78 | It is strictly prohibited to publish or distribute any illegal or inappropriate content on this website.
79 |
80 | }
81 | return (
82 | [
83 | ...retMsgs,
84 | ...outMsgs,
85 | ]
86 | .filter(item => !!item?.msg)
87 | .map(item => { if (!item.timestamp) { item.timestamp = new Date().valueOf() - 1000 * 100000}; return item; })
88 | .sort((itemA, itemB) => (itemA?.timestamp - itemB.timestamp))
89 | .map(ret => (
90 |
94 |
95 |
96 | {
97 | ret.type === 'incoming' ?
98 |
99 |
100 |
101 | :
102 | (userInfo?.avatar ?
:
)
103 | }
104 |
105 |
106 |
113 | {match[1]}
114 |
124 |
125 | ) : (
126 |
127 | {children}
128 |
129 | )
130 | }
131 | }}
132 | />
133 | {activeMsgId === ret.id ?
134 |
135 |
136 | { ret.type === 'incoming' ? null : }
137 |
: null}
138 |
139 |
140 | ))
141 | )
142 | }
143 |
144 |
145 | export const getShareHTML = async (props) => {
146 | const div = document.createElement('div');
147 | const root = createRoot(div);
148 | const { userInfo } = props?.props || props || {}
149 | flushSync(() => {
150 | root.render(
151 |
152 |
153 | {
154 | userInfo?.avatar ?
:
{ (userInfo?.name?.slice(0, 1) || 'C').toUpperCase() }
155 | }
156 |
157 |
ChatGPT - @webinfra
158 |
159 |
164 |
);
165 | });
166 | console.log('html contents got');
167 |
168 | const allCssStylesheetsLinks = [];
169 | const stylesheets = document.styleSheets;
170 |
171 | // looping through each stylesheet
172 | // and checksing if there is href property in each item
173 | for (let i = 0; i < stylesheets.length; i++) {
174 | if (stylesheets[i].href) {
175 | // prod
176 | allCssStylesheetsLinks.push(stylesheets[i].href);
177 | } else {
178 | // dev
179 | // TODO: collect styles
180 | }
181 | }
182 |
183 | const promiseList = allCssStylesheetsLinks.map(ssUrl => {
184 | console.log('fetching...', ssUrl, fetchPolyfill);
185 | if (!ssUrl) return '';
186 | return fetchPolyfill(ssUrl, {
187 | headers: {
188 | 'Content-Type': 'text/css'
189 | }
190 | })
191 | .then(resp => resp.text())
192 | .then(resp => {
193 | console.log('load external css contents: ');
194 | return ``
195 | })
196 | .catch(err => {
197 | console.error('fetchPolyfill css failed: ', err);
198 | return '';
199 | })
200 | });
201 | const loadedStyles = await Promise.all(promiseList).then(list => list.join(''));
202 |
203 | const htmlPre = `
204 |
205 |
206 |
207 |
208 |
209 | ChatGPT Service
210 | ${loadedStyles}
211 |
212 |
213 | You need to enable JavaScript to run this app.
214 | ${div.innerHTML}
215 |
216 |
217 |
218 | `
219 | return htmlPre;
220 | }
221 |
222 |
223 | export default dynamic(() => Promise.resolve(Messages), {
224 | ssr: false
225 | })
226 | // export default Messages;
--------------------------------------------------------------------------------
/components/Messages.module.css:
--------------------------------------------------------------------------------
1 | .codebox-handler {
2 | --tw-text-opacity: 1;
3 | color: rgba(217, 217, 227, var(--tw-text-opacity));
4 | --tw-bg-opacity: 0.6;
5 | background-color: rgba(52, 53, 65, var(--tw-bg-opacity));
6 | display: flex;
7 | justify-content: space-between;
8 | align-items: center;
9 | padding: 0 10px;
10 | border-top-right-radius:5px;
11 | border-top-left-radius:5px;
12 | line-height: 32px;
13 | }
14 |
15 | .codebox-handler span {
16 | display: flex;
17 | }
18 |
19 | .placeholder-content {
20 | text-align: center;
21 | text-align: center;
22 | position: absolute;
23 | margin: 0 auto;
24 | top: 50%;
25 | left: 0;
26 | transform: translateY(-50%);
27 | line-height: 2rem;
28 | font-size: large;
29 | font-weight: bolder;
30 | color: #000;
31 | }
32 |
33 | .placeholder-content > .description {
34 | font-size: small;
35 | color: #0000009c;
36 | }
37 |
38 | .incoming.bubble {
39 | background: #b2b2b2;
40 | }
41 |
42 | .talking-item {
43 | position: relative;
44 | display: inline-block;
45 | width: fit-content;
46 | box-sizing: border-box;
47 | max-width: calc(100% - 30px);
48 | margin: 5px;
49 | font-size: 0.7em;
50 | padding: 10px 10px 10px 12px;
51 | border-radius: 20px;
52 | display: flex;
53 | flex-direction: row;
54 | align-items: center;
55 | color: #19c37d;
56 | box-shadow: 0 0 3px 2px #9e9e9e75;
57 | }
58 | .talking-avatar {
59 | width: 24px;
60 | height: 24px;
61 | border-radius: 50%;
62 | margin-left: 0;
63 | margin-right: 5px;
64 | }
65 |
66 | .talking-avatar img,
67 | .talking-avatar svg {
68 | height: 100%;
69 | width: auto;
70 | }
71 |
72 | .talking-item-btns {
73 | padding: 4px 4px 0;
74 | display: flex;
75 | flex-direction: row;
76 | }
77 | .talking-item-btns > svg, .talking-item-btn {
78 | width: 16px;
79 | height: 16px;
80 | display: block;
81 | margin: 0px 8px;
82 | color: rgb(18, 150, 219);
83 | }
84 | .delete-btn {
85 | width: 16px;
86 | height: 16px;
87 | display: block;
88 | margin: 6px 8px;
89 | }
90 |
91 | .bubble {
92 | position: relative;
93 | display: inline-block;
94 | width: fit-content;
95 | margin: 5px;
96 | color: #F9FBFF;
97 | border-radius: 2px;
98 | text-align: left;
99 | font-size: 14px;
100 | overflow-x: scroll;
101 | }
102 | .bubble::-webkit-scrollbar {
103 | display: none;
104 | -ms-overflow-style: none; /* IE and Edge */
105 | }
106 | .bubble p {
107 | word-break: break-all;
108 | }
109 |
110 |
111 | .lower {
112 | margin-top: 45px;
113 | }
114 |
115 | /* .outgoing {
116 | position: absolute;
117 | padding: 20px;
118 | right: 0;
119 | top: 15%;
120 | width: 50%;
121 | height: 100%;
122 | } */
123 |
124 | .outgoing.talking-item {
125 | background: #79C7C5;
126 | float: right;
127 | text-align: right;
128 | align-self: flex-end;
129 | flex-direction: row-reverse;
130 | }
131 | .outgoing .talking-avatar {
132 | margin-right: 0;
133 | margin-left: 5px;
134 | }
135 |
136 | .incoming.talking-item {
137 | background: #b2b2b2;
138 | }
139 |
--------------------------------------------------------------------------------
/components/PromptElement.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { Popup, Radio, Space, Button, Collapse, SafeArea } from 'antd-mobile'
3 | import { EditSOutline } from 'antd-mobile-icons'
4 | import { DotLoading } from 'antd-mobile'
5 | import { CheckShieldOutline, CheckShieldFill } from 'antd-mobile-icons'
6 | // import { useNavigate } from 'react-router-dom'
7 | import { useLocalStorage } from '../utils/Others';
8 | import './PromptElement.module.css'
9 |
10 | const DefaultPromptList = [
11 | {
12 | label: '默认模式',
13 | detail: '不设置任何预选prompt,直接开始提问吧',
14 | value: 0,
15 | prompt: '',
16 | },
17 | {
18 | "label": "中英互译",
19 | "detail": "英汉互译 + 可定制风格 + 可学习英语",
20 | "value": 1,
21 | "prompt": "As an English-Chinese translator, your task is to accurately translate text between the two languages. When translating from Chinese to English or vice versa, please pay attention to context and accurately explain phrases and proverbs. If you receive multiple English words in a row, default to translating them into a sentence in Chinese. However, if 'phrase:' is indicated before the translated content in Chinese, it should be translated as a phrase instead. Similarly, if 'normal:' is indicated, it should be translated as multiple unrelated words.Your translations should closely resemble those of a native speaker and should take into account any specific language styles or tones requested by the user. Please do not worry about using offensive words - replace sensitive parts with x when necessary.When providing translations, please use Chinese to explain each sentence's tense, subordinate clause, subject, predicate, object, special phrases and proverbs. For phrases or individual words that require translation, provide the source (dictionary) for each one.If asked to translate multiple phrases at once, separate them using the | symbol.Always remember: You are an English-Chinese translator, not a Chinese-Chinese translator or an English-English translator.Please review and revise your answers carefully before submitting."
22 | },
23 | {
24 | "label": "写作助理",
25 | "detail": "个人最常使用的 prompt,可用于改进文字段落和句式",
26 | "value": 2,
27 | "prompt": "As a writing improvement assistant, your task is to improve the spelling, grammar, clarity, concision, and overall readability of the text provided, while breaking down long sentences, reducing repetition, and providing suggestions for improvement. Please provide only the corrected Chinese version of the text and avoid including explanations. Please begin by editing the following text:"
28 | },
29 | {
30 | "label": "周报生成",
31 | "detail": "根据日常工作内容,提取要点并适当扩充,以生成周报",
32 | "value": 3,
33 | "prompt": "Using the provided text below as the basis for a weekly report in Chinese, generate a concise summary that highlights the most important points. The report should be written in markdown format and should be easily readable and understandable for a general audience. In particular, focus on providing insights and analysis that would be useful to stakeholders and decision-makers. You may also use any additional information or sources as necessary. Please begin by editing the following text:"
34 | }
35 | ]
36 |
37 | export const getPrompts = async () => {
38 | return Promise.resolve(DefaultPromptList)
39 | }
40 |
41 | function PromotSelect(props) {
42 | // const navigate = useNavigate()
43 | const [loading, setLoading] = useState(false)
44 | const [visible, setVisible] = useState(props.visible)
45 | const [value, setValue] = useState(props.value || 0)
46 | const [promptList, setPromptList] = useLocalStorage('promps-list-lcal', []);
47 | const [cachedTime, setCachedTime] = useLocalStorage('promps-list-lcal-time', 0);
48 | const [customPromptList] = useLocalStorage('custom-promps-list-lcal', []);
49 |
50 | const closeView = () => {
51 | console.log('closing view');
52 | setVisible(false)
53 | props.onClose();
54 | }
55 | useEffect(() => {
56 | if ((promptList?.length > 0) && (new Date().valueOf() < cachedTime + 1000 * 60 * 10)) {
57 | // skip loading
58 | console.log('skip loading...')
59 | return;
60 | }
61 | setLoading(true)
62 | getPrompts().then(list => {
63 | setPromptList([...list])
64 | setCachedTime(new Date().valueOf())
65 | setLoading(false)
66 | }).catch(err => {
67 | setLoading(false);
68 | });
69 | }, [])
70 |
71 | const onAddPrompt = (e) => {
72 | e.preventDefault()
73 | // navigate('/build/editPrompt')
74 | }
75 |
76 | return (
77 | <>
78 |
86 |
87 | {
88 | loading ?
98 |
99 | 加载中
100 |
:
101 |
102 |
103 |
104 | {
108 | setValue(val)
109 | console.log('on value chagned: ', val);
110 | if (props.onConfirm) {
111 | const data = promptList.find(p => p.value === val);
112 | props.onConfirm(data);
113 | }
114 | closeView()
115 | }}>
116 |
117 |
118 |
119 | {promptList.map((p) => (
123 | checked ? (
124 |
125 | ) : (
126 |
127 | )
128 | }
129 | style={{
130 | '--icon-size': '22px',
131 | '--font-size': '16px',
132 | '--gap': '8px',
133 | }}>
134 | {p.label}
135 | ))}
136 |
137 |
138 |
139 |
140 | {customPromptList.length > 0 &&
141 | {
145 | setValue(val)
146 | console.log('on value chagned: ', val);
147 | if (props.onConfirm) {
148 | const data = customPromptList.find(p => p.value === val);
149 | console.log('data:', data)
150 | props.onConfirm(data);
151 | }
152 | closeView()
153 | }}>
154 |
155 |
156 | {customPromptList.map((p) => (
160 | checked ? (
161 |
162 | ) : (
163 |
164 | )
165 | }
166 | style={{
167 | '--icon-size': '22px',
168 | '--font-size': '16px',
169 | '--gap': '8px',
170 | }}>
171 | {p.label}
172 |
173 | {
174 | e.preventDefault()
175 | e.stopPropagation()
176 | // navigate(`/build/editPrompt/${p.value}`)
177 | }} />
178 |
179 | ))}
180 |
181 |
182 |
183 | }
184 |
185 |
186 | }
187 |
188 |
189 |
190 |
195 | onAddPrompt(e)}>
196 | 新增场景
197 |
198 |
199 |
200 |
201 |
202 |
203 | >
204 |
205 | )
206 | }
207 |
208 | export default PromotSelect;
--------------------------------------------------------------------------------
/components/PromptElement.module.css:
--------------------------------------------------------------------------------
1 | .prompt-list-container {
2 | padding: 40px 16px;
3 | }
4 |
5 | .prompt-block {
6 | position: relative;
7 | margin-bottom: 12px;
8 | }
9 |
10 | .prompt-block .title {
11 | padding: 12px 0 8px;
12 | color: #697b8c;
13 | font-size: 16px;
14 | }
15 |
16 | .prompt-block .split-line {
17 | height: 0;
18 | border-top: 1px solid #80808047;
19 | margin: 2px 0 4px;
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/components/Shapes.js:
--------------------------------------------------------------------------------
1 | export const CopyBtn = ({ ...props}) => {
2 | return
3 | }
4 |
5 | export const DeleteBtn = ({ ...props}) => {
6 | return
7 | }
--------------------------------------------------------------------------------
/components/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/notice.js:
--------------------------------------------------------------------------------
1 | import { fetch } from 'whatwg-fetch'
2 | import { HOST_URL } from './config'
3 |
4 | export const getNotice = async () => {
5 | return fetch(`${HOST_URL}/notice.json`)
6 | .then(resp => resp.json())
7 | .then(resp => {
8 | console.log('resp: ', resp);
9 | return resp?.contents;
10 | })
11 | .catch(err => {
12 | console.error('getting notice failed: ', err);
13 | return '因多条线路被封,可能消息响应速度较慢,请耐心等待'
14 | })
15 | }
16 |
--------------------------------------------------------------------------------
/components/share.js:
--------------------------------------------------------------------------------
1 |
2 | const ShareLogo = ({ ...props }) => {
3 | return
4 | }
5 |
6 | export default ShareLogo;
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | // const SSRPlugin = require("next/dist/build/webpack/plugins/nextjs-ssr-import")
3 | // .default;
4 | // const { dirname, relative, resolve, join } = require("path");
5 |
6 | const conf = {
7 | reactStrictMode: false,
8 | swcMinify: true,
9 | async redirects() {
10 | return [
11 | {
12 | source: "/github",
13 | destination: "https://github.com/Nutlope/twitterbio",
14 | permanent: false,
15 | },
16 | {
17 | source: "/deploy",
18 | destination: "https://vercel.com/templates/next.js/twitter-bio",
19 | permanent: false,
20 | },
21 | ];
22 | },
23 | images: {
24 | unoptimized: true,
25 | },
26 | experimental: {
27 | // layers: true,
28 | },
29 | transpilePackages: ['antd-mobile'],
30 | webpack(config, { isServer, dev }) {
31 | // Enable webassembly
32 | config.experiments = { ...config.experiments, asyncWebAssembly: true, layers: true };
33 | if (isServer) {
34 | config.output.webassemblyModuleFilename =
35 | '../static/wasm/[modulehash].wasm';
36 | } else {
37 | config.output.webassemblyModuleFilename =
38 | 'static/wasm/[modulehash].wasm';
39 | }
40 |
41 | // Unfortunately there isn't an easy way to override the replacement function body, so we
42 | // have to just replace the whole plugin `apply` body.
43 | function patchSsrPlugin(plugin) {
44 | plugin.apply = function apply(compiler) {
45 | compiler.hooks.compilation.tap("NextJsSSRImport", compilation => {
46 | compilation.mainTemplate.hooks.requireEnsure.tap(
47 | "NextJsSSRImport",
48 | (code, chunk) => {
49 | // This is the block that fixes https://github.com/vercel/next.js/issues/22581
50 | if (!chunk.name) {
51 | return;
52 | }
53 |
54 | // Update to load chunks from our custom chunks directory
55 | const outputPath = resolve("/");
56 | const pagePath = join("/", dirname(chunk.name));
57 | const relativePathToBaseDir = relative(pagePath, outputPath);
58 | // Make sure even in windows, the path looks like in unix
59 | // Node.js require system will convert it accordingly
60 | const relativePathToBaseDirNormalized = relativePathToBaseDir.replace(
61 | /\\/g,
62 | "/"
63 | );
64 | return code
65 | .replace(
66 | 'require("./"',
67 | `require("${relativePathToBaseDirNormalized}/"`
68 | )
69 | .replace(
70 | "readFile(join(__dirname",
71 | `readFile(join(__dirname, "${relativePathToBaseDirNormalized}"`
72 | );
73 | }
74 | );
75 | });
76 | };
77 | }
78 |
79 | // In prod mode and in the server bundle (the place where this "chunks" bug
80 | // appears), use the client static directory for the same .wasm bundle
81 | // config.output.webassemblyModuleFilename =
82 | // isServer && !dev ? "../static/wasm/[id].wasm" : "static/wasm/[id].wasm";
83 |
84 | // Ensure the filename for the .wasm bundle is the same on both the client
85 | // and the server (as in any other mode the ID's won't match)
86 | // config.optimization.moduleIds = "named";
87 | config.resolve = {
88 | ...config.resolve,
89 | fallback: {
90 | async_hooks: false,
91 | },
92 | }
93 |
94 | if (!isServer) {
95 | config.resolve = {
96 | ...config.resolve,
97 | fallback: {
98 | ...config.resolve.fallback,
99 | // fixes proxy-agent dependencies
100 | net: false,
101 | stream: false,
102 | dns: false,
103 | tls: false,
104 | assert: false,
105 | // fixes next-i18next dependencies
106 | path: false,
107 | fs: false,
108 | // fixes mapbox dependencies
109 | events: false,
110 | // fixes sentry dependencies
111 | process: false
112 | }
113 | }
114 | }
115 |
116 | return config;
117 | },
118 | };
119 |
120 |
121 | module.exports = conf;
122 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@frontend-engineering/chatgpt-webapp-fullstack",
3 | "version": "2.0.0",
4 | "description": "A full-stack web application that incorporates ChatGPT technology. It features both a webpage and a Node.js server, with the added convenience of one-click deployment on Vercel.",
5 | "private": true,
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start"
10 | },
11 | "homepage": "https://chatgpt-webapp-fullstack-b8td-8dzy0yt7z-webinfra.vercel.app/",
12 | "dependencies": {
13 | "@cashier/web": "^0.3.7",
14 | "@dqbd/tiktoken": "^1.0.7",
15 | "@fortaine/fetch-event-source": "^3.0.6",
16 | "@headlessui/react": "^1.7.7",
17 | "@headlessui/tailwindcss": "^0.1.2",
18 | "@heroicons/react": "^2.0.13",
19 | "@keyv/redis": "^2.5.7",
20 | "@tailwindcss/forms": "^0.5.3",
21 | "@vercel/analytics": "^0.1.8",
22 | "@vercel/edge-config": "^0.1.9",
23 | "@vercel/edge-primitives": "^0.12.4",
24 | "@vercel/kv": "^0.1.1",
25 | "antd-mobile": "^5.29.1",
26 | "antd-mobile-icons": "^0.3.0",
27 | "classnames": "^2.3.2",
28 | "eventsource-parser": "^0.0.5",
29 | "keyv": "^4.5.2",
30 | "nanoid": "^4.0.2",
31 | "next": "latest",
32 | "react": "18.2.0",
33 | "react-dom": "18.2.0",
34 | "react-hook-form": "^7.42.0",
35 | "react-hot-toast": "^2.4.0",
36 | "react-markdown": "^8.0.7",
37 | "react-syntax-highlighter": "^15.5.0",
38 | "react-use-measure": "^2.1.1",
39 | "sass": "^1.62.1",
40 | "whatwg-fetch": "^3.6.2"
41 | },
42 | "devDependencies": {
43 | "@types/node": "18.11.3",
44 | "@types/react": "18.0.21",
45 | "@types/react-dom": "18.0.6",
46 | "autoprefixer": "^10.4.12",
47 | "postcss": "^8.4.18",
48 | "tailwindcss": "^3.2.4",
49 | "typescript": "4.9.4"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { Analytics } from "@vercel/analytics/react";
2 | import type { AppProps } from "next/app";
3 | import Script from 'next/script'
4 | import { useRouter } from 'next/router'
5 | import "../styles/globals.css";
6 |
7 |
8 | function MyApp({ Component, pageProps }: AppProps) {
9 | const router = useRouter()
10 | return (
11 | <>
12 |
13 |