├── .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 |
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 |
32 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Button/index.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | import { ButtonProps } from "antd";
3 | import { useContext, useRef } from "react";
4 | import { UserStore } from "@/store/User";
5 |
6 | export default dynamic(
7 | () =>
8 | import("antd").then((mod) => {
9 | const { Button } = mod;
10 | const HookButton = ({ onClick, ...props }: ButtonProps) => {
11 | const buttonRef = useRef(null);
12 | const { userInfo } = useContext(UserStore);
13 |
14 | return (
15 |