├── .eslintrc.json ├── .prettierrc ├── public ├── author.jpg ├── favicon.ico ├── logo.svg ├── logo_with_bg.svg └── particles.json ├── postcss.config.js ├── src ├── styles │ ├── globals.css │ ├── text.scss │ ├── highlight.scss │ └── github-markdown.scss ├── pages │ ├── 404.tsx │ ├── _error.tsx │ ├── api │ │ ├── hello.ts │ │ ├── notice.ts │ │ ├── logout.ts │ │ ├── chat-progress.ts │ │ └── [...all].ts │ ├── _document.tsx │ ├── index.tsx │ ├── _app.tsx │ ├── chat │ │ └── [id].tsx │ └── login │ │ └── index.tsx ├── components │ ├── ClientOnly │ │ └── index.tsx │ ├── Avatar │ │ └── index.tsx │ ├── UserAvatar │ │ └── index.tsx │ ├── Setting │ │ └── index.tsx │ ├── Button │ │ └── index.tsx │ ├── Sidebar │ │ ├── Footer.tsx │ │ ├── History.tsx │ │ └── index.tsx │ ├── Scrollbar │ │ └── index.tsx │ ├── Image │ │ └── index.tsx │ ├── Text │ │ └── index.tsx │ ├── Header │ │ └── index.tsx │ ├── BasicInfo │ │ └── index.tsx │ ├── Message │ │ └── index.tsx │ ├── ChatContent │ │ └── index.tsx │ ├── Footer │ │ └── index.tsx │ └── Billing │ │ └── index.tsx ├── hooks │ ├── useScroll.ts │ ├── useIsMobile.ts │ ├── useCountDown.ts │ ├── useTheme.ts │ └── useChatProgress.ts ├── utils │ ├── requestAuth.ts │ ├── downloadAsImage.ts │ └── copyToClipboard.ts ├── constants.ts ├── middleware.ts ├── service │ ├── logger.ts │ ├── localStorage.ts │ ├── http.ts │ ├── server.ts │ └── chatgpt.ts ├── store │ ├── User.tsx │ ├── App.tsx │ └── Chat.tsx └── assets │ └── images │ └── logo.svg ├── next.config.js ├── tailwind.config.js ├── .gitignore ├── globals.d.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── README-zh_CN.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /public/author.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helianthuswhite/chatgpt-web-next/HEAD/public/author.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helianthuswhite/chatgpt-web-next/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import "antd/dist/reset.css"; 6 | 7 | html, 8 | body, 9 | #__next { 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from "antd"; 2 | 3 | export default function Page404() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import { Result } from "antd"; 2 | 3 | export default function PageError() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | webpack: (config) => { 5 | config.module.rules.push({ 6 | test: /\.svg$/, 7 | use: ["@svgr/webpack"], 8 | }); 9 | return config; 10 | }, 11 | }; 12 | 13 | module.exports = nextConfig; 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: "class", 4 | content: [ 5 | // Or if using `src` directory: 6 | "./src/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | corePlugins: { 12 | preflight: false, 13 | }, 14 | plugins: [], 15 | }; 16 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ClientOnly/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const ClientOnly = ({ children }: { children: React.ReactElement }) => { 4 | const [mounted, setMounted] = useState(false); 5 | 6 | useEffect(() => { 7 | setMounted(true); 8 | }, []); 9 | 10 | if (!mounted) return null; 11 | 12 | return children; 13 | }; 14 | 15 | export default ClientOnly; 16 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 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 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/api/notice.ts: -------------------------------------------------------------------------------- 1 | import logger from "@/service/logger"; 2 | import { sendResponse } from "@/service/server"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | logger.info("notice", req.url, req.body); 7 | 8 | const data = await sendResponse({ status: "success", data: process.env.NOTICE }); 9 | res.write(JSON.stringify(data)); 10 | res.end(); 11 | 12 | logger.info("notice", "send to client", process.env.NOTICE); 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 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .vscode 39 | 40 | *tencent*.txt 41 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import storage from "@/service/localStorage"; 2 | import { Chat, DEFAULT_UUID, LOCAL_NAME } from "@/store/Chat"; 3 | import { useRouter } from "next/router"; 4 | import { useEffect } from "react"; 5 | 6 | const chatStorage = storage(); 7 | 8 | const Home = () => { 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | const storageValue = chatStorage.get(LOCAL_NAME); 13 | const uuid = storageValue?.history?.[0].uuid || DEFAULT_UUID; 14 | router.push(`/chat/${uuid}`); 15 | }, [router]); 16 | }; 17 | 18 | export default Home; 19 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | TIMEOUT_MS: string; 5 | OPENAI_API_KEY: string; 6 | OPENAI_ACCESS_TOKEN: string; 7 | OPENAI_API_BASE_URL: string; 8 | OPENAI_API_MODEL: string; 9 | SOCKS_PROXY_HOST: string; 10 | SOCKS_PROXY_PORT: string; 11 | API_REVERSE_PROXY: string; 12 | LOCAL_ACCESS_TOKENS: string; 13 | BACKEND_ENDPOINT: string; 14 | } 15 | } 16 | 17 | interface Window { 18 | umami: any; 19 | } 20 | } 21 | 22 | export {}; 23 | -------------------------------------------------------------------------------- /src/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import Scrollbars from "react-custom-scrollbars-2"; 3 | 4 | const useScroll = () => { 5 | const scrollRef = useRef(null); 6 | 7 | const scrollToBottom = async () => { 8 | if (scrollRef.current) { 9 | scrollRef.current.scrollToBottom(); 10 | } 11 | }; 12 | 13 | const scrollToTop = async () => { 14 | if (scrollRef.current) { 15 | scrollRef.current.scrollToTop(); 16 | } 17 | }; 18 | 19 | return { 20 | scrollRef, 21 | scrollToBottom, 22 | scrollToTop, 23 | }; 24 | }; 25 | 26 | export default useScroll; 27 | -------------------------------------------------------------------------------- /src/pages/api/logout.ts: -------------------------------------------------------------------------------- 1 | import { USER_TOKEN } from "@/constants"; 2 | import logger from "@/service/logger"; 3 | import { sendResponse } from "@/service/server"; 4 | import type { NextApiRequest, NextApiResponse } from "next"; 5 | 6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 7 | res.setHeader("Set-Cookie", `${USER_TOKEN}=; path=/; Max-Age=0; HttpOnly`); 8 | res.setHeader("Content-Type", "application/json"); 9 | 10 | logger.info("logout", req.url, req.body); 11 | 12 | const data = await sendResponse({ status: "success", message: "登出成功" }); 13 | res.write(JSON.stringify(data)); 14 | res.end(); 15 | 16 | logger.info("logout", "logout success"); 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "globals.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/requestAuth.ts: -------------------------------------------------------------------------------- 1 | import { sendResponse } from "@/service/server"; 2 | import { NextApiRequest } from "next"; 3 | 4 | const requestAuth = (req: NextApiRequest) => { 5 | const tokens = process.env.LOCAL_ACCESS_TOKENS?.split(","); 6 | const token = req.headers.authorization; 7 | 8 | if (!process.env.LOCAL_ACCESS_TOKENS) { 9 | return Promise.resolve(); 10 | } 11 | 12 | if (!token || !tokens?.length) { 13 | return sendResponse({ 14 | status: "fail", 15 | message: "No authorization token provided", 16 | code: 401, 17 | }); 18 | } 19 | 20 | if (!tokens.includes(token)) { 21 | return sendResponse({ status: "fail", message: "Invalid authorization token", code: 401 }); 22 | } 23 | }; 24 | 25 | export default requestAuth; 26 | -------------------------------------------------------------------------------- /src/components/Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar as AvatarComp, AvatarProps } from "antd"; 2 | import { useContext } from "react"; 3 | import Image from "next/image"; 4 | import { UserStore } from "@/store/User"; 5 | import LogoSvg from "@/assets/images/logo.svg"; 6 | 7 | interface Props extends AvatarProps { 8 | isUser?: boolean; 9 | } 10 | 11 | const Avatar: React.FC = ({ isUser, ...props }) => { 12 | const { userInfo } = useContext(UserStore); 13 | 14 | if (isUser) { 15 | return ( 16 | 17 | {userInfo.name} 18 | 19 | ); 20 | } 21 | 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default Avatar; 30 | -------------------------------------------------------------------------------- /src/hooks/useIsMobile.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import debounce from "lodash/debounce"; 3 | 4 | const useIsMobile = (): boolean => { 5 | const [isMobile, setIsMobile] = useState(false); 6 | 7 | useEffect(() => { 8 | if ( 9 | /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 10 | navigator.userAgent 11 | ) 12 | ) { 13 | setIsMobile(true); 14 | return; 15 | } 16 | 17 | const updateSize = (): void => { 18 | setIsMobile(window.innerWidth < 768); 19 | }; 20 | window.addEventListener("resize", debounce(updateSize, 250)); 21 | return (): void => window.removeEventListener("resize", updateSize); 22 | }, []); 23 | 24 | return isMobile; 25 | }; 26 | 27 | export default useIsMobile; 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_TOKEN = "authorization"; 2 | 3 | export const TOKEN_MAX_AGE = 60 * 60 * 24 * 7; 4 | 5 | export const StaticFileExtensions = [ 6 | ".aac", 7 | ".avi", 8 | ".bmp", 9 | ".css", 10 | ".doc", 11 | ".docx", 12 | ".eot", 13 | ".flac", 14 | ".flv", 15 | ".gif", 16 | ".ico", 17 | ".jpeg", 18 | ".jpg", 19 | ".js", 20 | ".json", 21 | ".less", 22 | ".md", 23 | ".m4a", 24 | ".mov", 25 | ".mp3", 26 | ".mp4", 27 | ".ogg", 28 | ".otf", 29 | ".pdf", 30 | ".png", 31 | ".ppt", 32 | ".pptx", 33 | ".rar", 34 | ".sass", 35 | ".scss", 36 | ".svg", 37 | ".tar.gz", 38 | ".ttf", 39 | ".wav", 40 | ".webm", 41 | ".wmv", 42 | ".woff", 43 | ".woff2", 44 | ".xls", 45 | ".xlsx", 46 | ".xml", 47 | ".zip", 48 | ".txt", 49 | ]; 50 | -------------------------------------------------------------------------------- /src/hooks/useCountDown.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by ChatGPT 3 | */ 4 | 5 | import { useState, useEffect, useRef } from "react"; 6 | 7 | type CountDown = [number, () => void, () => void]; 8 | 9 | const useCountDown: (initialCount?: number) => CountDown = (initialCount = 60) => { 10 | const [count, setCount] = useState(0); 11 | const intervalRef = useRef(); 12 | 13 | useEffect(() => { 14 | if (count <= 0) return; 15 | intervalRef.current = setInterval(() => { 16 | setCount(count - 1); 17 | }, 1000); 18 | return () => clearInterval(intervalRef.current); 19 | }, [count]); 20 | 21 | const startCount = () => setCount(initialCount); 22 | const stopCount = () => { 23 | setCount(0); 24 | clearInterval(intervalRef.current); 25 | }; 26 | 27 | return [count, startCount, stopCount]; 28 | }; 29 | 30 | export default useCountDown; 31 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { AppStore } from "@/store/App"; 2 | import { useContext, useEffect, useState } from "react"; 3 | 4 | const useTheme = () => { 5 | const { theme } = useContext(AppStore); 6 | const [type, setType] = useState<"light" | "dark">("light"); 7 | 8 | useEffect(() => { 9 | if (theme === "auto") { 10 | const type = window.matchMedia("(prefers-color-scheme: dark)").matches 11 | ? "dark" 12 | : "light"; 13 | setType(type); 14 | return; 15 | } 16 | 17 | setType(theme as "light" | "dark"); 18 | }, [theme]); 19 | 20 | useEffect(() => { 21 | if (type === "dark") { 22 | document.documentElement.classList.add("dark"); 23 | } else { 24 | document.documentElement.classList.remove("dark"); 25 | } 26 | }, [type]); 27 | 28 | return type; 29 | }; 30 | 31 | export default useTheme; 32 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { StaticFileExtensions, USER_TOKEN } from "@/constants"; 3 | 4 | const checkPath = (pathname: string) => { 5 | const dotIndex = pathname.lastIndexOf("."); 6 | const suffix = pathname.substring(dotIndex + 1); 7 | 8 | return !( 9 | pathname.startsWith("/_next/") || 10 | StaticFileExtensions.includes(`.${suffix}`) || 11 | pathname.includes("/login") || 12 | pathname.includes("/register") || 13 | pathname.includes("/send_code") 14 | ); 15 | }; 16 | 17 | export function middleware(req: NextRequest) { 18 | const token = req.cookies.get(USER_TOKEN)?.value; 19 | const { pathname } = req.nextUrl; 20 | const isAuthPath = checkPath(pathname); 21 | 22 | if (pathname === "/login" && token) { 23 | return NextResponse.redirect(new URL("/", req.url)); 24 | } 25 | 26 | if (isAuthPath && !token) { 27 | return NextResponse.redirect(new URL("/login", req.url)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/downloadAsImage.ts: -------------------------------------------------------------------------------- 1 | import { message } from "antd"; 2 | import domToImage from "dom-to-image"; 3 | 4 | const downloadAsImage = async (dom: HTMLElement, title: string) => { 5 | message.loading("处理中...", 1); 6 | try { 7 | const scale = window.devicePixelRatio * 2 || 2; 8 | const { scrollWidth: width, scrollHeight: height } = dom; 9 | const data = await domToImage.toPng(dom, { 10 | width: width * scale, 11 | height: height * scale, 12 | style: { 13 | transform: `scale(${scale})`, 14 | transformOrigin: "top left", 15 | width: `${width}px`, 16 | height: `${height}px`, 17 | }, 18 | }); 19 | const link = document.createElement("a"); 20 | link.download = `${title}.png`; 21 | link.href = data; 22 | link.click(); 23 | } catch (error: any) { 24 | message.error(`下载失败: ${error?.message}`); 25 | } 26 | }; 27 | 28 | export default downloadAsImage; 29 | -------------------------------------------------------------------------------- /src/components/UserAvatar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { UserStore } from "@/store/User"; 3 | import Avatar from "@/components/Avatar"; 4 | 5 | interface Props {} 6 | 7 | const UserAvatar: React.FC = () => { 8 | const { userInfo } = useContext(UserStore); 9 | 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 |

17 | {userInfo.name} 18 |

19 |

20 | 21 |

22 |
23 |
24 | ); 25 | }; 26 | 27 | export default UserAvatar; 28 | -------------------------------------------------------------------------------- /src/service/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from "winston"; 2 | 3 | const { combine, colorize, printf, timestamp } = winston.format; 4 | 5 | const logger = winston.createLogger({ 6 | level: "info", 7 | format: combine( 8 | colorize(), 9 | timestamp(), 10 | printf(({ level, message, label, timestamp }) => { 11 | return `${timestamp} [${label}] ${level}: ${message}`; 12 | }) 13 | ), 14 | transports: [new winston.transports.Console()], 15 | }); 16 | 17 | const levels = Object.keys(winston.config.syslog.levels); 18 | 19 | levels.forEach((level) => { 20 | // @ts-ignore 21 | logger[level] = (...props) => { 22 | let label = "unknown"; 23 | 24 | if (props.length > 1) { 25 | label = props[0]; 26 | props = props.slice(1); 27 | } 28 | 29 | const messages = [...props].map((prop) => 30 | typeof prop === "object" ? JSON.stringify(prop) : prop 31 | ); 32 | logger.log({ level, label, message: messages.join(" ") }); 33 | }; 34 | }); 35 | 36 | export default logger; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Helianthuswhite 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 | -------------------------------------------------------------------------------- /src/utils/copyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import copy from "copy-to-clipboard"; 2 | 3 | const copyToClipboard = (textToCopy: string) => { 4 | const result = copy(textToCopy); 5 | if (result) { 6 | return Promise.resolve(); 7 | } 8 | return Promise.reject(); 9 | 10 | // // navigator clipboard needs a secure context (HTTPS) 11 | // if (navigator.clipboard && window.isSecureContext) { 12 | // // navigator clipboard api method' 13 | // return navigator.clipboard.writeText(textToCopy); 14 | // } else { 15 | // let textArea = document.createElement("textarea"); 16 | // textArea.value = textToCopy; 17 | // // set textArea invisible 18 | // textArea.style.position = "absolute"; 19 | // textArea.style.opacity = "0"; 20 | // textArea.style.left = "-999999px"; 21 | // textArea.style.top = "-999999px"; 22 | // document.body.appendChild(textArea); 23 | // textArea.focus(); 24 | // textArea.select(); 25 | // return new Promise((res, rej) => { 26 | // // exec copy command and remove text area 27 | // document.execCommand("copy") ? res() : rej(); 28 | // textArea.remove(); 29 | // }); 30 | // } 31 | }; 32 | 33 | export default copyToClipboard; 34 | -------------------------------------------------------------------------------- /src/pages/api/chat-progress.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { chatReplyImage, chatReplyProcess } from "@/service/chatgpt"; 3 | import logger from "@/service/logger"; 4 | import { ConversationRequest } from "@/store/Chat"; 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | 7 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 8 | logger.info("chat-progress", req.url, req.body); 9 | 10 | try { 11 | const { options = {} } = req.body as { 12 | options?: ConversationRequest; 13 | }; 14 | 15 | if (options.isImage) { 16 | res.setHeader("Content-type", "application/json"); 17 | await chatReplyImage(req, res); 18 | } else { 19 | res.setHeader("Content-type", "application/octet-stream"); 20 | await chatReplyProcess(req, res); 21 | } 22 | } catch (error: any) { 23 | logger.error("chat-progress", "chat-progress error:", error); 24 | 25 | const response = { status: "fail", message: error.message, code: error.code || 500 }; 26 | res.setHeader("Content-type", "application/json"); 27 | res.write(JSON.stringify(response)); 28 | } finally { 29 | res.end(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/service/localStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 由ChatGPT实现的localStorage的封装 3 | */ 4 | 5 | interface LocalStorage { 6 | set: (key: string, value: T) => void; 7 | get: (key: string) => T | null; 8 | remove: (key: string) => void; 9 | } 10 | 11 | const storage = (): LocalStorage => ({ 12 | set: (key: string, value: T) => { 13 | try { 14 | if (typeof window === "undefined") return; 15 | localStorage.setItem(key, JSON.stringify(value)); 16 | } catch (error) { 17 | console.error("Error saving to localStorage:", error); 18 | } 19 | }, 20 | get: (key: string) => { 21 | try { 22 | if (typeof window === "undefined") return null; 23 | const item = localStorage.getItem(key); 24 | return item ? JSON.parse(item) : null; 25 | } catch (error) { 26 | console.error("Error getting from localStorage:", error); 27 | return null; 28 | } 29 | }, 30 | remove: (key: string) => { 31 | try { 32 | if (typeof window === "undefined") return; 33 | localStorage.removeItem(key); 34 | } catch (error) { 35 | console.error("Error removing from localStorage:", error); 36 | } 37 | }, 38 | }); 39 | 40 | export default storage; 41 | -------------------------------------------------------------------------------- /src/components/Setting/index.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, Tabs } from "antd"; 2 | import BasicInfo from "@/components/BasicInfo"; 3 | import useIsMobile from "@/hooks/useIsMobile"; 4 | import Billing from "@/components/Billing"; 5 | 6 | interface Props { 7 | open: boolean; 8 | notice?: string; 9 | onCancel: () => void; 10 | } 11 | 12 | const Setting: React.FC = ({ open, notice, onCancel }) => { 13 | const isMobile = useIsMobile(); 14 | 15 | return ( 16 | 27 | {/* 28 | 29 | 30 | 31 | 32 | 33 | 34 | */} 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default Setting; 41 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import "@/styles/highlight.scss"; 3 | import "@/styles/github-markdown.scss"; 4 | import "katex/dist/katex.min.css"; 5 | import "@/styles/text.scss"; 6 | import type { AppProps } from "next/app"; 7 | import AppStoreProvider from "@/store/App"; 8 | import UserStoreProvider from "@/store/User"; 9 | import { ConfigProvider } from "antd"; 10 | import Script from "next/script"; 11 | 12 | export default function App({ Component, pageProps }: AppProps) { 13 | return ( 14 | 24 | 25 | 26 | 27 |