├── .dockerignore
├── .eslintrc.json
├── src-tauri
├── build.rs
├── app-icon.png
├── icons
│ ├── 32x32.png
│ ├── icon.icns
│ ├── icon.ico
│ ├── icon.png
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── StoreLogo.png
│ ├── Square30x30Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ └── Square310x310Logo.png
├── .gitignore
├── src
│ └── main.rs
├── Cargo.toml
└── tauri.conf.json
├── service
├── .dockerignore
├── .prettierrc
├── src
│ ├── middleware
│ │ ├── error.middleware.ts
│ │ ├── logger.middleware.ts
│ │ ├── index.ts
│ │ ├── auth.middleware.ts
│ │ └── ratelimit.middleware.ts
│ ├── config
│ │ └── configuration.ts
│ ├── modules
│ │ ├── app
│ │ │ ├── app.service.ts
│ │ │ ├── index.ts
│ │ │ └── app.controller.ts
│ │ └── chatgpt
│ │ │ ├── index.ts
│ │ │ ├── chatgpt.controller.ts
│ │ │ └── chatgpt.service.ts
│ ├── main.ts
│ ├── utils
│ │ └── index.ts
│ └── app.module.ts
├── tsconfig.build.json
├── test
│ ├── jest-e2e.json
│ └── app.e2e-spec.ts
├── vercel.json
├── tsconfig.json
├── webpack.config.js
├── .eslintrc.js
├── README.md
├── .env.example
└── package.json
├── public
├── logo.png
├── logo_w.png
├── app-icon.png
├── chatgpt.png
├── favicon.ico
├── vercel.svg
├── favicon.svg
├── thirteen.svg
├── next.svg
└── logo.svg
├── docs
├── screen_dark.png
└── screen_light.png
├── src
├── assets
│ ├── avatar.jpg
│ ├── chatgpt.png
│ ├── openai.png
│ ├── images
│ │ └── empty_box.png
│ └── icons
│ │ ├── dark.svg
│ │ └── light.svg
├── utils
│ ├── tool
│ │ ├── index.less
│ │ └── index.md
│ ├── index.ts
│ ├── uuid
│ │ └── index.ts
│ ├── request
│ │ ├── axios.ts
│ │ └── index.ts
│ └── storage
│ │ └── index.ts
├── components
│ ├── pages
│ │ ├── setting
│ │ │ ├── Box.tsx
│ │ │ ├── Common.tsx
│ │ │ ├── Network.tsx
│ │ │ └── Surface.tsx
│ │ ├── chat
│ │ │ ├── Setting.tsx
│ │ │ ├── Empty.tsx
│ │ │ ├── Box.tsx
│ │ │ ├── InputArea.tsx
│ │ │ ├── Markdown.tsx
│ │ │ ├── Option.tsx
│ │ │ └── List.tsx
│ │ ├── prompt
│ │ │ ├── Item.tsx
│ │ │ └── Export.tsx
│ │ └── plugin
│ │ │ ├── Info.tsx
│ │ │ └── Item.tsx
│ ├── Icon
│ │ └── index.tsx
│ └── EmojiPicker
│ │ ├── Button.tsx
│ │ └── index.tsx
├── types
│ ├── next-auth.d.ts
│ ├── prompt.d.ts
│ ├── plugin.d.ts
│ └── chat.d.ts
├── themes
│ └── index.tsx
├── contexts
│ ├── index.tsx
│ ├── setting.tsx
│ ├── plugin.tsx
│ ├── site.tsx
│ ├── prompt.tsx
│ └── chat.tsx
├── pages
│ ├── index.tsx
│ ├── share
│ │ └── index.tsx
│ ├── _document.tsx
│ ├── store
│ │ └── index.tsx
│ ├── readme
│ │ └── index.tsx
│ ├── plugin
│ │ └── index.tsx
│ ├── prompt
│ │ └── index.tsx
│ ├── setting
│ │ └── index.tsx
│ ├── _app.tsx
│ └── chat
│ │ └── index.tsx
├── data
│ ├── prompt.ts
│ └── plugin.ts
├── locales
│ ├── index.ts
│ ├── zh_TW.json
│ ├── zh_CN.json
│ └── en_US.json
├── api
│ └── index.ts
├── hooks
│ └── useChat.ts
├── styles
│ └── globals.css
└── config
│ └── constant.ts
├── .husky
├── pre-commit
└── commit-msg
├── commitlint.config.js
├── start.sh
├── docker-start.sh
├── docker-compose
├── readme.md
├── nginx
│ └── nginx.conf
└── docker-compose.yml
├── start.cmd
├── .env.example
├── .gitignore
├── tsconfig.json
├── .github
└── workflows
│ ├── ci.yml
│ └── build_docker.yml
├── LICENSE
├── .release-it.json
├── next.config.js
├── Dockerfile
└── package.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | node_modules
3 | *.log
4 | .DS_Store
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/service/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | node_modules
3 | *.log
4 | .DS_Store
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/logo_w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/public/logo_w.png
--------------------------------------------------------------------------------
/service/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/docs/screen_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/docs/screen_dark.png
--------------------------------------------------------------------------------
/public/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/public/app-icon.png
--------------------------------------------------------------------------------
/public/chatgpt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/public/chatgpt.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/docs/screen_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/docs/screen_light.png
--------------------------------------------------------------------------------
/src-tauri/app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/app-icon.png
--------------------------------------------------------------------------------
/src/assets/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src/assets/avatar.jpg
--------------------------------------------------------------------------------
/src/assets/chatgpt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src/assets/chatgpt.png
--------------------------------------------------------------------------------
/src/assets/openai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src/assets/openai.png
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/service/src/middleware/error.middleware.ts:
--------------------------------------------------------------------------------
1 | export function error(req, res, next) {
2 | next();
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx --no -- commitlint --edit ${1}
--------------------------------------------------------------------------------
/src/assets/images/empty_box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src/assets/images/empty_box.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhpd/chatgpt-plus/HEAD/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/service/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/service/src/config/configuration.ts:
--------------------------------------------------------------------------------
1 | export default () => ({
2 | port: parseInt(process.env.PORT, 10) || 3002,
3 | ...process.env,
4 | });
5 |
--------------------------------------------------------------------------------
/service/src/middleware/logger.middleware.ts:
--------------------------------------------------------------------------------
1 | export function logger(req, res, next) {
2 | console.log(`Request...`, req, res);
3 | next();
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/tool/index.less:
--------------------------------------------------------------------------------
1 | .ant-drawer-footer {
2 | .drawerFooterBtn {
3 | .ant-btn + .ant-btn:not(.ant-dropdown-trigger) {
4 | margin-bottom: 0;
5 | margin-left: 8px;
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/service/src/modules/app/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello ChatGPT!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import tool from './tool'
3 | import request from './request'
4 | import storage from './storage'
5 | import { uuidv4, nanoid } from './uuid'
6 | export { tool, request, uuidv4, nanoid }
7 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 |
2 | cd ./service
3 | nohup npm run dev > service.log &
4 | echo "Start service complete!"
5 |
6 |
7 | cd ..
8 | nohup npm run dev > front.log &
9 | echo "Start front complete!"
10 | tail -f front.log
11 |
--------------------------------------------------------------------------------
/service/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/service/src/middleware/index.ts:
--------------------------------------------------------------------------------
1 | import { logger } from './logger.middleware';
2 | import { error } from './error.middleware';
3 | import { auth } from './auth.middleware';
4 | // import { ratelimit } from './ratelimit.middleware';
5 | export { logger, error, auth };
6 |
--------------------------------------------------------------------------------
/docker-start.sh:
--------------------------------------------------------------------------------
1 |
2 |
3 | cd /app/service
4 | nohup node dist/main.js > /app/log/service.log &
5 | echo "Start service complete!"
6 |
7 |
8 | cd /app/web
9 | nohup node server.js > /app/log/front.log &
10 | echo "Start front complete!"
11 | tail -f /app/log/front.log
12 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 |
4 | fn main() {
5 | tauri::Builder::default()
6 | .run(tauri::generate_context!())
7 | .expect("error while running tauri application");
8 | }
9 |
--------------------------------------------------------------------------------
/docker-compose/readme.md:
--------------------------------------------------------------------------------
1 | ### docker-compose 部署教程
2 | - 将打包好的前端文件放到 `nginx/html` 目录下
3 | - ```shell
4 | # 拉取新镜像
5 | docker-compose pull
6 | ```
7 | - ```shell
8 | # 启动
9 | docker-compose up -d
10 | ```
11 | - ```shell
12 | # 查看运行状态
13 | docker ps
14 | ```
15 | - ```shell
16 | # 结束运行
17 | docker-compose down
18 | ```
19 |
--------------------------------------------------------------------------------
/service/src/modules/app/index.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | export { AppController, AppService };
6 |
7 | @Module({
8 | controllers: [AppController],
9 | providers: [AppService],
10 | })
11 | export class AppModule {}
12 |
--------------------------------------------------------------------------------
/start.cmd:
--------------------------------------------------------------------------------
1 | cd ./service
2 | @echo on
3 | start cmd /k "echo start service... & echo open service/service.log view log & npm run dev > service.log &"
4 | echo "Start service complete!"
5 |
6 |
7 | cd ..
8 | @echo on
9 | start cmd /k "echo start front... & echo open front.log view log & npm run dev > front.log &"
10 | echo "Start front complete!"
11 |
--------------------------------------------------------------------------------
/service/src/modules/app/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 |
4 | @Controller()
5 | export class AppController {
6 | constructor(private readonly appService: AppService) {}
7 |
8 | @Get()
9 | getHello(): string {
10 | return this.appService.getHello();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/pages/setting/Box.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from '@/locales'
2 |
3 | function Box(props: { children?: React.ReactElement; style?: React.CSSProperties }) {
4 | const { t } = useTranslation()
5 |
6 | return (
7 |
8 | {props?.children}
9 |
10 | )
11 | }
12 |
13 | export default Box
14 |
--------------------------------------------------------------------------------
/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth'
2 |
3 | declare module 'next-auth' {
4 | /**
5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
6 | */
7 | interface Session {
8 | user: {
9 | /** The user's postal country. */
10 | country: string
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/utils/uuid/index.ts:
--------------------------------------------------------------------------------
1 | // javascript
2 | import { nanoid } from 'nanoid'
3 |
4 | // 将nanoid转换为uuidv4格式
5 | const uuidv4 = () => {
6 | // 生成长度为 32 的随机字符串
7 | const randomString = nanoid();
8 | // 将随机字符串转换为 UUID 格式
9 | const uuid = `${randomString.slice(0, 8)}-${randomString.slice(8, 12)}-${randomString.slice(12, 16)}-${randomString.slice(16, 20)}-${randomString.slice(20)}`;
10 | return uuid
11 | }
12 |
13 | export { uuidv4, nanoid }
14 |
--------------------------------------------------------------------------------
/service/src/modules/chatgpt/index.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigModule } from '@nestjs/config';
3 | import { ChatgptController } from './chatgpt.controller';
4 | import { ChatgptService } from './chatgpt.service';
5 |
6 | export { ChatgptController, ChatgptService };
7 |
8 | @Module({
9 | imports: [ConfigModule],
10 | controllers: [ChatgptController],
11 | providers: [ChatgptService],
12 | })
13 | export class ChatgptModule {}
14 |
--------------------------------------------------------------------------------
/service/src/main.ts:
--------------------------------------------------------------------------------
1 | import { ConfigService } from '@nestjs/config';
2 | import { NestFactory } from '@nestjs/core';
3 | import { AppModule } from './app.module';
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(AppModule, {
7 | cors: true,
8 | });
9 | app.setGlobalPrefix('api');
10 | const configService = app.get(ConfigService);
11 | const port = configService.get('PORT', 3002);
12 | await app.listen(port);
13 | }
14 | bootstrap();
15 |
--------------------------------------------------------------------------------
/src/themes/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ConfigProvider } from 'antd'
3 |
4 | export const colorPrimary = '#1677ff'
5 | // export const colorPrimary = '#00B96B'
6 |
7 | const withTheme = (node: JSX.Element) => (
8 | <>
9 |
16 | {node}
17 |
18 | >
19 | )
20 |
21 | export default withTheme
22 |
--------------------------------------------------------------------------------
/src/contexts/index.tsx:
--------------------------------------------------------------------------------
1 | import { ChatProvider, useChatContext } from './chat'
2 | import { SiteProvider, useSiteContext } from './site'
3 | import { PromptProvider, usePromptContext } from './prompt'
4 | import { PluginProvider, usePluginContext } from './plugin'
5 | import { SettingProvider, useSettingContext } from './setting'
6 | export { ChatProvider, SiteProvider, PromptProvider, useChatContext, useSiteContext, usePromptContext, SettingProvider, useSettingContext, PluginProvider, usePluginContext }
7 |
--------------------------------------------------------------------------------
/src/components/Icon/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as icons from '@ant-design/icons';
3 | import { IconProps } from '@ant-design/icons/lib/components/IconBase';
4 |
5 | export interface BaseIconProps extends Omit {
6 | name: string;
7 | }
8 | const Icon = (props: BaseIconProps) => {
9 | const { name } = props;
10 | const antIcon: Record = icons;
11 | return React.createElement(antIcon[name], props);
12 | };
13 |
14 | export default Icon;
15 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # This is an example .env file.
3 | #
4 | # All of these environment vars must be defined either in your environment or in
5 | # a local .env file in order to run the demo for this project.
6 | # ------------------------------------------------------------------------------
7 |
8 | # port
9 | PORT=3000
10 |
11 | # api url
12 | NEXT_PUBLIC_API_URL=http://localhost:3002
13 |
14 | # google analytics
15 | GOOGLE_GA_ID=
--------------------------------------------------------------------------------
/service/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export interface OutputOptions {
2 | code: 0 | number;
3 | message?: string;
4 | data?: T;
5 | }
6 |
7 | export function output(options: OutputOptions) {
8 | if (options.code === 0) {
9 | return Promise.resolve({
10 | code: options.code,
11 | message: options.message ?? null,
12 | data: options.data ?? null,
13 | });
14 | }
15 |
16 | return Promise.reject({
17 | code: options.code,
18 | message: options.message ?? 'Failed',
19 | data: options.data ?? null,
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | // import ChatPage from './chat'
3 |
4 | import dynamic from 'next/dynamic'
5 | import { Spin } from 'antd'
6 |
7 | const ChatPage = dynamic(() => import('./chat'), {
8 | loading: () => (
9 |
10 |
11 |
12 | ),
13 | })
14 |
15 | const App: React.FC = () => {
16 | return (
17 | <>
18 |
19 | >
20 | )
21 | }
22 |
23 | export default App
24 |
--------------------------------------------------------------------------------
/src/pages/share/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | import { useTranslation } from '@/locales'
3 | import { useSiteContext } from '@/contexts/site'
4 | import { useEffect } from 'react'
5 | function IndexPage() {
6 | const { setTitle } = useSiteContext()
7 | const { t } = useTranslation()
8 | useEffect(() => {
9 | const title = t('window.title', { title: t('c.share') })
10 | setTitle(title)
11 | }, [setTitle, t])
12 | return (
13 |
14 |
Hello world! Share
15 |
16 | )
17 | }
18 |
19 | export default IndexPage
20 |
--------------------------------------------------------------------------------
/.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 | /dist/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env
31 | .env.local
32 | .env*.local
33 |
34 | # vercel
35 | .vercel
36 |
37 | # typescript
38 | *.tsbuildinfo
39 | next-env.d.ts
40 |
41 | # log
42 | *.log
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docker-compose/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name localhost;
4 | charset utf-8;
5 | error_page 500 502 503 504 /50x.html;
6 | location / {
7 | # root /usr/share/nginx/html;
8 | # try_files $uri /index.html;
9 | proxy_pass http://app:3000;
10 | }
11 |
12 | location /api {
13 | proxy_buffering off;
14 | proxy_set_header X-Real-IP $remote_addr; #转发用户IP
15 | proxy_pass http://app:3002;
16 | }
17 |
18 | proxy_set_header Host $host;
19 | proxy_set_header X-Real-IP $remote_addr;
20 | proxy_set_header REMOTE-HOST $remote_addr;
21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
22 | }
23 |
--------------------------------------------------------------------------------
/service/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "devCommand": "npm run dev",
3 | "outputDirectory": "./dist/",
4 | "routes": [
5 | {
6 | "src": "/(.*)",
7 | "dest": "main.js",
8 | "methods": ["GET", "POST", "PUT", "DELETE", "PATCH"],
9 | "headers": {
10 | "Access-Control-Allow-Credentials": "true",
11 | "Access-Control-Allow-Origin": "*",
12 | "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT",
13 | "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version"
14 | }
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/service/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "lib": ["esnext"],
5 | "esModuleInterop": true,
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "resolveJsonModule": true,
9 | "isolatedModules": true,
10 | "declaration": true,
11 | "removeComments": true,
12 | "emitDecoratorMetadata": true,
13 | "experimentalDecorators": true,
14 | "allowSyntheticDefaultImports": true,
15 | "sourceMap": true,
16 | "outDir": "./dist",
17 | "baseUrl": "./",
18 | "incremental": true,
19 | "skipLibCheck": true
20 | },
21 | "include": ["**/*.ts"],
22 | "exclude": ["node_modules", "dist"]
23 | }
--------------------------------------------------------------------------------
/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", "**/*.ts", "**/*.tsx"],
22 | "exclude": ["node_modules", "service", "out", "src-tauri"]
23 | }
24 |
--------------------------------------------------------------------------------
/service/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (options, webpack) => {
2 | const lazyImports = [
3 | '@nestjs/microservices/microservices-module',
4 | '@nestjs/websockets/socket-module',
5 | ];
6 |
7 | return {
8 | ...options,
9 | externals: [/node_modules/],
10 | plugins: [
11 | ...options.plugins,
12 | new webpack.IgnorePlugin({
13 | checkResource(resource) {
14 | if (lazyImports.includes(resource)) {
15 | try {
16 | require.resolve(resource);
17 | } catch (err) {
18 | return true;
19 | }
20 | }
21 | return false;
22 | },
23 | }),
24 | ],
25 | };
26 | };
27 |
28 |
--------------------------------------------------------------------------------
/service/src/middleware/auth.middleware.ts:
--------------------------------------------------------------------------------
1 | export function auth(req, res, next) {
2 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY;
3 | if (!AUTH_SECRET_KEY) {
4 | try {
5 | const Authorization = req.header('Authorization');
6 | if (
7 | !Authorization ||
8 | Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim()
9 | ) {
10 | throw new Error('Error: 无访问权限 | No access rights');
11 | }
12 | next();
13 | } catch (error) {
14 | res.send({
15 | status: 'Unauthorized',
16 | message: error.message ?? 'Please authenticate.',
17 | data: null,
18 | });
19 | }
20 | } else {
21 | next();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Html, Head, Main, NextScript } from 'next/document'
2 |
3 |
4 |
5 | export default function Document() {
6 | return (
7 |
8 |
9 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/service/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/service/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 | import { Test } from '@nestjs/testing';
3 | import { AppModule } from '../src/modules/module';
4 | import { INestApplication } from '@nestjs/common';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeAll(async () => {
10 | const moduleFixture = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | afterAll(async () => {
19 | await app.close();
20 | });
21 |
22 | it('/ (GET)', () => {
23 | return request(app.getHttpServer())
24 | .get('/')
25 | .expect(200)
26 | .expect('Hello World!');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/service/src/middleware/ratelimit.middleware.ts:
--------------------------------------------------------------------------------
1 | // import { rateLimit } from 'express-rate-limit';
2 |
3 | // const MAX_REQUEST_PER_HOUR = process.env.MAX_REQUEST_PER_HOUR;
4 |
5 | // const maxCount =
6 | // !MAX_REQUEST_PER_HOUR && !isNaN(Number(MAX_REQUEST_PER_HOUR))
7 | // ? parseInt(MAX_REQUEST_PER_HOUR)
8 | // : 0; // 0 means unlimited
9 |
10 | // export const ratelimit = rateLimit({
11 | // windowMs: 60 * 60 * 1000, // Maximum number of accesses within an hour
12 | // max: maxCount,
13 | // statusCode: 200, // 200 means success,but the message is 'Too many request from this IP in 1 hour'
14 | // message: async (req, res) => {
15 | // res.send({
16 | // status: 'Fail',
17 | // message: 'Too many request from this IP in 1 hour',
18 | // data: null,
19 | // });
20 | // },
21 | // });
22 |
--------------------------------------------------------------------------------
/src/types/prompt.d.ts:
--------------------------------------------------------------------------------
1 | export interface Prompt {
2 | uuid: string // uuid
3 | name?: string // 名称
4 | image?: string // 图片
5 | intro?: string // 简介
6 | description?: string // 描述
7 | prompt?: string // 提示词
8 | context?: { role: string; content: string }[] // 上下文内容
9 | modelConfig?: { [key: string]: any } // 模型配置
10 | category?: string // 分类
11 | type?: 'text' // 类型
12 | self?: boolean // 是否自有
13 | isRecommend?: boolean // 是否推荐
14 | isOfficial?: boolean // 是否官方
15 | isStar?: boolean // 是否收藏
16 | isSystem?: boolean // 是否系统预设
17 | status?: string // 状态
18 | datetime?: string // 时间
19 | options?: { [key: string]: any }[] // 选项
20 | private?: boolean // 是否私有
21 | star?: number // 收藏星数
22 | createUser?: string // 创建人
23 | historyList?: Prompt[] // 历史版本
24 | // 多语言
25 | lang?: string | string[] // 语言
26 | }
27 |
--------------------------------------------------------------------------------
/src/types/plugin.d.ts:
--------------------------------------------------------------------------------
1 | export interface Plugin {
2 | uuid: string // uuid
3 | name?: string // 名称
4 | image?: string // 图片
5 | intro?: string // 简介
6 | description?: string // 描述
7 | category?: string // 分类
8 | mail?: string // 邮箱
9 | website?: string // 网站
10 | apiurl?: string // api地址
11 | namespace?: string // 命名空间
12 | datetime?: string // 时间
13 | status?: string // 状态
14 | version?: string // 版本
15 | isInstall?: boolean // 是否安装
16 | isRecommend?: boolean // 是否推荐
17 | isNew?: boolean // 是否新品
18 | isStar?: boolean // 是否收藏
19 | isSystem?: boolean // 是否系统预设
20 | apply?: string | string[] // 适用
21 | // 多语言
22 | lang?: {
23 | [key: string]: {
24 | name?: string // 名称
25 | image?: string // 图片
26 | intro?: string // 简介
27 | description?: string // 描述
28 | }
29 | }
30 | basicPrompts?: string[] // 基础提示词
31 | advancedPrompts?: string[] // 高级提示词
32 | }
33 |
--------------------------------------------------------------------------------
/docker-compose/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | app:
5 | image: zpd106/chatgpt-plus # 总是使用latest,更新时重新pull该tag镜像即可
6 | ports:
7 | - 3000:3000
8 | - 3002:3002
9 | environment:
10 | # chatgpt api key,可选,不设置时使用OPENAI_ACCESS_TOKEN
11 | OPENAI_API_KEY:
12 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用
13 | OPENAI_API_BASE_URL:
14 | # chatgpt access token,可选,不设置时使用OPENAI_API_KEY
15 | OPENAI_ACCESS_TOKEN:
16 | # 反向代理
17 | API_REVERSE_PROXY:
18 | # 每小时最大请求次数,可选,默认无限
19 | MAX_REQUEST_PER_HOUR: 0
20 | # 超时,单位毫秒,可选
21 | TIMEOUT_MS: 60000
22 | volumes:
23 | - ./log:/app/log
24 | nginx:
25 | image: nginx:alpine
26 | ports:
27 | - '80:80'
28 | expose:
29 | - '80'
30 | volumes:
31 | - ./nginx/html:/usr/share/nginx/html
32 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
33 | links:
34 | - app
35 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "app"
3 | version = "0.1.0"
4 | description = "A Tauri App"
5 | authors = ["you"]
6 | license = ""
7 | repository = ""
8 | default-run = "app"
9 | edition = "2021"
10 | rust-version = "1.60"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "1.3.0", features = [] }
16 |
17 | [dependencies]
18 | serde_json = "1.0"
19 | serde = { version = "1.0", features = ["derive"] }
20 | tauri = { version = "1.3.0", features = ["shell-open", "system-tray"] }
21 |
22 | [features]
23 | # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
24 | # If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
25 | # DO NOT REMOVE!!
26 | custom-protocol = [ "tauri/custom-protocol" ]
27 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | app:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: git config
21 | run: |
22 | git config user.name "${{secrets.CI_GIT_NAME}}"
23 | git config user.email "${{secrets.CI_GIT_EMAIL}}"
24 | git config --global --add safe.directory /home/runner/work/chatgpt-plus/chatgpt-plus
25 |
26 | - name: Set node
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: 18.x
30 |
31 | - name: Install
32 | run: npm i
33 |
34 | - name: Release
35 | run: npm run release-ci -- --ci --no-git.requireCleanWorkingDir
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 |
39 |
--------------------------------------------------------------------------------
/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service/README.md:
--------------------------------------------------------------------------------
1 |
2 | # ChatGPT-Plus API
3 |
4 | > Node.js client for [ChatGPT-Plus](https://github.com/zhpd/chatgpt-plus.git) service API.
5 |
6 | ## Description
7 |
8 | [ChatGPT-Plus](https://github.com/zhpd/chatgpt-plus.git) framework TypeScript starter repository.
9 |
10 | ## Installation
11 |
12 | ```bash
13 | $ npm install
14 | ```
15 |
16 | ## Running the app
17 |
18 | ```bash
19 | # development
20 | $ npm run dev
21 |
22 | # watch mode
23 | $ npm run start:dev
24 |
25 | # production mode
26 | $ npm run start:prod
27 | ```
28 |
29 | ## Test
30 |
31 | ```bash
32 | # unit tests
33 | $ npm run test
34 |
35 | # e2e tests
36 | $ npm run test:e2e
37 |
38 | # test coverage
39 | $ npm run test:cov
40 | ```
41 |
42 | ## Support
43 |
44 | ChatGPT-Plus is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them.
45 |
46 | ## License
47 |
48 | ChatGPT-Plus is [MIT licensed](https://github.com/zhpd/chatgpt-plus/blob/master/LICENSE).
49 |
--------------------------------------------------------------------------------
/service/.env.example:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 | # This is an example .env file.
3 | #
4 | # All of these environment vars must be defined either in your environment or in
5 | # a local .env file in order to run the demo for this project.
6 | # ------------------------------------------------------------------------------
7 |
8 | # -----------------------------------------------------------------------------
9 | # OpenAI
10 | # -----------------------------------------------------------------------------
11 |
12 | # OpenAI API Key - https://platform.openai.com/overview
13 | OPENAI_API_KEY=
14 |
15 | # OpenAI Base Url - https://api.openai.com/v1
16 | OPENAI_API_BASE_URL=
17 |
18 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
19 | OPENAI_ACCESS_TOKEN=
20 |
21 | # Reverse Proxy default 'https://api.pawan.krd/backend-api/conversation'
22 | API_REVERSE_PROXY=
23 |
24 | # timeout
25 | TIMEOUT_MS=60000
26 |
27 | # Rate Limit
28 | MAX_REQUEST_PER_HOUR=
--------------------------------------------------------------------------------
/src/utils/request/axios.ts:
--------------------------------------------------------------------------------
1 | import axios, { type AxiosResponse } from 'axios'
2 | import storage from '../storage'
3 |
4 | const service = axios.create({
5 | baseURL: '/',
6 | timeout: 60000,
7 | })
8 |
9 | service.interceptors.request.use(
10 | async (config) => {
11 | const token: string | '' = typeof window !== 'undefined' ? localStorage.getItem('token') || '' : ''
12 | if (token) config.headers.Authorization = `Bearer ${token}`
13 | // 如果设置里baseURL, 则需要设置变量
14 | const backend_base_url = await storage.get('backend_base_url')
15 | if (backend_base_url) {
16 | config.baseURL = backend_base_url
17 | }
18 | return config
19 | },
20 | (error) => {
21 | return Promise.reject(error.response)
22 | }
23 | )
24 |
25 | service.interceptors.response.use(
26 | (response: AxiosResponse): AxiosResponse => {
27 | if (response.status === 200) return response
28 |
29 | throw new Error(response.status.toString())
30 | },
31 | (error) => {
32 | return Promise.reject(error)
33 | }
34 | )
35 |
36 | export default service
37 |
--------------------------------------------------------------------------------
/src/pages/store/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Result, Empty } from 'antd'
2 | import Head from 'next/head'
3 | import { useTranslation } from '@/locales'
4 | import { useSiteContext } from '@/contexts/site'
5 | import { useEffect } from 'react'
6 | function IndexPage() {
7 | const { setTitle } = useSiteContext()
8 | const { t } = useTranslation()
9 | useEffect(() => {
10 | const title = t('window.title', { title: t('c.store') })
11 | setTitle(title)
12 | }, [setTitle, t])
13 |
14 | const emptyStyle: React.CSSProperties = {
15 | height: '100%',
16 | alignItems: 'center',
17 | display: 'flex',
18 | flexDirection: 'column',
19 | justifyContent: 'center',
20 | }
21 | return (
22 |
28 | // Go Github
29 | //
30 | // }
31 | />
32 | )
33 | }
34 |
35 | export default IndexPage
36 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Hey Jude
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/data/prompt.ts:
--------------------------------------------------------------------------------
1 | import type { Prompt } from '@/types/prompt'
2 | import { uuidv4 } from '@/utils'
3 | // 导出提示词列表数据
4 | export const promptList: Prompt[] = [
5 | {
6 | uuid: uuidv4(),
7 | name: '机器学习',
8 | image: 'https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/apple/64/1f600.png',
9 | description: '机器学习',
10 | intro: '我想让你担任机器学习工程师。我会写一些机器学习的概念,你的工作就是用通俗易懂的术语来解释它们。这可能包括提供构建模型的分步说明、给出所用的技术或者理论、提供评估函数等。我的问题是',
11 | prompt: '我想让你担任机器学习工程师。我会写一些机器学习的概念,你的工作就是用通俗易懂的术语来解释它们。这可能包括提供构建模型的分步说明、给出所用的技术或者理论、提供评估函数等。我的问题是',
12 | datetime: '2023/3/20 11:32:26',
13 | type: 'text',
14 | status: 'online',
15 | private: true,
16 | star: 0,
17 | isStar: false,
18 | isSystem: true,
19 | modelConfig: {
20 | model: 'GPT-3.5-Turbo',
21 | },
22 | historyList: [
23 | {
24 | uuid: uuidv4(),
25 | datetime: '2023/3/20 11:32:26',
26 | name: '机器学习',
27 | description: '机器学习',
28 | prompt: '机器学习',
29 | type: 'text',
30 | status: 'online',
31 | },
32 | ],
33 | lang: ['zh_CN'],
34 | },
35 | ]
36 |
37 | export default promptList
38 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/readme/index.tsx:
--------------------------------------------------------------------------------
1 | import { theme as antdTheme, Spin } from 'antd'
2 | import { useTranslation } from '@/locales'
3 | import { useSiteContext } from '@/contexts/site'
4 | import { useEffect } from 'react'
5 | // import Markdown from '@/components/pages/chat/Markdown'
6 | import dynamic from 'next/dynamic'
7 |
8 | const Markdown = dynamic(() => import('@/components/pages/chat/Markdown'), {
9 | loading: () => (
10 |
11 |
12 |
13 | ),
14 | })
15 |
16 | // 读取 README.md的内容
17 | const text = require('!raw-loader!../../../README.md').default
18 |
19 | function IndexPage() {
20 | const { theme } = useSiteContext()
21 | const { token } = antdTheme.useToken()
22 | const { setTitle } = useSiteContext()
23 | const { t } = useTranslation()
24 |
25 | useEffect(() => {
26 | const title = t('window.title', { title: t('c.readme') })
27 | setTitle(title)
28 | }, [setTitle, t])
29 |
30 | return (
31 |
32 |
42 | {text}
43 |
44 |
45 | )
46 | }
47 |
48 | export default IndexPage
49 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "changelog": "npx auto-changelog --stdout --commit-limit false --unreleased --template https://raw.githubusercontent.com/release-it/release-it/master/templates/changelog-compact.hbs",
4 | "commitMessage": "chore: release ${version}",
5 | "tagName": "v${version}",
6 | "requireCleanWorkingDir": true,
7 | "requireUpstream": false
8 | },
9 | "hooks": {
10 | "before:init": ["npm run lint"],
11 | "after:bump": "npx auto-changelog -p"
12 | },
13 | "npm": {
14 | "skipChecks": true,
15 | "publish": false
16 | },
17 | "github": {
18 | "release": true
19 | },
20 | "plugins": {
21 | "@release-it/conventional-changelog": {
22 | "infile": "CHANGELOG.md",
23 | "ignoreRecommendedBump": true,
24 | "preset": {
25 | "name": "conventionalcommits",
26 | "types": [
27 | { "type": "feat", "section": "Features" },
28 | { "type": "fix", "section": "Bug Fixes" },
29 | { "type": "chore", "section": "Chore", "hidden": false },
30 | { "type": "docs", "section": "Docs", "hidden": false },
31 | { "type": "style", "section": "Style", "hidden": false },
32 | { "type": "refactor", "section": "Refactor", "hidden": false },
33 | { "type": "perf", "section": "Perf", "hidden": false },
34 | { "type": "test", "section": "Test", "hidden": false }
35 | ]
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | // output mode: 'export' | 'standalone'
4 | let output = 'standalone'
5 | if (process.env.APP_ENV === 'tauri') {
6 | output = 'export'
7 | console.log('APP_ENV', process.env.APP_ENV)
8 | }
9 | const nextConfig = {
10 | reactStrictMode: false,
11 | output: output,
12 | // distDir: 'dist', // 静态化
13 | // trailingSlash: true,
14 | transpilePackages: ['antd','ahooks'],
15 | ...(output !== 'export'
16 | ? {
17 | rewrites() {
18 | return {
19 | fallback: [
20 | {
21 | source: '/api/:path*',
22 | destination: `${process.env.NEXT_PUBLIC_API_URL || ''}/api/:path*`, // Proxy to Backend
23 | },
24 | ],
25 | }
26 | },
27 | }
28 | : {}),
29 | images: {
30 | unoptimized: true,
31 | remotePatterns: [
32 | {
33 | protocol: 'http',
34 | hostname: '**',
35 | },
36 | ],
37 | dangerouslyAllowSVG: true,
38 | contentDispositionType: 'attachment',
39 | contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
40 | },
41 | webpack(config) {
42 | config.module.rules.push({
43 | test: /\.svg$/,
44 | issuer: /\.[jt]sx?$/,
45 | resourceQuery: { not: /url/ }, // exclude if *.svg?url
46 | use: ['@svgr/webpack'],
47 | }) // 针对 SVG 的处理规则
48 | return config
49 | },
50 | }
51 |
52 | module.exports = nextConfig
53 |
--------------------------------------------------------------------------------
/src/locales/index.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next'
2 | import { initReactI18next } from 'react-i18next'
3 | import LanguageDetector from 'i18next-browser-languagedetector'
4 | // import path from 'path'
5 | // 导入语言枚举
6 | // const LanguageList = ['zh_CN', 'zh_TW', 'en_US']
7 | import { LanguageList } from '@/config/constant'
8 |
9 | const resources = () => {
10 | const _resources: any = {}
11 | for (const lang of LanguageList) {
12 | // 只加载简体中文,繁体中文,英文
13 | if (lang?.value !== 'zh_CN' && lang?.value !== 'zh_TW' && lang?.value !== 'en_US') continue
14 | _resources[lang?.value] = {
15 | translation: require(`./${lang?.value}.json`),
16 | }
17 | }
18 | return _resources
19 | }
20 |
21 | // init i18next
22 | i18next
23 | .use(LanguageDetector)
24 | .use(initReactI18next)
25 | .init(
26 | {
27 | resources: resources(),
28 | fallbackLng: {
29 | default: [LanguageList?.[0]?.value],
30 | },
31 | defaultNS: 'translation',
32 | ns: [],
33 | detection: {
34 | caches: ['localStorage', 'sessionStorage', 'cookie'],
35 | },
36 | debug: false,
37 | },
38 | () => {
39 | console.log('i18next init', i18next.t('title'))
40 | }
41 | )
42 | export default i18next
43 |
44 | export const changeLanguage = (val: string | undefined) => {
45 | i18next.changeLanguage(val)
46 | }
47 |
48 | export function useTranslation(ns = [], options = {}) {
49 | return {
50 | t: i18next.t,
51 | i18n: i18next,
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/plugin/index.tsx:
--------------------------------------------------------------------------------
1 | import Store from '@/components/pages/plugin/Store'
2 | import { useTranslation } from '@/locales'
3 | import { useSiteContext } from '@/contexts/site'
4 | import { ReactNode, useEffect, useState } from 'react'
5 | import { usePluginContext } from '@/contexts'
6 | import { useRouter } from 'next/router'
7 |
8 | function IndexPage() {
9 | const router = useRouter()
10 | const { setTitle, event$ } = useSiteContext()
11 | const { t } = useTranslation()
12 | const { pluginList } = usePluginContext()
13 | const [action, setAction] = useState('')
14 | const [openList, setOpenList] = useState(true)
15 | const [ContentElement, setContentElement] = useState(<>>)
16 |
17 | event$.useSubscription((val: any) => {
18 | if (val?.type == 'tabSwich') {
19 | // 二次点击,则隐藏消息列表
20 | if (val?.url.indexOf('/plugin') > -1) {
21 | console.log(val)
22 | setOpenList(!openList)
23 | }
24 | }
25 | })
26 | useEffect(() => {
27 | const title = t('window.title', { title: t('c.plugin') })
28 | setTitle(title)
29 | }, [setTitle, t])
30 |
31 | const setContent = (ele: ReactNode) => {
32 | setContentElement(ele)
33 | }
34 |
35 | const renderBox = () => {
36 | return ContentElement
37 | }
38 | return (
39 | <>
40 |
41 |
42 |
43 | >
44 | )
45 | }
46 |
47 | export default IndexPage
48 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # # build front-end
2 | # FROM node:lts-alpine AS frontend
3 |
4 | # WORKDIR /app
5 |
6 | # COPY . /app
7 | # COPY ./package.json /app
8 | # COPY ./package-lock.json /app
9 | # RUN npm install --registry=https://registry.npmmirror.com
10 | # RUN npm run build
11 |
12 | # RUN rm -rf ./node_modules
13 |
14 | # # build backend
15 | # FROM node:lts-alpine AS backend
16 |
17 | # WORKDIR /app
18 |
19 | # COPY ./service /app
20 | # COPY ./service/package.json /app
21 | # COPY ./service/package-lock.json /app
22 | # RUN npm install --registry=https://registry.npmmirror.com
23 | # RUN npm run build
24 |
25 | # RUN rm -rf ./node_modules
26 |
27 | # service
28 | FROM node:lts-alpine
29 |
30 | RUN apk add --no-cache bash
31 |
32 | RUN mkdir -p /app/log
33 |
34 | WORKDIR /app
35 | # # copy front-end -- 从docker构建中复制,运行太慢
36 | # COPY --from=frontend /app/.next/standalone /app/web
37 | # COPY --from=frontend /app/.next/static /app/web/.next/static
38 | # COPY ./public /app/web/public
39 |
40 | # copy .next/front -- 从源码构建结果中复制
41 | COPY .next/standalone /app/web
42 | COPY .next/static /app/web/.next/static
43 | COPY ./public /app/web/public
44 |
45 | WORKDIR /app/service
46 | # # copy backend -- 从docker构建中复制,运行太慢
47 | # COPY --from=backend /app/dist /app/service/dist
48 |
49 | # copy service/dist/backend -- 从源码构建结果中复制
50 | COPY service/dist /app/service/dist
51 |
52 | # start.sh
53 | COPY ./docker-start.sh /app
54 |
55 | EXPOSE 3000
56 |
57 | EXPOSE 3002
58 |
59 | WORKDIR /app
60 |
61 | CMD [ "/bin/sh", "./docker-start.sh" ]
62 |
--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
2 | import { post } from '@/utils/request'
3 |
4 | export function fetchChatAPI(text: string, options?: { conversationId?: string; parentMessageId?: string }, config?: { [key: string]: any }, signal?: GenericAbortSignal) {
5 | return post({
6 | url: '/api/chatgpt/chat',
7 | data: { text, options, config },
8 | signal,
9 | })
10 | }
11 |
12 | export function fetchChatAPIProcess(params: {
13 | text: string
14 | options?: { conversationId?: string; parentMessageId?: string }
15 | config?: { [key: string]: any }
16 | signal?: GenericAbortSignal
17 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
18 | systemMessage?: string
19 | }) {
20 | return post({
21 | url: '/api/chatgpt/chat',
22 | data: { text: params.text, options: params.options, config: params.config, systemMessage: params.systemMessage },
23 | signal: params.signal,
24 | onDownloadProgress: params.onDownloadProgress,
25 | // headers: { 'content-type': 'application/octet-stream' },
26 | })
27 | }
28 |
29 | export function fetchSession() {
30 | return post({
31 | url: '/api/chatgpt/session',
32 | })
33 | }
34 |
35 | export function fetchChatConfig() {
36 | return post({
37 | url: '/api/chatgpt/config',
38 | })
39 | }
40 |
41 | export function fetchVerify(token: string) {
42 | return post({
43 | url: '/api/chatgpt/verify',
44 | data: { token },
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/EmojiPicker/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Tooltip } from 'antd'
3 | import EmojiPicker from './index'
4 | import Image from 'next/image'
5 |
6 | const ButtonEmojiPicker = (props: {
7 | value?: ''
8 | readOnly?: boolean
9 | onChange?: (val: any) => void
10 | onEmojiClick?: (emoji: any) => void
11 | theme?: 'auto' | 'light' | 'dark'
12 | style?: 'native' | 'apple' | 'google' | 'facebook' | 'microsoft' | 'bubble'
13 | }) => {
14 | const [_value, setValue] = React.useState(props.value)
15 | return (
16 | {
30 | props?.onEmojiClick?.(emoji)
31 | props?.onChange?.(emoji.imageUrl)
32 | setValue(emoji.imageUrl)
33 | }}
34 | />
35 | }
36 | >
37 |
40 |
41 | )
42 | }
43 |
44 | export default ButtonEmojiPicker
45 |
--------------------------------------------------------------------------------
/src/pages/prompt/index.tsx:
--------------------------------------------------------------------------------
1 | import List from '@/components/pages/prompt/List'
2 | import { useTranslation } from '@/locales'
3 | import { useSiteContext } from '@/contexts/site'
4 | import { ReactNode, useEffect, useState } from 'react'
5 | import Store from '@/components/pages/prompt/Store'
6 |
7 | function IndexPage() {
8 | const { setTitle, event$ } = useSiteContext()
9 | const { t } = useTranslation()
10 | const [openList, setOpenList] = useState(true)
11 | const [ContentElement, setContentElement] = useState()
12 |
13 | event$.useSubscription((val: any) => {
14 | if (val?.type == 'tabSwich') {
15 | // 二次点击,则隐藏消息列表
16 | if (val?.url.indexOf('/prompt') > -1) {
17 | console.log(val)
18 | setOpenList(!openList)
19 | }
20 | }
21 | })
22 | useEffect(() => {
23 | const title = t('window.title', { title: t('c.prompt') })
24 | setTitle(title)
25 | }, [setTitle, t])
26 |
27 | const setContent = (ele: ReactNode) => {
28 | setContentElement(ele)
29 | }
30 |
31 | const renderBox = () => {
32 | return ContentElement
33 | }
34 | return (
35 | <>
36 |
37 |
38 |
{renderBox()}
39 |
40 | >
41 | )
42 | }
43 |
44 | export default IndexPage
45 |
--------------------------------------------------------------------------------
/src/components/EmojiPicker/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import data from '@emoji-mart/data'
3 | import Picker from '@emoji-mart/react'
4 |
5 | export function getEmojiUrl(unified: string, style: string) {
6 | // return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`
7 | return `https://www.emojiall.com/images/120/${style}/${unified}.png`
8 | }
9 |
10 | const EmojiPicker = (props: { onEmojiClick: (emoji: any) => void; theme?: 'auto' | 'light' | 'dark'; style?: 'native' | 'apple' | 'google' | 'facebook' | 'microsoft' | 'bubble' }) => {
11 | const [style, setStyle] = useState(props.style || 'apple')
12 | useEffect(() => {
13 | console.log('EmojiPicker navigator', navigator)
14 | if (navigator?.platform?.indexOf('Win') !== -1) {
15 | // 当前为Windows系统
16 | setStyle('microsoft')
17 | } else {
18 | setStyle('apple')
19 | }
20 | }, [])
21 |
22 | useEffect(() => {
23 | if (props.style == 'native') {
24 | if (navigator?.platform?.indexOf('Win') !== -1) {
25 | // 当前为Windows系统
26 | setStyle('microsoft')
27 | } else {
28 | setStyle('apple')
29 | }
30 | } else {
31 | if (props.style) setStyle(props.style)
32 | }
33 | }, [props.style])
34 |
35 | return (
36 | {
40 | // 返回emoji图片链接
41 | emoji.imageUrl = getEmojiUrl(emoji.unified, style || 'apple')
42 | console.log('onEmojiSelect', emoji)
43 | props.onEmojiClick(emoji)
44 | }}
45 | />
46 | )
47 | }
48 |
49 | export default EmojiPicker
50 |
--------------------------------------------------------------------------------
/src/data/plugin.ts:
--------------------------------------------------------------------------------
1 | import type { Plugin } from '@/types/plugin'
2 | import { uuidv4 } from '@/utils'
3 | // 导出插件列表数据
4 | export const pluginList: Plugin[] = [
5 | {
6 | uuid: uuidv4(),
7 | name: 'Zapier',
8 | image: 'https://cdn.zappy.app/8f853364f9b383d65b44e184e04689ed.png',
9 | intro:
10 | 'Interact with over 5,000+ apps like Google Sheets, Gmail, HubSpot, Salesforce, and thousands more.Interact with over 5,000+ apps like Google Sheets, Gmail, HubSpot, Salesforce, and thousands more.',
11 | description:
12 | 'Zapier can talk to any of 20k+ actions the user has exposed. Actions are single tasks (EG: add a lead, find a doc), Zaps are workflows of actions. Start new chat to refresh actions. Markdown links are relative to https://zapier.com/.',
13 | mail: 'nla@zapier.com',
14 | website: 'zapier.com',
15 | apiurl: 'https://nla.zapier.com/api/v1/dynamic/openapi.json',
16 | namespace: 'Zapier',
17 | datetime: '2023/3/20 11:32:26',
18 | apply: 'chatgpt',
19 | isInstall: false,
20 | isStar: true,
21 | isNew: true,
22 | isRecommend: true,
23 | basicPrompts: ['连接我的谷歌表,获取有关我的最新开支的数据'],
24 | advancedPrompts: ['建立一个Zapier任务,把我的Gmail附件保存到Dropbox里', '创建一个Zap任务,把我的Instagram照片发到Twitter 上', '建立一个自动化任务,把新的销售领导添加到我的谷歌表格中。'],
25 | lang: {
26 | zh_CN: {
27 | name: 'Zapier',
28 | intro: '与超过5,000个应用程序(如Google表格,Gmail,HubSpot,Salesforce等)进行交互。与超过5,000个应用程序(如Google表格,Gmail,HubSpot,Salesforce等)进行交互。',
29 | description:
30 | 'Zapier可以与用户公开的20k +操作中的任何操作进行通信。操作是单个任务(例如:添加线索,查找文档),Zaps是操作的工作流程。开始新的聊天以刷新操作。 Markdown链接相对于https://zapier.com/。',
31 | },
32 | },
33 | },
34 | ]
35 |
36 | export default pluginList
--------------------------------------------------------------------------------
/src/types/chat.d.ts:
--------------------------------------------------------------------------------
1 | export interface Chat {
2 | uuid: string
3 | name?: string
4 | avatar?: string
5 | description?: string
6 | type?: string
7 | status?: string
8 | place?: 'left' | 'right'
9 | config?: { [key: string]: any }
10 | option?: { [key: string]: any }
11 | conversationId?: string
12 | lastMessage?: Message // last message
13 | lastMessageText?: string // last message text
14 | lastMessageTime?: string // last message time
15 | messageList?: Message[] // all messages
16 | }
17 |
18 | export interface Message {
19 | id?: string
20 | uuid?: string
21 | dateTime: string
22 | text: string
23 | inversion?: boolean
24 | error?: boolean
25 | loading?: boolean
26 | temp?: boolean
27 | conversationId?: string
28 | messageId?: string
29 | conversationOptions?: ConversationRequest | null
30 | requestOptions?: { prompt: string; options?: ConversationRequest | null } | null
31 | conversationRequest?: ConversationRequest | null
32 | conversationResponse?: ConversationResponse | null
33 | }
34 |
35 | export interface History {
36 | title: string
37 | isEdit: boolean
38 | uuid: number
39 | }
40 |
41 | export interface ChatState {
42 | active: number | null
43 | usingContext: boolean
44 | history: History[]
45 | chat: { uuid: number; data: Chat[] }[]
46 | }
47 |
48 | export interface ConversationRequest {
49 | messageId?: string
50 | conversationId?: string
51 | parentMessageId?: string
52 | }
53 |
54 | export interface ConversationResponse {
55 | conversationId: string
56 | detail: {
57 | choices: { finish_reason: string; index: number; logprobs: any; text: string }[]
58 | created: number
59 | id: string
60 | model: string
61 | object: string
62 | usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number }
63 | }
64 | id: string
65 | parentMessageId: string
66 | role: string
67 | text: string
68 | }
69 |
--------------------------------------------------------------------------------
/service/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2 | import { APP_GUARD } from '@nestjs/core';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
5 | import { ConfigModule } from '@nestjs/config';
6 | import configuration from './config/configuration';
7 | import { auth, error, logger } from './middleware';
8 | import { AppModule as BaseAppModule } from './modules/app';
9 | import { ChatgptModule } from './modules/chatgpt';
10 |
11 | @Module({
12 | imports: [
13 | ConfigModule.forRoot({
14 | envFilePath: ['.env.local', '.env'],
15 | isGlobal: true,
16 | load: [configuration],
17 | }),
18 | ThrottlerModule.forRoot({
19 | ttl: 60, //1分钟
20 | limit: 10, //请求10次
21 | }),
22 | ...(process.env.DB_OPEN === 'true'
23 | ? [
24 | TypeOrmModule.forRoot({
25 | type: process.env.DB_TYPE as any,
26 | host: process.env.DB_HOST,
27 | port: parseInt(process.env.DB_PORT),
28 | username: process.env.DB_USERNAME,
29 | password: process.env.DB_PASSWORD,
30 | database: process.env.DB_DATABASE,
31 | entities: [],
32 | synchronize: true,
33 | toRetry: (err) => {
34 | console.log('toRetry', err);
35 | return true;
36 | },
37 | }),
38 | ]
39 | : []),
40 | BaseAppModule,
41 | ChatgptModule,
42 | ],
43 | providers: [
44 | {
45 | provide: APP_GUARD,
46 | useClass: ThrottlerGuard,
47 | },
48 | ],
49 | })
50 | export class AppModule implements NestModule {
51 | configure(consumer: MiddlewareConsumer) {
52 | consumer.apply(logger);
53 | consumer.apply(auth);
54 | consumer.apply(error);
55 | // .forRoutes({ path: 'cats', method: RequestMethod.GET });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../node_modules/@tauri-apps/cli/schema.json",
3 | "build": {
4 | "beforeBuildCommand": "npm run build:tauri",
5 | "beforeDevCommand": "npm run dev",
6 | "devPath": "http://localhost:3000",
7 | "distDir": "../out",
8 | "withGlobalTauri": true
9 | },
10 | "package": {
11 | "productName": "ChatGPT-Plus",
12 | "version": "0.1.0"
13 | },
14 | "tauri": {
15 | "allowlist": {
16 | "all": false,
17 | "shell": {
18 | "open": true
19 | }
20 | },
21 | "bundle": {
22 | "active": true,
23 | "category": "DeveloperTool",
24 | "copyright": "",
25 | "deb": {
26 | "depends": []
27 | },
28 | "externalBin": [],
29 | "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
30 | "identifier": "com.chatgptplus.app",
31 | "longDescription": "",
32 | "macOS": {
33 | "entitlements": null,
34 | "exceptionDomain": "",
35 | "frameworks": [],
36 | "providerShortName": null,
37 | "signingIdentity": null
38 | },
39 | "resources": [],
40 | "shortDescription": "",
41 | "targets": "all",
42 | "windows": {
43 | "certificateThumbprint": null,
44 | "digestAlgorithm": "sha256",
45 | "timestampUrl": "",
46 | "webviewInstallMode": {
47 | "type": "embedBootstrapper"
48 | }
49 | }
50 | },
51 | "security": {
52 | "csp": null
53 | },
54 | "updater": {
55 | "active": false
56 | },
57 | "windows": [
58 | {
59 | "fullscreen": false,
60 | "title": "ChatGPT-Plus",
61 | "resizable": true,
62 | "height": 720,
63 | "width": 1080,
64 | "skipTaskbar": false,
65 | "additionalBrowserArgs": "--disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection,PasswordImport"
66 | }
67 | ],
68 | "systemTray": {
69 | "iconPath": "icons/icon.png",
70 | "iconAsTemplate": true
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/.github/workflows/build_docker.yml:
--------------------------------------------------------------------------------
1 | name: build_docker
2 |
3 | on:
4 | workflow_run:
5 | workflows: [ci]
6 | types:
7 | - completed
8 | release:
9 | types: [created] # 表示在创建新的 Release 时触发
10 |
11 | jobs:
12 | build_docker:
13 | name: Build docker
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v3
18 |
19 | - run: |
20 | echo 本次构建的版本为:${{ github.ref_name }}
21 | env
22 |
23 | - name: Set up QEMU
24 | uses: docker/setup-qemu-action@v2
25 | - name: Set up Docker Buildx
26 | uses: docker/setup-buildx-action@v2
27 | - name: Login to DockerHub
28 | uses: docker/login-action@v2
29 | with:
30 | username: ${{ secrets.DOCKERHUB_USERNAME }}
31 | password: ${{ secrets.DOCKERHUB_TOKEN }}
32 |
33 | - name: Set node
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version: 18.x
37 |
38 | - name: Build Web
39 | id: build_web
40 | run: |
41 | npm i
42 | npm run build
43 |
44 | - name: Build Service
45 | id: build_service
46 | run: |
47 | cd service
48 | npm i
49 | npm run build
50 | cd ../
51 |
52 | - name: Show List
53 | id: show_list
54 | run: |
55 | ls -la
56 | cd .next
57 | ls -la
58 | cd ../service/dist
59 | ls -la
60 | cd ../../
61 |
62 | - name: Get version number
63 | id: version
64 | run: |
65 | echo "::set-output name=version::$(node -p "require('./package.json').version")"
66 |
67 | - name: Build and push
68 | id: docker_build
69 | uses: docker/build-push-action@v4
70 | with:
71 | context: .
72 | push: true
73 | labels: ${{ steps.meta.outputs.labels }}
74 | platforms: linux/amd64,linux/arm64
75 | tags: |
76 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-plus:${{ steps.version.outputs.version }}
77 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-plus:latest
78 |
--------------------------------------------------------------------------------
/src/utils/tool/index.md:
--------------------------------------------------------------------------------
1 | # page - 页面工具
2 |
3 | page 工具是为了方便打开侧栏、打开弹窗,可以全局调用,无需在每个页面单独设置侧栏和弹窗的组件,通过属性控制。可全局统一管理打开和关闭。
4 |
5 | ```tsx | pure
6 | // 打开侧栏
7 | const ref = page.showDrawer({'侧栏内容'}
, {
8 | title: '侧栏标题',
9 | });
10 | // 关闭侧栏
11 | page.closeDrawer(ref);
12 |
13 | // 打开弹窗
14 | const ref = page.showModal({'弹窗内容'}
, {
15 | title: '弹窗标题',
16 | });
17 | // 关闭弹窗
18 | page.closeModal(ref);
19 | ```
20 |
21 | ## 代码演示
22 |
23 | ### 打开侧栏
24 |
25 |
26 |
27 | ### 关闭侧栏
28 |
29 |
30 |
31 | ### 打开弹窗
32 |
33 |
34 |
35 | ### 关闭弹窗
36 |
37 |
38 |
39 | ## API
40 |
41 | ### showDrawer
42 |
43 | > 打开侧栏的方法参数
44 |
45 | | 参数 | 说明 | 类型 | 默认值 |
46 | | --- | --- | --- | --- |
47 | | component | 需要呈现的组件, 编辑组件设置 setFooter 属性,可将底部操作栏放置侧栏底部 | ReactNode |
48 | | drawerProps | 侧栏的属性,可透传 Drawer[属性](https://ant.design/components/drawer-cn/) | object | - |
49 |
50 | ### closeDrawer
51 |
52 | > 关闭侧栏的方法参数
53 |
54 | | 参数 | 说明 | 类型 | 默认值 |
55 | | ---- | ------------------------------------------------------ | --------------- | ------ |
56 | | ref | 需要关闭组件的引用,留空则关闭全部打开的侧栏 | React.RefObject |
57 | | node | 需要关闭组件的节点,留空则关闭全部打开的侧栏,[无动画] | Element | - |
58 |
59 | ### showModal
60 |
61 | > 打开弹窗的方法参数
62 |
63 | | 参数 | 说明 | 类型 | 默认值 |
64 | | --- | --- | --- | --- |
65 | | component | 需要呈现的组件, 编辑组件设置 setFooter 属性,可将底部操作栏放置弹窗底部 | ReactNode |
66 | | modalProps | 弹窗的属性,可透传 Modal[属性](https://ant.design/components/modal-cn/) | object | - |
67 |
68 | ### closeModal
69 |
70 | > 关闭弹窗的方法参数
71 |
72 | | 参数 | 说明 | 类型 | 默认值 |
73 | | ---- | ------------------------------------------------------ | --------------- | ------ |
74 | | ref | 需要关闭组件的引用,留空则关闭全部打开的弹窗 | React.RefObject |
75 | | node | 需要关闭组件的节点,留空则关闭全部打开的弹窗,[无动画] | Element | - |
76 |
--------------------------------------------------------------------------------
/src/pages/setting/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Result, Empty, Layout, theme as antdTheme, Tabs, TabsProps } from 'antd'
2 | import { SkinOutlined, ControlOutlined, IdcardOutlined, ExceptionOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
3 | import { useTranslation } from '@/locales'
4 | import { useSiteContext } from '@/contexts/site'
5 | import { useEffect, useState } from 'react'
6 | import Surface from '@/components/pages/setting/Surface'
7 | import Network from '@/components/pages/setting/Network'
8 | import Common from '@/components/pages/setting/Common'
9 |
10 | function IndexPage() {
11 | const { lang, setTitle } = useSiteContext()
12 | const { t } = useTranslation()
13 | const { token } = antdTheme.useToken()
14 | const [items, setItems] = useState([])
15 | const _list: TabsProps['items'] = [
16 | {
17 | key: 'surface',
18 | label: (
19 |
20 |
21 | {t('setting.m_surface')}
22 |
23 | ),
24 | children: ,
25 | },
26 | {
27 | key: 'common',
28 | label: (
29 |
30 |
31 | {t('setting.m_common')}
32 |
33 | ),
34 | children: ,
35 | },
36 | {
37 | key: 'network',
38 | label: (
39 |
40 |
41 | {t('setting.m_network')}
42 |
43 | ),
44 | children: ,
45 | },
46 | ]
47 | useEffect(() => {
48 | const title = t('window.title', { title: t('c.setting') })
49 | setTitle(title)
50 | }, [setTitle, t])
51 |
52 | useEffect(() => {
53 | setItems(_list)
54 | }, [lang])
55 |
56 | const onChange = (key: string) => {
57 | console.log(key)
58 | }
59 |
60 | return (
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | export default IndexPage
68 |
--------------------------------------------------------------------------------
/service/src/modules/chatgpt/chatgpt.controller.ts:
--------------------------------------------------------------------------------
1 | import { All, Controller, Get, Post, Sse, Req } from '@nestjs/common';
2 | import { ChatgptService } from './chatgpt.service';
3 | import type { ConfigOptions } from './chatgpt.service';
4 | import type {
5 | ChatMessage,
6 | SendMessageBrowserOptions,
7 | SendMessageOptions,
8 | } from 'chatgpt';
9 | import type { OutputOptions } from '../../utils';
10 | import type { Request } from 'express';
11 | import { Observable } from 'rxjs';
12 |
13 | @Controller('chatgpt')
14 | export class ChatgptController {
15 | constructor(private readonly chatgptService: ChatgptService) {}
16 |
17 | @All('chat')
18 | @Sse('chat')
19 | sendMessage(@Req() request: Request): Observable {
20 | const {
21 | text,
22 | options = {},
23 | config = {},
24 | } = request.body as {
25 | text: string;
26 | options?: SendMessageOptions & SendMessageBrowserOptions;
27 | config?: ConfigOptions;
28 | };
29 |
30 | console.log('request.body', request.body);
31 | console.log('request.query', request.query);
32 | // return stream
33 | const ob$ = new Observable((subscriber) => {
34 | this.chatgptService
35 | .sendMessage(
36 | text || request?.query?.text?.toString(),
37 | options,
38 | config,
39 | (chat: ChatMessage) => {
40 | subscriber.next(JSON.stringify(chat));
41 | },
42 | )
43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
44 | .then((res: OutputOptions) => {
45 | subscriber.next(JSON.stringify({ complete: true, ...res?.data }));
46 | subscriber.complete();
47 | })
48 | .catch((err) => {
49 | console.log('err', err);
50 | subscriber.next(JSON.stringify({ error: true, err: err?.message }));
51 | // subscriber.error(err);
52 | subscriber.complete();
53 | });
54 | });
55 | return ob$;
56 | }
57 |
58 | @Get('set')
59 | async setConfig(): Promise> {
60 | return await this.chatgptService.setConfig();
61 | }
62 |
63 | @Get('bill')
64 | async getBill(): Promise> {
65 | return await this.chatgptService.getBill();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/utils/request/index.ts:
--------------------------------------------------------------------------------
1 | import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'
2 | import request from './axios'
3 |
4 | export interface HttpOption {
5 | url: string
6 | data?: any
7 | method?: string
8 | headers?: any
9 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
10 | signal?: GenericAbortSignal
11 | beforeRequest?: () => void
12 | afterRequest?: () => void
13 | }
14 |
15 | export interface Response {
16 | data: T
17 | message: string | null
18 | code: number
19 | status: string
20 | }
21 |
22 | function http({ url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption) {
23 | const successHandler = (res: AxiosResponse>) => {
24 | if (res.data.status === 'Success' || typeof res.data === 'string') return res.data
25 |
26 | if (res.data.status === 'Unauthorized') {
27 | typeof window !== 'undefined' ? localStorage.removeItem('token') : ''
28 | window.location.reload()
29 | }
30 |
31 | return Promise.reject(res.data)
32 | }
33 |
34 | const failHandler = (error: Response) => {
35 | afterRequest?.()
36 | throw new Error(error?.message || 'Error')
37 | }
38 |
39 | beforeRequest?.()
40 |
41 | method = method || 'GET'
42 |
43 | const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
44 |
45 | return method === 'GET'
46 | ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler)
47 | : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler)
48 | }
49 |
50 | export function get({ url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption): Promise> {
51 | return http({
52 | url,
53 | method,
54 | data,
55 | onDownloadProgress,
56 | signal,
57 | beforeRequest,
58 | afterRequest,
59 | })
60 | }
61 |
62 | export function post({ url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption): Promise> {
63 | return http({
64 | url,
65 | method,
66 | data,
67 | headers,
68 | onDownloadProgress,
69 | signal,
70 | beforeRequest,
71 | afterRequest,
72 | })
73 | }
74 |
75 | export default post
76 |
--------------------------------------------------------------------------------
/src/contexts/setting.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useRef, useState } from 'react'
2 | import storage from '@/utils/storage'
3 |
4 | export type SettingType = {
5 | common: any
6 | setCommon: Function
7 | surface: any
8 | setSurface: Function
9 | network: any
10 | setNetwork: Function
11 | }
12 |
13 | const Context = createContext({
14 | common: {},
15 | setCommon: (common: any) => {},
16 | surface: {},
17 | setSurface: (surface: any) => {},
18 | network: {},
19 | setNetwork: (network: any) => {},
20 | })
21 |
22 | // @ts-ignore
23 | export function SettingProvider({ children }) {
24 | const [common, setCommon] = useState({})
25 | const [surface, setSurface] = useState({})
26 | const [network, setNetwork] = useState({})
27 |
28 | // useEffect(() => {
29 | // // 存储
30 | // storage.set('theme', theme)
31 | // console.log('theme', theme)
32 | // // 现在可以使用 JavaScript 来更改颜色方案
33 | // document?.documentElement?.setAttribute('data-theme', theme)
34 | // // 打印用户首选的颜色方案
35 | // console.log('window prefers-color-scheme:', window?.matchMedia('(prefers-color-scheme)'))
36 | // }, [theme])
37 |
38 | useEffect(() => {
39 | storage.get('setting_common').then((res) => {
40 | let _value = res
41 | if (!_value) {
42 | _value = {}
43 | }
44 | setCommon(_value)
45 | })
46 | storage.get('setting_surface').then((res) => {
47 | let _value = res
48 | if (!_value) {
49 | _value = {}
50 | }
51 | setSurface(_value)
52 | })
53 | storage.get('setting_network').then((res) => {
54 | let _value = res
55 | if (!_value) {
56 | _value = {}
57 | }
58 | setNetwork(_value)
59 | })
60 | }, [])
61 |
62 | useEffect(() => {
63 | // refLang.current = lang
64 | storage.set('setting_common', common)
65 | }, [common])
66 | useEffect(() => {
67 | // refLang.current = lang
68 | storage.set('setting_surface', surface)
69 | }, [surface])
70 | useEffect(() => {
71 | // refLang.current = lang
72 | storage.set('setting_network', network)
73 | }, [network])
74 |
75 | return (
76 |
86 | {children}
87 |
88 | )
89 | }
90 |
91 | export function useSettingContext() {
92 | return useContext(Context)
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/pages/chat/Setting.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Button, Cascader, Checkbox, DatePicker, Form, Input, InputNumber, Radio, Select, Switch, theme as antdTheme, TreeSelect, Upload } from 'antd'
3 | import { PlusOutlined } from '@ant-design/icons'
4 |
5 | function Setting(props: any) {
6 | const { item } = props
7 | const { theme } = useSiteContext()
8 | const { token } = antdTheme.useToken()
9 | return (
10 | <>
11 |
13 | Checkbox
14 |
15 |
16 |
17 | Apple
18 | Pear
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | >
70 | )
71 | }
72 |
73 | export default Setting
74 |
--------------------------------------------------------------------------------
/src/contexts/plugin.tsx:
--------------------------------------------------------------------------------
1 | import { Plugin } from '@/types/plugin'
2 | import { createContext, useContext, useEffect, useRef, useState } from 'react'
3 | import { useRouter } from 'next/router'
4 | import storage from '@/utils/storage'
5 |
6 | export type PluginContentType = {
7 | pluginList: Array
8 | setPluginList: (pluginList: Array) => void
9 | installPlugin: (plugin: Plugin) => void
10 | uninstallPlugin: (uuid: string) => void
11 | starPlugin: (plugin: Plugin) => void
12 | unstarPlugin: (uuid: string) => void
13 | }
14 |
15 | const Context = createContext({
16 | pluginList: [],
17 | setPluginList: () => { },
18 | installPlugin: () => { },
19 | uninstallPlugin: () => { },
20 | starPlugin: () => { },
21 | unstarPlugin: () => { },
22 | })
23 |
24 | // @ts-ignore
25 | export function PluginProvider({ children }) {
26 | const router = useRouter()
27 |
28 | const [pluginList, setPluginList] = useState>([])
29 | let refList = useRef()
30 |
31 | useEffect(() => {
32 | storage.get('pluginList').then((res) => {
33 | let _pluginList = res || []
34 | // 如果是string,则转换成数组
35 | if (typeof _pluginList == 'string') {
36 | _pluginList = JSON.parse(_pluginList)
37 | }
38 | setPluginList(_pluginList as any[])
39 | })
40 | }, [])
41 |
42 | useEffect(() => {
43 | refList.current = pluginList || []
44 | // 存储
45 | storage.set('pluginList', pluginList)
46 | }, [pluginList])
47 |
48 | const installPlugin = (plugin?: Plugin) => {
49 | }
50 | const uninstallPlugin = (uuid: string) => {
51 | }
52 |
53 | // 收藏plugin
54 | const starPlugin = (plugin?: Plugin) => {
55 | const _pluginList = [...(refList.current as Plugin[])]
56 | if (!plugin) {
57 | return
58 | }
59 | const index = _pluginList.findIndex((item) => item.uuid == plugin?.uuid)
60 | if (index > -1) {
61 | _pluginList.splice(index, 1)
62 | }
63 | _pluginList.unshift(plugin)
64 | setPluginList(_pluginList)
65 | }
66 |
67 | // 取消plugin
68 | const unstarPlugin = (uuid?: string) => {
69 | const _pluginList = [...(refList.current as Plugin[])]
70 | if (!uuid) {
71 | return
72 | }
73 | const index = _pluginList.findIndex((item) => item.uuid == uuid)
74 | if (index > -1) {
75 | _pluginList.splice(index, 1)
76 | }
77 | setPluginList(_pluginList)
78 | }
79 | return (
80 |
90 | {children}
91 |
92 | )
93 | }
94 |
95 | export function usePluginContext() {
96 | return useContext(Context)
97 | }
98 |
--------------------------------------------------------------------------------
/src/utils/storage/index.ts:
--------------------------------------------------------------------------------
1 | // data storage
2 | let storage = {
3 | // get
4 | get: async (key: string): Promise => {
5 | if (!key) {
6 | return
7 | }
8 | let val = null
9 | if (typeof window !== 'undefined') {
10 | // 获取数据类型
11 | let type = window.localStorage.getItem(key + '_typeof')
12 | switch (type) {
13 | case 'boolean':
14 | val = window.localStorage.getItem(key)
15 | val = val === 'true'
16 | break
17 | case 'number':
18 | val = window.localStorage.getItem(key)
19 | val = Number(val)
20 | break
21 | case 'string':
22 | val = window.localStorage.getItem(key)
23 | val = String(val)
24 | break
25 | case 'object':
26 | val = window.localStorage.getItem(key)
27 | if (val) {
28 | val = JSON.parse(val)
29 | }
30 | break
31 | default:
32 | val = window.localStorage.getItem(key)
33 | break
34 | }
35 | }
36 | console.log('data get ', typeof val, key, val)
37 | return val
38 | },
39 | // set
40 | set: async (key: string, value: any): Promise => {
41 | if (!key) {
42 | return false
43 | }
44 | console.log('data set ', typeof value, key, value)
45 | if (typeof window !== 'undefined') {
46 | switch (typeof value) {
47 | case 'boolean':
48 | window.localStorage.setItem(key, value ? 'true' : 'false')
49 | break
50 | case 'number':
51 | window.localStorage.setItem(key, String(value))
52 | break
53 | case 'string':
54 | window.localStorage.setItem(key, value)
55 | break
56 | case 'object':
57 | window.localStorage.setItem(key, JSON.stringify(value))
58 | break
59 | default:
60 | window.localStorage.setItem(key, String(value))
61 | break
62 | }
63 | // 记录数据类型
64 | window.localStorage.setItem(key + '_typeof', typeof value)
65 | }
66 | return true
67 | },
68 | // delete
69 | delete: async (key: string): Promise => {
70 | if (!key) {
71 | return false
72 | }
73 | if (typeof window !== 'undefined') {
74 | window.localStorage.removeItem(key)
75 | return true
76 | }
77 | return false
78 | },
79 | // has
80 | has: async (key: string): Promise => {
81 | if (!key) {
82 | return false
83 | }
84 | if (typeof window !== 'undefined') {
85 | return window.localStorage.getItem(key) !== null
86 | }
87 | return false
88 | },
89 | // clear
90 | clear: async (): Promise => {
91 | if (typeof window !== 'undefined') {
92 | window.localStorage.clear()
93 | return true
94 | }
95 | return false
96 | },
97 | }
98 | export default storage
99 |
--------------------------------------------------------------------------------
/src/components/pages/setting/Common.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Checkbox, Form, Input, InputNumber, Slider, Radio, Select, Switch, theme as antdTheme, Typography } from 'antd'
3 | import { PlusOutlined } from '@ant-design/icons'
4 | import { Chat } from '@/types/chat'
5 | import { useSettingContext } from '@/contexts'
6 | import { useTranslation } from 'react-i18next'
7 | import { useEffect, useState } from 'react'
8 | import { LanguageList, EnterKeyList } from '@/config/constant'
9 | import Box from './Box'
10 |
11 | function Setting(props: { children?: React.ReactElement; style?: React.CSSProperties }) {
12 | const { t, i18n } = useTranslation()
13 | const { common, setCommon } = useSettingContext()
14 | const { lang, setLang } = useSiteContext()
15 | const [form] = Form.useForm()
16 | const [option, setOption] = useState<{ [key: string]: string | number | boolean }>({
17 | lang: 'zh_CN',
18 | send_style: 'ctrl.enter',
19 | ...common,
20 | })
21 | const langList = [
22 | // { name: '简体中文', value: 'zh_CN' },
23 | // { name: '繁體中文', value: 'zh_TW' },
24 | // { name: 'English', value: 'en_US' },
25 | ...LanguageList,
26 | ]
27 | const enterKeyList = [
28 | // { name: 'Enter', value: 'enter' },
29 | // { name: 'Ctrl + Enter', value: 'ctrl.enter' },
30 | ...EnterKeyList,
31 | ]
32 |
33 | useEffect(() => {
34 | setOption({ ...option, ...common })
35 | form.setFieldsValue({ ...option, ...common })
36 | // eslint-disable-next-line react-hooks/exhaustive-deps
37 | }, [common])
38 |
39 | const onValuesChange = (changedValues: any, values: any) => {
40 | console.log('changedValues', changedValues)
41 | if (changedValues['lang']) {
42 | console.log('changedValues lang', changedValues['lang'])
43 | setLang(changedValues['lang'])
44 | i18n.changeLanguage(changedValues['lang'])
45 | }
46 | const _option = { ...option, ...changedValues }
47 | setOption({ ..._option })
48 | setCommon && setCommon({ ..._option })
49 | }
50 |
51 | return (
52 |
53 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | )
71 | }
72 |
73 | export default Setting
74 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import 'antd/dist/reset.css'
3 | import React from 'react'
4 | import type { AppProps, NextWebVitalsMetric } from 'next/app'
5 | import { ConfigProvider, App as AntdApp, Spin } from 'antd'
6 | import { StyleProvider } from '@ant-design/cssinjs'
7 | import { SiteProvider, ChatProvider, PromptProvider, useSiteContext, SettingProvider } from '@/contexts'
8 | // import Layout from '@/components/Layout'
9 | import dynamic from 'next/dynamic'
10 | import withTheme from '@/themes'
11 | import { GetServerSideProps } from 'next'
12 | import { Analytics } from '@vercel/analytics/react'
13 |
14 | const isProduction = process.env.NODE_ENV === 'production'
15 | const GOOGLE_GA_ID = process.env.GOOGLE_GA_ID || ''
16 |
17 | const Layout = dynamic(() => import('@/components/Layout'), {
18 | ssr: false,
19 | loading: () => (
20 |
21 |
22 |
23 | ),
24 | })
25 |
26 | ConfigProvider.config({
27 | prefixCls: 'ant', // 4.13.0+
28 | iconPrefixCls: 'anticon', // 4.17.0+
29 | })
30 |
31 | function App({ Component, pageProps: { ...pageProps } }: AppProps) {
32 | return (
33 | <>
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {/* Global Site Tag (gtag.js) - Google Analytics */}
49 | {isProduction && GOOGLE_GA_ID && (
50 | <>
51 |
52 |
64 | >
65 | )}
66 | >
67 | )
68 | }
69 |
70 | export function reportWebVitals(metric: NextWebVitalsMetric) {
71 | console.log(metric)
72 | }
73 |
74 | if (typeof window !== 'undefined' && typeof document !== 'undefined') {
75 | window.onload = () => {
76 | console.log('window.onload')
77 | document?.getElementById('holderStyle')?.remove()
78 | }
79 | }
80 |
81 | export default App
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatgpt-plus",
3 | "private": true,
4 | "version": "1.5.17",
5 | "description": "ChatGPT-Plus Application.",
6 | "author": "Zhpd ",
7 | "keywords": [
8 | "chatgpt",
9 | "chatgpt-plus",
10 | "openai",
11 | "chatbot",
12 | "gpt3",
13 | "gpt4",
14 | "nextjs"
15 | ],
16 | "license": "MIT",
17 | "engines": {
18 | "node": ">=14"
19 | },
20 | "homepage": "https://chat.gptc.cc",
21 | "bugs": "https://github.com/zhpd/chatgpt-plus/issues",
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/zhpd/chatgpt-plus"
25 | },
26 | "scripts": {
27 | "dev": "next dev",
28 | "build": "next build",
29 | "start": "next start",
30 | "lint": "next lint",
31 | "test": "jest",
32 | "tauri": "tauri",
33 | "build:tauri": "cross-env APP_ENV=tauri next build",
34 | "deploy": "npm run build && npm run release-ci",
35 | "release": "dotenv -- release-it ",
36 | "release-ci": "dotenv -- release-it --ci",
37 | "prepare": "husky install"
38 | },
39 | "dependencies": {
40 | "@emoji-mart/data": "1.1.2",
41 | "@emoji-mart/react": "1.1.1",
42 | "@reduxjs/toolkit": "1.9.5",
43 | "@vercel/analytics": "1.0.1",
44 | "ahooks": "3.7.8",
45 | "antd": "5.8.2",
46 | "axios": "1.4.0",
47 | "cross-env": "7.0.3",
48 | "dayjs": "1.11.9",
49 | "emoji-mart": "5.5.2",
50 | "github-markdown-css": "5.2.0",
51 | "i18next": "23.4.4",
52 | "i18next-browser-languagedetector": "7.1.0",
53 | "katex": "0.16.8",
54 | "markdown-it": "13.0.1",
55 | "nanoid": "4.0.2",
56 | "next": "13.4.13",
57 | "next-i18next": "14.0.0",
58 | "nodemailer": "6.9.4",
59 | "react": "18.2.0",
60 | "react-dom": "18.2.0",
61 | "react-i18next": "13.0.3",
62 | "react-icons": "4.10.1",
63 | "react-markdown": "8.0.7",
64 | "react-redux": "8.1.2",
65 | "react-syntax-highlighter": "15.5.0",
66 | "rehype-katex": "6.0.3",
67 | "rehype-raw": "6.1.1",
68 | "remark-gfm": "3.0.1",
69 | "remark-math": "5.1.1",
70 | "typescript": "5.1.6"
71 | },
72 | "devDependencies": {
73 | "@commitlint/cli": "17.7.0",
74 | "@commitlint/config-conventional": "17.7.0",
75 | "@release-it/conventional-changelog": "7.0.0",
76 | "@svgr/webpack": "8.0.1",
77 | "@tauri-apps/cli": "1.4.0",
78 | "@types/katex": "0.16.2",
79 | "@types/markdown-it": "13.0.0",
80 | "@types/node": "20.4.9",
81 | "@types/react": "18.2.19",
82 | "@types/react-dom": "18.2.7",
83 | "@types/react-syntax-highlighter": "15.5.7",
84 | "auto-changelog": "2.4.0",
85 | "dotenv-cli": "7.2.1",
86 | "eslint": "8.46.0",
87 | "eslint-config-next": "13.4.13",
88 | "husky": "8.0.3",
89 | "jest": "29.6.2",
90 | "raw-loader": "4.0.2",
91 | "release-it": "16.1.4"
92 | },
93 | "husky": {
94 | "hooks": {
95 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/assets/icons/dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/service/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatgpt-service",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Node.js client for ChatGPT-Plus service API.",
6 | "author": "Zhpd ",
7 | "keywords": [
8 | "chatgpt",
9 | "chatgpt-plus",
10 | "openai",
11 | "chatbot",
12 | "gpt",
13 | "gpt-3",
14 | "gpt3",
15 | "gpt4",
16 | "nestjs",
17 | "express"
18 | ],
19 | "license": "MIT",
20 | "engines": {
21 | "node": ">=14"
22 | },
23 | "scripts": {
24 | "dev": "nest start --watch",
25 | "build": "nest build --webpack && cd dist && npm init -y && npm i . chatgpt node-fetch quick-lru --production && cd ..",
26 | "start": "node dist/main",
27 | "start:nest": "nest start",
28 | "start:dev": "nest start --watch",
29 | "start:debug": "nest start --debug --watch",
30 | "start:prod": "node dist/main",
31 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
32 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
33 | "test": "jest",
34 | "test:watch": "jest --watch",
35 | "test:cov": "jest --coverage",
36 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
37 | "test:e2e": "jest --config ./test/jest-e2e.json"
38 | },
39 | "dependencies": {
40 | "@keyv/redis": "2.5.8",
41 | "@nestjs/common": "9.3.12",
42 | "@nestjs/config": "2.3.1",
43 | "@nestjs/core": "9.3.12",
44 | "@nestjs/platform-express": "9.3.12",
45 | "@nestjs/throttler": "4.0.0",
46 | "@nestjs/typeorm": "9.0.1",
47 | "cache-manager": "5.2.0",
48 | "chatgpt": "^5.2.4",
49 | "keyv": "4.5.2",
50 | "mysql2": "3.2.0",
51 | "node-fetch": "^3.3.1",
52 | "quick-lru": "^6.1.1",
53 | "reflect-metadata": "0.1.13",
54 | "rxjs": "7.8.0",
55 | "typeorm": "0.3.12",
56 | "uuid": "9.0.0",
57 | "webpack-node-externals": "^3.0.0"
58 | },
59 | "devDependencies": {
60 | "@nestjs/cli": "9.3.0",
61 | "@nestjs/schematics": "9.0.4",
62 | "@nestjs/testing": "9.3.12",
63 | "@types/cache-manager": "4.0.2",
64 | "@types/express": "4.17.17",
65 | "@types/jest": "29.5.0",
66 | "@types/node": "18.15.11",
67 | "@types/supertest": "2.0.12",
68 | "@typescript-eslint/eslint-plugin": "5.57.0",
69 | "@typescript-eslint/parser": "5.57.0",
70 | "@vercel/node": "2.10.3",
71 | "eslint": "8.37.0",
72 | "eslint-config-prettier": "8.8.0",
73 | "eslint-plugin-prettier": "4.2.1",
74 | "jest": "29.5.0",
75 | "prettier": "2.8.7",
76 | "source-map-support": "0.5.21",
77 | "supertest": "6.3.3",
78 | "ts-jest": "29.0.5",
79 | "ts-loader": "9.4.2",
80 | "ts-node": "10.9.1",
81 | "tsconfig-paths": "4.2.0",
82 | "typescript": "5.0.2"
83 | },
84 | "jest": {
85 | "moduleFileExtensions": [
86 | "js",
87 | "json",
88 | "ts"
89 | ],
90 | "rootDir": "src",
91 | "testRegex": ".*\\.spec\\.ts$",
92 | "transform": {
93 | ".+\\.(t|j)s$": "ts-jest"
94 | },
95 | "collectCoverageFrom": [
96 | "**/*.(t|j)s"
97 | ],
98 | "coverageDirectory": "../coverage",
99 | "testEnvironment": "node"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/hooks/useChat.ts:
--------------------------------------------------------------------------------
1 | import { fetchChatAPIProcess } from '@/api'
2 | import { Chat, Message, ConversationRequest, ConversationResponse } from '@/types/chat'
3 | import { useState } from 'react'
4 |
5 | export type MessageProps = {
6 | text?: string
7 | options?: ConversationRequest
8 | config?: { [key: string]: any }
9 | onProgress?: (event?: ProgressEvent, scene?: string, body?: any) => void
10 | signal?: AbortSignal
11 | }
12 | async function onConversation(props: MessageProps) {
13 | const { text, options, config, onProgress, signal } = props
14 | if (!text || text.trim() === '') return
15 | try {
16 | const fetchChatAPIOnce = async () => {
17 | await fetchChatAPIProcess({
18 | text,
19 | options,
20 | config,
21 | signal,
22 | onDownloadProgress: ({ event }) => {
23 | const xhr = event.target
24 | const { responseText } = xhr
25 | try {
26 | // Always process the final line
27 | // 按行读取最后一行数据,去掉头部data:字符,转换为json对象
28 | const lines = responseText.split('\n')
29 | // 读取倒数第三行数据
30 | const dataline = lines[lines.length - 3]
31 | // 解析第二行收据,去掉头部data:字符,读取json字符串
32 | let chunk = dataline?.substring(5)
33 | // console.log('onProgress chunk', chunk, lines)
34 | const data = chunk && JSON.parse(chunk)
35 | // console.log('onProgress data', data)
36 | if (data) {
37 | if (data?.complete) {
38 | onProgress?.(event, 'complete', data)
39 | } else if (data?.error) {
40 | onProgress?.(event, 'error', data)
41 | } else {
42 | onProgress?.(event, 'receive', data)
43 | }
44 | }
45 | } catch (error) {
46 | onProgress?.(event, 'error', error)
47 | }
48 | },
49 | })
50 | }
51 | await fetchChatAPIOnce()
52 | } catch (error: any) {
53 | const errorMessage = error?.message ?? 'Unknown error'
54 | console.error('onConversation error:', errorMessage)
55 | return error
56 | }
57 | }
58 |
59 | export function useChat() {
60 | const [loading, setLoading] = useState(false)
61 | const sendMessage = (props: MessageProps) => {
62 | const { onProgress, ...rest } = props
63 | setLoading(true)
64 | const _onProgress = (e?: ProgressEvent, scene?: string, body?: any) => {
65 | setLoading(true)
66 | onProgress && onProgress(e, scene, body)
67 | }
68 | return onConversation({
69 | onProgress: _onProgress,
70 | ...rest,
71 | }).catch((error) => {
72 | setLoading(false)
73 | throw error
74 | })
75 | }
76 | const stopMessage = (props: MessageProps) => {
77 | const { onProgress, ...rest } = props
78 | setLoading(true)
79 | const _onProgress = (e?: ProgressEvent, scene?: string, body?: any) => {
80 | setLoading(true)
81 | onProgress && onProgress(e, scene, body)
82 | }
83 | return onConversation({
84 | onProgress: _onProgress,
85 | ...rest,
86 | }).catch((error) => {
87 | setLoading(false)
88 | throw error
89 | })
90 | }
91 | return { sendMessage, stopMessage, onConversation, loading }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/pages/setting/Network.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Checkbox, Form, Input, InputNumber, Slider, Radio, Select, Switch, theme as antdTheme, Typography } from 'antd'
3 | import { PlusOutlined } from '@ant-design/icons'
4 | import { Chat } from '@/types/chat'
5 | import { useSettingContext } from '@/contexts'
6 | import { useTranslation } from '@/locales'
7 | import { useEffect, useState } from 'react'
8 | import request from '@/utils/request/axios'
9 | import Box from './Box'
10 | import storage from '@/utils/storage'
11 |
12 | function Setting(props: { children?: React.ReactElement; style?: React.CSSProperties }) {
13 | const { t } = useTranslation()
14 | const { network, setNetwork } = useSettingContext()
15 | const [form] = Form.useForm()
16 | const [option, setOption] = useState<{ [key: string]: string | number | boolean }>({
17 | API_TYPE: 'chatgpt-api',
18 | OPENAI_API_KEY: '',
19 | OPENAI_API_BASE_URL: '',
20 | OPENAI_ACCESS_TOKEN: '',
21 | API_REVERSE_PROXY: '',
22 | ...network,
23 | })
24 |
25 | useEffect(() => {
26 | setOption({ ...option, ...network })
27 | form.setFieldsValue({ ...option, ...network })
28 | // eslint-disable-next-line react-hooks/exhaustive-deps
29 | }, [network])
30 |
31 | const onValuesChange = (changedValues: any, values: any) => {
32 | console.log('changedValues', changedValues, values)
33 | const _option = { ...option, ...changedValues }
34 | if (values.hasOwnProperty('backend_base_url')) {
35 | const backend_base_url = values['backend_base_url']
36 | storage.set('backend_base_url', backend_base_url)
37 | }
38 | setOption({ ..._option })
39 | setNetwork && setNetwork({ ..._option })
40 | }
41 |
42 | return (
43 |
44 |
54 |
55 | {t('setting.m_network_option.apiTypeWEB')}
56 | {t('setting.m_network_option.apiTypeAPI')}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | )
77 | }
78 |
79 | export default Setting
80 |
--------------------------------------------------------------------------------
/src/components/pages/setting/Surface.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Checkbox, Form, Input, InputNumber, Slider, Radio, Select, Switch, theme as antdTheme, Typography, ColorPicker } from 'antd'
3 | import type { Color } from 'antd/es/color-picker';
4 | import { PlusOutlined } from '@ant-design/icons'
5 | import { Chat } from '@/types/chat'
6 | import { useSettingContext } from '@/contexts'
7 | import { useTranslation } from '@/locales'
8 | import { useEffect, useState } from 'react'
9 | import Box from './Box'
10 |
11 | function Setting(props: { children?: React.ReactElement; style?: React.CSSProperties }) {
12 | const { t } = useTranslation()
13 | const { surface, setSurface } = useSettingContext()
14 | const [form] = Form.useForm()
15 | const [option, setOption] = useState<{ [key: string]: string | number | boolean }>({
16 | colorPrimary: '#1677ff',
17 | radius: 4,
18 | loose: 'default',
19 | ...surface
20 | })
21 | const themeList = [
22 | { label: '', value: '#1677ff'},
23 | { label: '', value: '#00B96B'},
24 | { label: '', value: '#ff6600'},
25 | ]
26 |
27 | useEffect(() => {
28 | setOption({ ...option, ...surface })
29 | form.setFieldsValue({ ...option, ...surface })
30 | // eslint-disable-next-line react-hooks/exhaustive-deps
31 | }, [surface])
32 |
33 | const onValuesChange = (changedValues: any, values: any) => {
34 | console.log('changedValues', changedValues)
35 | if(changedValues.colorPrimary){
36 | // 判断Color类型
37 | if(typeof changedValues.colorPrimary === 'object'){
38 | changedValues.colorPrimary = changedValues.colorPrimary?.toHexString?.()
39 | }
40 | }
41 | const _option = { ...option, ...changedValues }
42 | setOption({ ..._option })
43 | setSurface && setSurface({ ..._option })
44 | }
45 |
46 | return (
47 |
48 |
58 |
59 | {themeList.map((item) => (
60 |
61 |
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {t('setting.m_surface_option.default')}
76 |
77 |
78 | {t('setting.m_surface_option.compact')}
79 |
80 |
81 |
82 |
83 |
84 | )
85 | }
86 |
87 | export default Setting
88 |
--------------------------------------------------------------------------------
/src/components/pages/prompt/Item.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Button, Card, Drawer, message, Input, InputRef, App, Popconfirm, Space, theme as antdTheme, Tooltip, Typography, Empty, Col, Row, Badge, Tag } from 'antd'
3 | import { FireFilled, StarOutlined, StarFilled } from '@ant-design/icons'
4 | import { useTranslation } from '@/locales'
5 | import { useRouter } from 'next/router'
6 | import Image from 'next/image'
7 | import { useEffect, useRef, useState } from 'react'
8 | import { Prompt } from '@/types/prompt'
9 | import { usePromptContext } from '@/contexts/prompt'
10 | import { tool } from '@/utils'
11 | import Edit from './Edit'
12 |
13 | function Item(props: { info: Prompt; openInfo?: Function }) {
14 | const { info, openInfo } = props
15 | const router = useRouter()
16 | const [item, setItem] = useState(info)
17 | const { promptList, starPrompt, unstarPrompt, addPrompt } = usePromptContext()
18 | const { theme, lang } = useSiteContext()
19 | const { t } = useTranslation()
20 | const { token } = antdTheme.useToken()
21 |
22 | useEffect(() => {
23 | setItem({
24 | ...info,
25 | })
26 | }, [info, lang])
27 |
28 | const toCopy = () => {
29 | addPrompt(item)
30 | message.success(t('prompt.copySuccess'))
31 | }
32 |
33 | const toChat = () => {
34 | // 跳转到聊天页面
35 | router.push(`/chat`)
36 | }
37 |
38 | const toInfo = () => {
39 | // 跳转到详情页面
40 | openInfo?.(item)
41 | }
42 | return (
43 | <>
44 |
48 | {item.isRecommend && }
49 | {item.isStar && }
50 | >
51 | }
52 | hoverable={true}
53 | title={
54 |
55 |
62 | {item.name}
63 |
64 | }
65 | bordered={true}
66 | style={{ width: '100%' }}
67 | >
68 |
69 | {item.intro || item.prompt || item.context?.[0]?.content || ''}
70 |
71 |
72 |
73 | {item.modelConfig?.model || ''}
74 |
75 | {/* {item.star || ''} */}
76 |
77 |
80 |
83 |
84 |
85 |
86 | >
87 | )
88 | }
89 |
90 | export default Item
91 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 0, 0, 0;
9 | --background-start-rgb: 214, 219, 220;
10 | --background-end-rgb: 255, 255, 255;
11 |
12 | --primary-glow: conic-gradient(
13 | from 180deg at 50% 50%,
14 | #16abff33 0deg,
15 | #0885ff33 55deg,
16 | #54d6ff33 120deg,
17 | #0071ff33 160deg,
18 | transparent 360deg
19 | );
20 | --secondary-glow: radial-gradient(
21 | rgba(255, 255, 255, 1),
22 | rgba(255, 255, 255, 0)
23 | );
24 |
25 | --tile-start-rgb: 239, 245, 249;
26 | --tile-end-rgb: 228, 232, 233;
27 | --tile-border: conic-gradient(
28 | #00000080,
29 | #00000040,
30 | #00000030,
31 | #00000020,
32 | #00000010,
33 | #00000010,
34 | #00000080
35 | );
36 |
37 | --callout-rgb: 238, 240, 241;
38 | --callout-border-rgb: 172, 175, 176;
39 | --card-rgb: 180, 185, 188;
40 | --card-border-rgb: 131, 134, 135;
41 | color-scheme: light dark;
42 | }
43 |
44 | @media (prefers-color-scheme: dark) {
45 | :root {
46 | --foreground-rgb: 255, 255, 255;
47 | --background-start-rgb: 0, 0, 0;
48 | --background-end-rgb: 0, 0, 0;
49 |
50 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
51 | --secondary-glow: linear-gradient(
52 | to bottom right,
53 | rgba(1, 65, 255, 0),
54 | rgba(1, 65, 255, 0),
55 | rgba(1, 65, 255, 0.3)
56 | );
57 |
58 | --tile-start-rgb: 2, 13, 46;
59 | --tile-end-rgb: 2, 5, 19;
60 | --tile-border: conic-gradient(
61 | #ffffff80,
62 | #ffffff40,
63 | #ffffff30,
64 | #ffffff20,
65 | #ffffff10,
66 | #ffffff10,
67 | #ffffff80
68 | );
69 |
70 | --callout-rgb: 20, 20, 20;
71 | --callout-border-rgb: 108, 108, 108;
72 | --card-rgb: 100, 100, 100;
73 | --card-border-rgb: 200, 200, 200;
74 | }
75 | }
76 |
77 | * {
78 | box-sizing: border-box;
79 | padding: 0;
80 | margin: 0;
81 | }
82 |
83 | html,
84 | body {
85 | max-width: 100vw;
86 | overflow-x: hidden;
87 | }
88 |
89 | body {
90 | color: rgb(var(--foreground-rgb));
91 | background: linear-gradient(
92 | to bottom,
93 | transparent,
94 | rgb(var(--background-end-rgb))
95 | )
96 | rgb(var(--background-start-rgb));
97 | }
98 |
99 | a {
100 | color: inherit;
101 | text-decoration: none;
102 | }
103 | p {
104 | margin-top: 0;
105 | /* margin-bottom: 1em; */
106 | margin-bottom: 0em;
107 | }
108 |
109 | @media (prefers-color-scheme: dark) {
110 | html {
111 | color-scheme: dark;
112 | }
113 | }
114 |
115 | /* 隐藏滚动条 */
116 | /* ::-webkit-scrollbar{ display: none} */
117 | /* 美化滚动条 */
118 | ::-webkit-scrollbar {
119 | width: 6px;
120 | height: 6px;
121 | }
122 |
123 | ::-webkit-scrollbar-track {
124 | width: 6px;
125 | background: rgba(16, 31, 28, 0.1);
126 | -webkit-border-radius: 2em;
127 | -moz-border-radius: 2em;
128 | border-radius: 2em;
129 | }
130 |
131 | ::-webkit-scrollbar-thumb {
132 | background-color: rgba(144,147,153,.5);
133 | background-clip: padding-box;
134 | min-height: 28px;
135 | -webkit-border-radius: 2em;
136 | -moz-border-radius: 2em;
137 | border-radius: 2em;
138 | transition: background-color .3s;
139 | cursor: pointer;
140 | }
141 |
142 | ::-webkit-scrollbar-thumb:hover {
143 | background-color: rgba(144,147,153,.3);
144 | }
145 |
--------------------------------------------------------------------------------
/src/pages/chat/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head'
2 | // import Message from '@/components/pages/chat/Message'
3 | import List from '@/components/pages/chat/List'
4 | import { useTranslation } from '@/locales'
5 | import { useSiteContext } from '@/contexts/site'
6 | import { useCallback, useEffect, useState } from 'react'
7 | import { useChatContext } from '@/contexts/chat'
8 | import { useRouter } from 'next/router'
9 | import { Chat } from '@/types/chat'
10 | import type { Message as MessageType } from '@/types/chat'
11 | import { uuidv4 } from '@/utils/uuid'
12 | import dayjs from 'dayjs'
13 | import { Spin } from 'antd'
14 | import dynamic from 'next/dynamic'
15 |
16 | const Message = dynamic(() => import('@/components/pages/chat/Message'), {
17 | loading: () => (
18 |
19 |
20 |
21 | ),
22 | })
23 |
24 | function IndexPage() {
25 | const router = useRouter()
26 | const { setTitle, event$ } = useSiteContext()
27 | const { t } = useTranslation()
28 | const { chatList, newChat, setActiveChat } = useChatContext()
29 | const [uuid, setUuid] = useState('')
30 | const [openList, setOpenList] = useState(true)
31 |
32 | const openChat = useCallback(
33 | (_uuid: string) => {
34 | console.log(_uuid)
35 | if (_uuid == uuid) return
36 | router.push(`/chat?uuid=${_uuid}`)
37 | },
38 | [uuid, router]
39 | )
40 |
41 | useEffect(() => {
42 | console.log('router', router)
43 | const _uuid = router.query?.uuid as string
44 | const _prompt = router.query?.prompt as string
45 | console.log(_uuid, _prompt)
46 | if (_uuid) {
47 | setUuid(_uuid)
48 | const _chat = chatList?.find?.((item) => item.uuid == _uuid)
49 | if (_chat) {
50 | console.log(_chat)
51 | setActiveChat(_chat)
52 | }
53 | } else {
54 | if (_prompt) {
55 | addChat(_prompt)
56 | } else {
57 | if (chatList && chatList?.length > 0) {
58 | openChat(chatList[0]['uuid'])
59 | }
60 | }
61 | }
62 | // eslint-disable-next-line react-hooks/exhaustive-deps
63 | }, [router.query?.uuid, router.query?._prompt])
64 |
65 | const addChat = (_prompt?: string) => {
66 | // 创建新聊天
67 | const chat: Chat = {
68 | uuid: uuidv4(),
69 | name: 'ChatGPT',
70 | lastMessageText: 'No message',
71 | }
72 | const nMessage: MessageType = {
73 | id: uuidv4(),
74 | uuid: chat?.uuid,
75 | dateTime: dayjs().format('YYYY/MM/DD HH:mm:ss'),
76 | text: _prompt || '',
77 | inversion: true,
78 | error: false,
79 | conversationOptions: {},
80 | requestOptions: {
81 | prompt: _prompt || '',
82 | options: {},
83 | },
84 | }
85 | newChat(chat, _prompt ? nMessage : undefined)
86 | router.push(`/chat?uuid=${chat.uuid}`)
87 | console.log('newChat', chat, _prompt)
88 | }
89 |
90 | event$.useSubscription((val: any) => {
91 | if (val?.type == 'tabSwich') {
92 | // 二次点击,则隐藏消息列表
93 | if (val?.url.indexOf('/chat') > -1) {
94 | console.log(val)
95 | setOpenList(!openList)
96 | }
97 | }
98 | })
99 | useEffect(() => {
100 | const title = t('window.title', { title: t('c.message') })
101 | setTitle(title)
102 | }, [setTitle, t])
103 | return (
104 | <>
105 |
106 |
107 |
108 |
109 | >
110 | )
111 | }
112 |
113 | export default IndexPage
114 |
--------------------------------------------------------------------------------
/src/contexts/site.tsx:
--------------------------------------------------------------------------------
1 | import { useEventEmitter } from 'ahooks'
2 | import { EventEmitter } from 'ahooks/lib/useEventEmitter'
3 | import { createContext, useContext, useEffect, useRef, useState } from 'react'
4 | import { useTranslation } from 'react-i18next'
5 | import { changeLanguage } from '@/locales'
6 | import storage from '@/utils/storage'
7 |
8 | export type SiteType = {
9 | theme: 'dark' | 'light' | 'auto'
10 | setTheme: Function
11 | lang: string
12 | setLang: Function
13 | title: string
14 | setTitle: Function
15 | event$: EventEmitter
16 | }
17 |
18 | const Context = createContext({
19 | theme: 'light',
20 | setTheme: (theme: string) => {},
21 | lang: 'zh_CN',
22 | setLang: (lang: string) => {},
23 | title: 'ChatGPT-Plus',
24 | setTitle: (title: string) => {},
25 | event$: new EventEmitter(), // 全局event事件
26 | })
27 |
28 | // @ts-ignore
29 | export function SiteProvider({ children }) {
30 | const [theme, setTheme] = useState<'dark' | 'light' | 'auto'>('light')
31 | const [lang, setLang] = useState('zh_CN')
32 | const [title, setTitle] = useState('ChatGPT-Plus')
33 | const event$ = useEventEmitter()
34 | // const refTheme = useRef<'dark' | 'light' | 'auto' >('light')
35 | // const refLang = useRef('zh_CN')
36 |
37 | useEffect(() => {
38 | storage.get('theme').then((res) => {
39 | let _theme = res
40 | if (!_theme) {
41 | // 从浏览器获取
42 | _theme = window?.matchMedia && window?.matchMedia('(prefers-color-scheme)').matches ? (window?.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : 'light'
43 | }
44 | if (_theme !== 'dark' && _theme !== 'light' && _theme !== 'auto') {
45 | _theme = 'light'
46 | }
47 | setTheme(_theme)
48 | })
49 | storage.get('lang').then((res) => {
50 | let _lang = res
51 | if (!_lang) {
52 | // 从浏览器获取
53 | _lang = navigator?.language
54 | // 转换成xx_XX格式
55 | _lang = _lang.replace('-', '_')
56 | }
57 | if (_lang !== 'zh_CN' && _lang !== 'en_US' && _lang !== 'zh_TW') {
58 | _lang = 'zh_CN'
59 | }
60 | setLang(_lang)
61 | // 切换语言
62 | changeLanguage(_lang)
63 | })
64 | }, [])
65 |
66 | useEffect(() => {
67 | // refTheme.current = theme
68 | // 存储
69 | storage.set('theme', theme)
70 | console.log('theme', theme)
71 | // 现在可以使用 JavaScript 来更改颜色方案
72 | document?.documentElement?.setAttribute('data-theme', theme)
73 | // 打印用户首选的颜色方案
74 | console.log('window prefers-color-scheme:', window?.matchMedia('(prefers-color-scheme)'))
75 |
76 | // if (window?.matchMedia && window?.matchMedia('(prefers-color-scheme)').matches) {
77 | // // 如果浏览器支持媒体查询且用户设置了颜色方案,则执行以下代码:
78 | // if (window?.matchMedia('(prefers-color-scheme: light)').matches) {
79 | // // 如果用户首选浅色主题,则执行以下代码:
80 | // document.documentElement.setAttribute('data-theme', 'light')
81 | // } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
82 | // // 如果用户首选深色主题,则执行以下代码:
83 | // document.documentElement.setAttribute('data-theme', 'dark')
84 | // }
85 | // }
86 | }, [theme])
87 | useEffect(() => {
88 | // refLang.current = lang
89 | console.log('site lang', lang)
90 | // 存储
91 | storage.set('lang', lang)
92 | }, [lang])
93 |
94 | return (
95 |
106 | {children}
107 |
108 | )
109 | }
110 |
111 | export function useSiteContext() {
112 | return useContext(Context)
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/pages/chat/Empty.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Col, Row, theme as antdTheme, Typography } from 'antd'
3 | import { BulbOutlined, WarningOutlined, ThunderboltOutlined } from '@ant-design/icons'
4 | import { useTranslation } from '@/locales'
5 | import { ReactNode } from 'react'
6 | import React from 'react'
7 |
8 | const styleTop: React.CSSProperties = {
9 | display: 'flex',
10 | justifyContent: 'center',
11 | flexDirection: 'column',
12 | alignItems: 'center',
13 | margin: '10px',
14 | }
15 | const styleMsg: React.CSSProperties = {
16 | display: 'flex',
17 | justifyContent: 'center',
18 | flexDirection: 'column',
19 | alignItems: 'center',
20 | textAlign: 'center',
21 | borderRadius: '4px',
22 | margin: '10px',
23 | // background: '#0092ff',
24 | padding: '8px 12px',
25 | }
26 |
27 | function Empty(props: any) {
28 | const { t } = useTranslation()
29 | const { theme } = useSiteContext()
30 | const { token } = antdTheme.useToken()
31 |
32 | const data: { text: string; type?: string; icon?: ReactNode }[][] = [
33 | [
34 | { text: 'Examples', type: 'top', icon: },
35 | { text: '"Explain quantum computingin simple terms" ' },
36 | { text: `"Got any creative ideas for a10 vear old's birthday?"` },
37 | { text: '"How do I make an HTTPrequest in Javascript?"' },
38 | ],
39 | [
40 | { text: 'Capabilities', type: 'top', icon: },
41 | { text: 'Remembers what user saidearlier in the conversation' },
42 | { text: 'Allows user to provide follow-up corrections' },
43 | { text: 'Trained to declineinappropriate requests' },
44 | ],
45 | [
46 | { text: 'Limitations', type: 'top', icon: },
47 | { text: 'May occasionally generateincorrect information' },
48 | { text: 'May occasionally produceharmful instructions or biasedcontent' },
49 | { text: 'Limited knowledge of worldand events after 2021' },
50 | ],
51 | ]
52 |
53 | return (
54 |
55 |
56 | {t('title')}
57 |
58 |
59 | {data.map((row, i) => (
60 |
61 | {row.map((tt, j) => (
62 |
63 | {tt.type == 'top' ? (
64 | // 图标+文字
65 |
66 | {tt.icon}
67 | {tt.text}
68 |
69 | ) : (
70 |
78 | {tt.text}
79 |
80 | )}
81 |
82 | ))}
83 |
84 | ))}
85 |
86 |
87 | )
88 | }
89 |
90 | export default Empty
91 |
--------------------------------------------------------------------------------
/src/config/constant.ts:
--------------------------------------------------------------------------------
1 | // 常量枚举
2 |
3 | // 空图片枚举
4 | export enum EmptyImage {
5 | EMPTY_BOX = '@/assets/images/empty_box.png',
6 | }
7 |
8 | // 主题枚举
9 | export enum Theme {
10 | LIGHT = 'light',
11 | DARK = 'dark',
12 | }
13 |
14 | // 模型枚举
15 | export enum Model {
16 | 'GPT-3.5-Turbo' = 'gpt-3.5-turbo',
17 | 'GPT-3.5-Turbo-0301' = 'gpt-3.5-turbo-0301',
18 | 'GPT-3.5-Turbo-0613' = 'gpt-3.5-turbo-0613',
19 | 'GPT-3.5-Turbo-16k' = 'gpt-3.5-turbo-16k',
20 | 'GPT-4' = 'gpt-4',
21 | 'GPT-4-0314' = 'gpt-4-0314',
22 | 'GPT-4-0613' = 'gpt-4-0613',
23 | 'GPT-4-32k' = 'gpt-4-32k',
24 | 'GPT-4-32k-0314' = 'gpt-4-32k-0314',
25 | 'GPT-4-32k-0613' = 'gpt-4-32k-0613',
26 | 'GPT-4-Mobile' = 'gpt-4-mobile',
27 | }
28 |
29 | // 模型列表
30 | export const ModelList = [
31 | {
32 | label: 'GPT-3.5-Turbo',
33 | value: Model['GPT-3.5-Turbo'],
34 | },
35 | {
36 | label: 'GPT-3.5-Turbo-16k',
37 | value: Model['GPT-3.5-Turbo-16k'],
38 | },
39 | {
40 | label: 'GPT-3.5-Turbo-0301',
41 | value: Model['GPT-3.5-Turbo-0301'],
42 | },
43 | {
44 | label: 'GPT-3.5-Turbo-0613',
45 | value: Model['GPT-3.5-Turbo-0613'],
46 | },
47 | {
48 | label: 'GPT-4',
49 | value: Model['GPT-4'],
50 | },
51 | {
52 | label: 'GPT-4-0314',
53 | value: Model['GPT-4-0314'],
54 | },
55 | {
56 | label: 'GPT-4-0613',
57 | value: Model['GPT-4-0613'],
58 | },
59 | {
60 | label: 'GPT-4-32k',
61 | value: Model['GPT-4-32k'],
62 | },
63 | {
64 | label: 'GPT-4-32k-0314',
65 | value: Model['GPT-4-32k-0314'],
66 | },
67 | {
68 | label: 'GPT-4-32k-0613',
69 | value: Model['GPT-4-32k-0613'],
70 | },
71 | {
72 | label: 'GPT-4-Mobile',
73 | value: Model['GPT-4-Mobile'],
74 | },
75 | ]
76 |
77 | // 回车键枚举
78 | export enum EnterKey {
79 | ENTER = 'enter',
80 | CTRL_ENTER = 'ctrl.enter',
81 | SHIFT_ENTER = 'shift.enter',
82 | ALT_ENTER = 'alt.enter',
83 | }
84 |
85 | // 回车键列表
86 | export const EnterKeyList = [
87 | {
88 | label: 'ENTER',
89 | value: EnterKey.ENTER,
90 | },
91 | {
92 | label: 'CTRL + ENTER',
93 | value: EnterKey.CTRL_ENTER,
94 | },
95 | {
96 | label: 'SHIFT + ENTER',
97 | value: EnterKey.SHIFT_ENTER,
98 | },
99 | {
100 | label: 'ALT + ENTER',
101 | value: EnterKey.ALT_ENTER,
102 | },
103 | ]
104 |
105 | // cn: "简体中文",
106 | // en: "English",
107 | // tw: "繁體中文",
108 | // fr: "Français",
109 | // es: "Español",
110 | // it: "Italiano",
111 | // tr: "Türkçe",
112 | // jp: "日本語",
113 | // de: "Deutsch",
114 | // vi: "Tiếng Việt",
115 | // ru: "Русский",
116 | // cs: "Čeština",
117 | // ko: "한국어",
118 | // 语言枚举
119 | export enum Language {
120 | ZH_CN = 'zh_CN',
121 | EN_US = 'en_US',
122 | ZH_TW = 'zh_TW',
123 | FR_FR = 'fr_FR',
124 | ES_ES = 'es_ES',
125 | IT_IT = 'it_IT',
126 | TR_TR = 'tr_TR',
127 | JA_JP = 'ja_JP',
128 | DE_DE = 'de_DE',
129 | VI_VN = 'vi_VN',
130 | RU_RU = 'ru_RU',
131 | CS_CZ = 'cs_CZ',
132 | KO_KR = 'ko_KR',
133 | }
134 |
135 | // 语言列表
136 | export const LanguageList = [
137 | {
138 | label: '简体中文',
139 | value: Language.ZH_CN,
140 | },
141 | {
142 | label: '繁体中文',
143 | value: Language.ZH_TW,
144 | },
145 | {
146 | label: 'English',
147 | value: Language.EN_US,
148 | },
149 | {
150 | label: 'Français',
151 | value: Language.FR_FR,
152 | },
153 | {
154 | label: 'Español',
155 | value: Language.ES_ES,
156 | },
157 | {
158 | label: 'Italiano',
159 | value: Language.IT_IT,
160 | },
161 | {
162 | label: 'Türkçe',
163 | value: Language.TR_TR,
164 | },
165 | {
166 | label: '日本語',
167 | value: Language.JA_JP,
168 | },
169 | {
170 | label: 'Deutsch',
171 | value: Language.DE_DE,
172 | },
173 | {
174 | label: 'Tiếng Việt',
175 | value: Language.VI_VN,
176 | },
177 | {
178 | label: 'Русский',
179 | value: Language.RU_RU,
180 | },
181 | {
182 | label: 'Čeština',
183 | value: Language.CS_CZ,
184 | },
185 | {
186 | label: '한국어',
187 | value: Language.KO_KR,
188 | },
189 | ]
190 |
--------------------------------------------------------------------------------
/src/contexts/prompt.tsx:
--------------------------------------------------------------------------------
1 | import { Prompt } from '@/types/prompt'
2 | import { createContext, useContext, useEffect, useRef, useState } from 'react'
3 | import { useRouter } from 'next/router'
4 | import storage from '@/utils/storage'
5 | import { uuidv4 } from '@/utils'
6 |
7 | export type PromptContentType = {
8 | promptList: Array
9 | setPromptList: (promptList: Array) => void
10 | addPrompt: (prompt: Prompt) => void
11 | upPrompt: (uuid: string, obj: { [key: string]: string }) => void
12 | delPrompt: (uuid: string) => void
13 | starPrompt: (prompt: Prompt) => void
14 | unstarPrompt: (uuid: string) => void
15 | }
16 |
17 | const Context = createContext({
18 | promptList: [],
19 | setPromptList: () => {},
20 | addPrompt: () => {},
21 | delPrompt: () => {},
22 | upPrompt: () => {},
23 | starPrompt: () => {},
24 | unstarPrompt: () => {},
25 | })
26 |
27 | // @ts-ignore
28 | export function PromptProvider({ children }) {
29 | const router = useRouter()
30 |
31 | const [promptList, setPromptList] = useState>([])
32 | let refList = useRef()
33 |
34 | useEffect(() => {
35 | storage.get('promptList').then((res) => {
36 | let _promptList = res || []
37 | // 如果是string,则转换成数组
38 | if (typeof _promptList == 'string') {
39 | _promptList = JSON.parse(_promptList)
40 | }
41 | setPromptList(_promptList as any[])
42 | })
43 | }, [])
44 |
45 | useEffect(() => {
46 | refList.current = promptList || []
47 | // 存储
48 | storage.set('promptList', promptList)
49 | }, [promptList])
50 |
51 | const addPrompt = (prompt?: Prompt) => {
52 | const _promptList = [...(refList.current as Prompt[])]
53 | if (!prompt) {
54 | return
55 | }
56 | prompt.uuid = uuidv4()
57 | console.log('addPrompt', prompt)
58 | if (!prompt?.name || !prompt?.prompt) {
59 | return
60 | }
61 | const index = _promptList.findIndex((item) => item.uuid == prompt?.uuid)
62 | if (index > -1) {
63 | _promptList.splice(index, 1)
64 | }
65 | _promptList.unshift(prompt)
66 | setPromptList(_promptList)
67 | }
68 | const delPrompt = (uuid: string) => {
69 | const _promptList = [...(refList.current as Prompt[])]
70 | const _list = _promptList.filter((item) => item.uuid !== uuid)
71 | setPromptList(_list)
72 | }
73 |
74 | const upPrompt = (uuid: string, obj: { [key: string]: string }) => {
75 | const _promptList = [...(refList.current as Prompt[])]
76 | const index = _promptList.findIndex((item) => item.uuid == uuid)
77 | if (index > -1) {
78 | const _prompt = _promptList[index]
79 | const _nPrompt = Object.assign({}, { ..._prompt }, { ...obj })
80 | console.log('upPrompt', uuid, obj, _nPrompt)
81 | if (!_nPrompt?.name || !_nPrompt?.prompt) {
82 | return
83 | }
84 | _promptList[index] = _nPrompt
85 | setPromptList(_promptList)
86 | }
87 | }
88 |
89 | // 收藏prompt
90 | const starPrompt = (prompt?: Prompt) => {
91 | const _promptList = [...(refList.current as Prompt[])]
92 | if (!prompt) {
93 | return
94 | }
95 | const index = _promptList.findIndex((item) => item.uuid == prompt?.uuid)
96 | if (index > -1) {
97 | _promptList.splice(index, 1)
98 | }
99 | _promptList.unshift(prompt)
100 | setPromptList(_promptList)
101 | }
102 |
103 | // 取消prompt
104 | const unstarPrompt = (uuid?: string) => {
105 | const _promptList = [...(refList.current as Prompt[])]
106 | if (!prompt) {
107 | return
108 | }
109 | const index = _promptList.findIndex((item) => item.uuid == uuid)
110 | if (index > -1) {
111 | _promptList.splice(index, 1)
112 | }
113 | setPromptList(_promptList)
114 | }
115 | return (
116 |
127 | {children}
128 |
129 | )
130 | }
131 |
132 | export function usePromptContext() {
133 | return useContext(Context)
134 | }
135 |
--------------------------------------------------------------------------------
/src/assets/icons/light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/pages/plugin/Info.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Button, theme as antdTheme, Typography, Descriptions } from 'antd'
3 | import { DeleteOutlined, MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'
4 | import { usePluginContext } from '@/contexts'
5 | import { Plugin } from '@/types/plugin'
6 | import { useTranslation } from '@/locales'
7 | import { uuidv4 } from '@/utils/uuid'
8 | import Image from 'next/image'
9 | import { useEffect, useState } from 'react'
10 | import { Model, ModelList, LanguageList } from '@/config/constant'
11 | import { useRouter } from 'next/router'
12 |
13 | function Info(props: { plugin?: Plugin }) {
14 | const { plugin } = props
15 | const { theme, lang } = useSiteContext()
16 | const { t } = useTranslation()
17 | const { token } = antdTheme.useToken()
18 | const [item, setItem] = useState(plugin as Plugin)
19 |
20 | useEffect(() => {
21 | // 获取语言,语言-换成_,如果没有则使用默认语言
22 | const _lang = lang.replace('-', '_')
23 | const langInfo = plugin?.lang?.[_lang] || {}
24 | setItem({
25 | ...(plugin as Plugin),
26 | ...langInfo,
27 | })
28 | }, [plugin, lang])
29 |
30 | return (
31 |
32 |
35 |
42 | {item.name}
43 |
44 | }
45 | column={1}
46 | size="small"
47 | >
48 | {(item?.basicPrompts?.length as number) > 0 && (item?.advancedPrompts?.length as number) > 0 && (
49 |
50 |
51 | {item?.basicPrompts?.map((item) => {
52 | return (
53 |
54 | {item}
55 |
56 | )
57 | })}
58 |
59 |
60 | {item?.advancedPrompts?.map((item) => {
61 | return (
62 |
63 | {item}
64 |
65 | )
66 | })}
67 |
68 |
69 | )}
70 |
71 | {item?.name}
72 | {item?.intro}
73 | {item?.description}
74 | {item?.namespace}
75 | {item?.mail}
76 |
77 |
80 |
81 |
82 |
85 |
86 | {plugin?.intro}
87 | {plugin?.description}
88 |
89 |
90 | )
91 | }
92 |
93 | export default Info
94 |
--------------------------------------------------------------------------------
/src/components/pages/chat/Box.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Button, Popconfirm, Space, theme as antdTheme } from 'antd'
3 | import { DeleteOutlined, CopyOutlined, RedoOutlined, CheckOutlined } from '@ant-design/icons'
4 |
5 | import Image from 'next/image'
6 | import Markdown from './Markdown'
7 | import type { Message } from '@/types/chat'
8 | import { useState } from 'react'
9 | import { useChatContext } from '@/contexts/chat'
10 |
11 | import HeadImg from '@/assets/openai.png'
12 |
13 | export type BoxProps = {
14 | uuid: string
15 | item: Message
16 | place: 'left' | 'right'
17 | resendMessage?: (item: Message) => void
18 | }
19 |
20 | function Box(props: BoxProps) {
21 | const { item, uuid, place, resendMessage } = props
22 | const [copyOk, setCopyOk] = useState(false)
23 | const { theme } = useSiteContext()
24 | const { token } = antdTheme.useToken()
25 | const { delMessage } = useChatContext()
26 |
27 | const copyText = (text: string) => {
28 | navigator.clipboard.writeText(text)
29 | setCopyOk(true)
30 | setTimeout(() => {
31 | setCopyOk(false)
32 | }, 1200)
33 | }
34 |
35 | return (
36 |
37 |
40 |
41 |
{item?.dateTime}
42 |
43 |
52 | {item?.text}
53 |
54 |
55 |
:
}
60 | onClick={() => {
61 | copyText(item?.text)
62 | }}
63 | >
64 |
) => {
69 | resendMessage?.(item)
70 | return
71 | }}
72 | onCancel={(e?: React.MouseEvent) => {}}
73 | okText="Yes"
74 | cancelText="No"
75 | >
76 | }>
77 |
78 |
) => {
83 | delMessage(uuid, item)
84 | return
85 | }}
86 | onCancel={(e?: React.MouseEvent) => {}}
87 | okText="Yes"
88 | cancelText="No"
89 | >
90 | }>
91 |
92 |
93 |
94 |
95 |
96 | )
97 | }
98 |
99 | export default Box
100 |
--------------------------------------------------------------------------------
/src/components/pages/chat/InputArea.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Button, Mentions, Popconfirm, Space, Tooltip, Typography, theme as antdTheme } from 'antd'
3 | import { DisconnectOutlined, LinkOutlined, SendOutlined } from '@ant-design/icons'
4 | import type { Message } from '@/types/chat'
5 | import type { MentionsOptionProps } from 'antd/es/mentions'
6 | import { useEffect, useState } from 'react'
7 | import { useKeyPress } from 'ahooks'
8 | import { t } from 'i18next'
9 | import { usePromptContext, useSettingContext } from '@/contexts'
10 |
11 | export type BoxProps = {
12 | coiled: boolean
13 | setCoiled: (val: boolean) => void
14 | sendMessageText: (text: string) => void
15 | }
16 |
17 | function InputArea(props: BoxProps) {
18 | const { coiled, setCoiled, sendMessageText } = props
19 | const { promptList } = usePromptContext()
20 | const { common: commonConfig } = useSettingContext()
21 | const [plist, setPlist] = useState<{ label: any; value: string }[]>([])
22 | const [input, setInput] = useState('')
23 | const [canSend, setCanSend] = useState(false)
24 | const { theme } = useSiteContext()
25 | const { token } = antdTheme.useToken()
26 |
27 | useEffect(() => {
28 | if (promptList) {
29 | const list =
30 | promptList?.map?.((item) => {
31 | return {
32 | key: item.uuid as string,
33 | label: (
34 |
35 |
{item.name as string}
36 |
37 | ),
38 | value: item.prompt as string,
39 | }
40 | }) || []
41 | setPlist(list)
42 | }
43 | }, [promptList])
44 |
45 | // input change
46 | const inputChange = (val: string) => {
47 | setInput(val)
48 | if (val) {
49 | setCanSend(true)
50 | } else {
51 | setCanSend(false)
52 | }
53 | }
54 |
55 | const onSend = () => {
56 | setCanSend(false)
57 | setInput('')
58 | sendMessageText(input)
59 | }
60 |
61 | useKeyPress(
62 | [commonConfig?.send_style || 'ctrl.enter'],
63 | (event) => {
64 | console.log('event', event)
65 | onSend()
66 | setTimeout(() => {
67 | setInput('')
68 | }, 200)
69 | },
70 | {
71 | exactMatch: true,
72 | // events: ['keydown', 'keyup'],
73 | }
74 | )
75 |
76 | return (
77 |
84 |
85 | : }
90 | onClick={() => setCoiled(!coiled)}
91 | >
92 |
93 |
104 | {/* {
114 | setInput(e.target.value)
115 | if (e.target.value) {
116 | setCanSend(true)
117 | } else {
118 | setCanSend(false)
119 | }
120 | }}
121 | onPressEnter={(e) => {
122 | sendMessageText(input)
123 | }}
124 | > */}
125 | } disabled={canSend ? false : true} style={{ marginLeft: 10, marginRight: 10 }} onClick={onSend}>
126 | {t('chat.send')}
127 |
128 |
129 | )
130 | }
131 |
132 | export default InputArea
133 |
--------------------------------------------------------------------------------
/src/components/pages/plugin/Item.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Button, Card, Drawer, FloatButton, Input, InputRef, App, Popconfirm, Space, theme as antdTheme, Tooltip, Typography, Empty, Col, Row, Badge, message } from 'antd'
3 | import { StarOutlined, StarFilled } from '@ant-design/icons'
4 | import { useTranslation } from '@/locales'
5 | import { useRouter } from 'next/router'
6 | import Image from 'next/image'
7 | import { useEffect, useRef, useState } from 'react'
8 | import { Plugin } from '@/types/plugin'
9 | import { usePluginContext } from '@/contexts/plugin'
10 |
11 | function Item(props: { info: Plugin; openInfo?: Function }) {
12 | const { info, openInfo } = props
13 | const [item, setItem] = useState(info)
14 | const { pluginList, starPlugin, unstarPlugin, installPlugin, uninstallPlugin } = usePluginContext()
15 | const { theme, lang } = useSiteContext()
16 | const { t } = useTranslation()
17 | const { token } = antdTheme.useToken()
18 |
19 | useEffect(() => {
20 | // 获取语言,语言-换成_,如果没有则使用默认语言
21 | const _lang = lang.replace('-', '_')
22 | const langInfo = info?.lang?.[_lang] || {}
23 | setItem({
24 | ...info,
25 | ...langInfo,
26 | })
27 | }, [info, lang])
28 |
29 | const toInstall = (item: Plugin) => {
30 | if (item.isInstall) {
31 | uninstallPlugin(item?.uuid)
32 | setItem({ ...item, isInstall: false })
33 | message.success(t('plugin.tag.uninstallSuccess'))
34 | } else {
35 | installPlugin(item)
36 | setItem({ ...item, isInstall: true })
37 | message.success(t('plugin.tag.installSuccess'))
38 | }
39 | }
40 |
41 | const toInfo = () => {
42 | // 跳转到详情页面
43 | openInfo?.(item)
44 | }
45 |
46 | return (
47 | <>
48 | unstarPlugin(item.uuid)} />
53 | // ) : (
54 | // starPlugin(item)} />
55 | // )
56 | // }
57 | hoverable={true}
58 | title={null}
59 | bordered={true}
60 | style={{ width: '100%' }}
61 | >
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {item.name}
71 |
72 |
73 | {item?.isRecommend && }
74 | {item?.isInstall && }
75 | {item?.isStar && }
76 | {item?.isNew && }
77 |
78 |
79 |
80 |
81 |
84 |
85 |
86 | {/*
{item.datetime} */}
87 |
88 |
89 |
90 |
91 | {item.intro}
92 |
93 |
94 |
95 |
96 | >
97 | )
98 | }
99 |
100 | export default Item
101 |
--------------------------------------------------------------------------------
/src/locales/zh_TW.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang_name": "繁體中文",
3 | "title": "ChatGPT-Plus",
4 | "window": {
5 | "title": "ChatGPT-Plus {{title}}"
6 | },
7 | "c": {
8 | "message": "訊息",
9 | "prompt": "提示詞",
10 | "plugin": "外掛",
11 | "share": "分享",
12 | "store": "商店",
13 | "readme": "說明",
14 | "setting": "設定",
15 | "github": "Github",
16 | "open": "開啟",
17 | "close": "關閉",
18 | "delete": "刪除",
19 | "deleteSuccess": "刪除成功",
20 | "save": "儲存",
21 | "saveSuccess": "儲存成功",
22 | "reset": "重設",
23 | "action": "操作",
24 | "export": "匯出",
25 | "exportSuccess": "匯出成功",
26 | "import": "匯入",
27 | "importSuccess": "匯入成功",
28 | "clear": "清空",
29 | "clearSuccess": "清空成功",
30 | "yes": "是",
31 | "no": "否",
32 | "confirm": "確定",
33 | "download": "下載",
34 | "success": "操作成功",
35 | "failed": "操作失敗",
36 | "devBuilding": "正在開發中"
37 | },
38 | "chat": {
39 | "newChat": "新訊息",
40 | "send": "發送",
41 | "setting": "設定",
42 | "api_warning": "外掛系統正在開發中,請勿使用外掛功能。",
43 | "inputPlaceholder": "來說點什麼吧.... 輸入/ 以查看提示詞",
44 | "coiledText": "連續對話 {{status}}",
45 | "tryGpt4": "嘗試 GPT-4",
46 | "optionTitle": "選項",
47 | "option": {
48 | "apiType": "存取方式",
49 | "apiTypeAPI": "API",
50 | "apiTypeWEB": "網頁",
51 | "model": "模型",
52 | "modelTip": "選擇模型",
53 | "max_tokens": "單次回覆限制",
54 | "max_tokensTip": "單次互動所用的最大 Token 數",
55 | "top_p": "回覆篩選",
56 | "top_pTip": "top_p 值越大,回覆越有可能是高品質的",
57 | "temperature": "回覆隨機性",
58 | "temperatureTip": "temperature 值越大,回覆越有可能是隨機的",
59 | "presence_penalty": "話題重複度",
60 | "presence_penaltyTip": "presence_penalty 值越大,越有可能談論新內容話題",
61 | "frequency_penalty": "詞彙重複度",
62 | "frequency_penaltyTip": "frequency_penalty 值越大,越有可能避免重複內容",
63 | "stop": "結束字元",
64 | "stopTip": "回覆中出現該字元時,停止回覆"
65 | }
66 | },
67 | "prompt": {
68 | "newPrompt": "建立提示詞",
69 | "editPrompt": "編輯提示詞",
70 | "save": "儲存",
71 | "contextEmpty": "上下文提示詞為空",
72 | "onlinePrompt": "線上提示詞",
73 | "promptStoreTitle": "推薦提示詞",
74 | "importExport": "匯入匯出",
75 | "search": "搜尋",
76 | "searchPlaceholder": "請輸入提示詞",
77 | "languagePlaceholder": "請選擇語言",
78 | "tag": {
79 | "all": "全部",
80 | "recommend": "推薦",
81 | "star": "收藏",
82 | "delete": "刪除",
83 | "deleteSuccess": "刪除成功",
84 | "deleteFailed": "刪除失敗",
85 | "starSuccess": "收藏成功",
86 | "starFailed": "收藏失敗",
87 | "unstarSuccess": "取消收藏成功",
88 | "unstarFailed": "取消收藏失敗"
89 | }
90 | },
91 | "plugin": {
92 | "newPlugin": "建立外掛",
93 | "editPlugin": "編輯外掛",
94 | "save": "儲存",
95 | "onlinePlugin": "線上外掛",
96 | "pluginStoreTitle": "推薦外掛",
97 | "importExport": "匯入匯出",
98 | "search": "搜尋",
99 | "searchPlaceholder": "請輸入外掛",
100 | "tag": {
101 | "all": "全部",
102 | "new": "新增",
103 | "recommend": "推薦",
104 | "star": "收藏",
105 | "install": "安裝",
106 | "uninstall": "移除",
107 | "delete": "刪除",
108 | "deleteSuccess": "刪除成功",
109 | "deleteFailed": "刪除失敗",
110 | "starSuccess": "收藏成功",
111 | "starFailed": "收藏失敗",
112 | "unstarSuccess": "取消收藏成功",
113 | "unstarFailed": "取消收藏失敗",
114 | "installSuccess": "安裝成功",
115 | "installFailed": "安裝失敗",
116 | "uninstallSuccess": "移除成功",
117 | "uninstallFailed": "移除失敗"
118 | }
119 | },
120 | "setting": {
121 | "m_surface": "外觀設定",
122 | "m_surface_option": {
123 | "theme": "主題",
124 | "colorPrimary": "主色",
125 | "radius": "圓角",
126 | "loose": "寬鬆",
127 | "compact": "緊湊",
128 | "default": "預設"
129 | },
130 | "m_common": "一般設定",
131 | "m_common_option": {
132 | "lang": "語言",
133 | "send_message_style": "發送訊息方式"
134 | },
135 | "m_network": "網路設定",
136 | "m_network_option": {
137 | "DEFAULT_API_TYPE": "預設存取模式",
138 | "apiTypeAPI": "API",
139 | "apiTypeWEB": "網頁",
140 | "BACKEND_URL": "後端網址",
141 | "OPENAI_API_KEY": "OpenAI API 金鑰",
142 | "OPENAI_API_BASE_URL": "OpenAI API 網址",
143 | "OPENAI_ACCESS_TOKEN": "OpenAI Access Token",
144 | "API_REVERSE_PROXY": "API 反向代理"
145 | },
146 | "m_feedback": "意見回饋",
147 | "m_feedback_option": {},
148 | "m_about": "關於應用",
149 | "m_about_option": {
150 | "now_version": "目前版本",
151 | "check_update": "檢查更新",
152 | "update_log": "更新日誌"
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/locales/zh_CN.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang_name": "简体中文",
3 | "title": "ChatGPT-Plus",
4 | "window": {
5 | "title": "ChatGPT-Plus {{title}}"
6 | },
7 | "c": {
8 | "message": "消息",
9 | "prompt": "提示",
10 | "plugin": "插件",
11 | "share": "分享",
12 | "store": "商店",
13 | "readme": "介绍",
14 | "setting": "设置",
15 | "github": "Github",
16 | "open": "开启",
17 | "close": "关闭",
18 | "delete": "删除",
19 | "deleteSuccess": "删除成功",
20 | "save": "保存",
21 | "saveSuccess": "保存成功",
22 | "reset": "重置",
23 | "action": "操作",
24 | "export": "导出",
25 | "exportSuccess": "导出成功",
26 | "import": "导入",
27 | "importSuccess": "导入成功",
28 | "clear": "清空",
29 | "clearSuccess": "清空成功",
30 | "yes": "是",
31 | "no": "否",
32 | "confirm": "确定",
33 | "download": "下载",
34 | "success": "操作成功",
35 | "failed": "操作失败",
36 | "devBuilding": "正在开发中"
37 | },
38 | "chat": {
39 | "newChat": "新消息",
40 | "send": "发送",
41 | "setting": "设置",
42 | "api_warning": "插件系统正在开发中, 请勿使用插件功能。",
43 | "inputPlaceholder": "来说点什么吧.... 输入/ 以查看提示词",
44 | "coiledText": "连续对话 {{status}}",
45 | "tryGpt4": "Try GPT-4",
46 | "optionTitle": "配置",
47 | "option": {
48 | "apiType": "访问方式",
49 | "apiTypeAPI": "api",
50 | "apiTypeWEB": "web",
51 | "model": "模型",
52 | "modelTip": "选择模型",
53 | "max_tokens": "单次回复限制",
54 | "max_tokensTip": "单次交互所用的最大 Token 数",
55 | "top_p": "回复筛选",
56 | "top_pTip": "top_p值越大,回复越有可能是高质量的",
57 | "temperature": "回复随机性",
58 | "temperatureTip": "temperature值越大,回复越有可能是随机的",
59 | "presence_penalty": "话题重复度",
60 | "presence_penaltyTip": "presence_penalty值越大,越有可能谈论新内容话题",
61 | "frequency_penalty": "词汇重复度",
62 | "frequency_penaltyTip": "frequency_penalty值越大,越有可能避免重复内容",
63 | "stop": "结束符",
64 | "stopTip": "回复中出现该字符时,停止回复"
65 | }
66 | },
67 | "prompt": {
68 | "newPrompt": "创建提示词",
69 | "editPrompt": "编辑提示词",
70 | "save": "保存",
71 | "contextEmpty": "上下文提示词不能为空",
72 | "onlinePrompt": "在线提示词",
73 | "promptStoreTitle": "推荐提示词Prompt",
74 | "importExport": "导入导出",
75 | "copy": "复制",
76 | "copySuccess": "复制成功",
77 | "chat": "对话",
78 | "search": "搜索",
79 | "searchPlaceholder": "请输入提示词",
80 | "languagePlaceholder": "请选择语言",
81 | "tag": {
82 | "all": "全部",
83 | "recommend": "推荐",
84 | "star": "收藏",
85 | "delete": "删除",
86 | "deleteSuccess": "删除成功",
87 | "deleteFailed": "删除失败",
88 | "starSuccess": "收藏成功",
89 | "starFailed": "收藏失败",
90 | "unstarSuccess": "取消收藏成功",
91 | "unstarFailed": "取消收藏失败"
92 | }
93 | },
94 | "plugin": {
95 | "newPlugin": "创建插件",
96 | "editPlugin": "编辑插件",
97 | "save": "保存",
98 | "onlinePlugin": "在线插件",
99 | "pluginStoreTitle": "推荐插件",
100 | "importExport": "导入导出",
101 | "search": "搜索",
102 | "searchPlaceholder": "请输入插件",
103 | "tag": {
104 | "all": "全部",
105 | "new": "新上",
106 | "recommend": "推荐",
107 | "star": "收藏",
108 | "install": "安装",
109 | "uninstall": "卸载",
110 | "delete": "删除",
111 | "deleteSuccess": "删除成功",
112 | "deleteFailed": "删除失败",
113 | "starSuccess": "收藏成功",
114 | "starFailed": "收藏失败",
115 | "unstarSuccess": "取消收藏成功",
116 | "unstarFailed": "取消收藏失败",
117 | "installSuccess": "安装成功",
118 | "installFailed": "安装失败",
119 | "uninstallSuccess": "卸载成功",
120 | "uninstallFailed": "卸载失败"
121 | }
122 | },
123 | "setting": {
124 | "m_surface": "外观配置",
125 | "m_surface_option": {
126 | "theme": "主题",
127 | "colorPrimary": "主色",
128 | "radius": "圆角",
129 | "loose": "宽松",
130 | "compact": "紧凑",
131 | "default": "默认"
132 | },
133 | "m_common": "通用配置",
134 | "m_common_option": {
135 | "lang": "语言",
136 | "send_message_style": "发送消息"
137 | },
138 | "m_network": "网络设置",
139 | "m_network_option": {
140 | "DEFAULT_API_TYPE": "默认请求方式",
141 | "apiTypeAPI": "api",
142 | "apiTypeWEB": "web",
143 | "BACKEND_URL": "服务端地址",
144 | "OPENAI_API_KEY": "OpenAI API Key",
145 | "OPENAI_API_BASE_URL": "OpenAI API Base URL",
146 | "OPENAI_ACCESS_TOKEN": "OpenAI Access Token",
147 | "API_REVERSE_PROXY": "API Reverse Proxy"
148 | },
149 | "m_feedback": "意见反馈",
150 | "m_feedback_option": {},
151 | "m_about": "关于应用",
152 | "m_about_option": {
153 | "now_version": "当前版本",
154 | "check_update": "检查更新",
155 | "update_log": "更新日志"
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/service/src/modules/chatgpt/chatgpt.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { ConfigService } from '@nestjs/config';
3 | import type {
4 | ChatGPTAPI,
5 | ChatGPTUnofficialProxyAPI,
6 | ChatGPTAPIOptions,
7 | ChatMessage,
8 | SendMessageOptions,
9 | SendMessageBrowserOptions,
10 | } from 'chatgpt';
11 | // import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt';
12 | // import fetch from 'node-fetch';
13 | // import QuickLRU from 'quick-lru';
14 | import Keyv from 'keyv';
15 | import type { OutputOptions } from '../../utils';
16 | import { output } from '../../utils';
17 |
18 | // fixed load esm module
19 | export const importDynamic = new Function(
20 | 'modulePath',
21 | 'return import(modulePath)',
22 | );
23 |
24 | export interface ConfigOptions {
25 | API_TYPE?: 'chatgpt-api' | 'chatgpt-web';
26 | OPENAI_API_KEY?: string;
27 | OPENAI_API_BASE_URL?: string;
28 | OPENAI_ACCESS_TOKEN?: string;
29 | API_REVERSE_PROXY?: string;
30 | }
31 |
32 | @Injectable()
33 | export class ChatgptService {
34 | setConfig(): OutputOptions | PromiseLike> {
35 | throw new Error('Method not implemented.');
36 | }
37 | private api: ChatGPTAPI | ChatGPTUnofficialProxyAPI;
38 | private messageStore: Keyv;
39 |
40 | constructor(private config: ConfigService) {}
41 |
42 | async getApi(
43 | options?: SendMessageOptions & SendMessageBrowserOptions,
44 | _config?: ConfigOptions,
45 | ): Promise {
46 | const API_TYPE =
47 | _config?.API_TYPE || this.config.get('API_TYPE') || 'chatgpt-web';
48 | const OPENAI_API_KEY =
49 | _config?.OPENAI_API_KEY || this.config.get('OPENAI_API_KEY');
50 | const OPENAI_API_BASE_URL =
51 | _config?.OPENAI_API_BASE_URL || this.config.get('OPENAI_API_BASE_URL');
52 | const OPENAI_ACCESS_TOKEN =
53 | _config?.OPENAI_ACCESS_TOKEN || this.config.get('OPENAI_ACCESS_TOKEN');
54 | const API_REVERSE_PROXY =
55 | _config?.API_REVERSE_PROXY || this.config.get('API_REVERSE_PROXY');
56 |
57 | // async load chatgpt
58 | const { ChatGPTAPI, ChatGPTUnofficialProxyAPI } = await importDynamic(
59 | 'chatgpt',
60 | );
61 | // eslint-disable-next-line @typescript-eslint/no-var-requires
62 | let fetch = await importDynamic('node-fetch');
63 | fetch = fetch.default;
64 |
65 | // eslint-disable-next-line @typescript-eslint/no-var-requires
66 | let QuickLRU = await importDynamic('quick-lru');
67 | QuickLRU = QuickLRU.default;
68 | if (!this.messageStore) {
69 | this.messageStore = new Keyv({
70 | store: new QuickLRU({ maxSize: 10000 }),
71 | });
72 | }
73 |
74 | const _options: ChatGPTAPIOptions = {
75 | apiKey: '',
76 | fetch,
77 | debug: false,
78 | };
79 |
80 | let api = this.api;
81 | // chatgpt-api style
82 | if (API_TYPE == 'chatgpt-api') {
83 | api = new ChatGPTAPI({
84 | ..._options,
85 | apiKey: OPENAI_API_KEY,
86 | apiBaseUrl: OPENAI_API_BASE_URL || 'https://api.openai.com/v1',
87 | completionParams: {
88 | model: 'gpt-3.5-turbo',
89 | temperature: 0.8,
90 | top_p: 1.0,
91 | presence_penalty: 1.0,
92 | frequency_penalty: 0,
93 | ...(options as SendMessageOptions)?.completionParams,
94 | },
95 | messageStore: this.messageStore,
96 | maxModelTokens: 4000,
97 | maxResponseTokens: 1000,
98 | });
99 | }
100 | // chatgpt-web style
101 | if (API_TYPE == 'chatgpt-web') {
102 | api = new ChatGPTUnofficialProxyAPI({
103 | ..._options,
104 | accessToken: OPENAI_ACCESS_TOKEN,
105 | apiReverseProxyUrl:
106 | API_REVERSE_PROXY ||
107 | 'https://bypass.churchless.tech/api/conversation',
108 | model: 'text-davinci-002-render-sha',
109 | });
110 | }
111 |
112 | this.api = api;
113 | return this.api;
114 | }
115 |
116 | /**
117 | * send message
118 | * @param text
119 | * @param opt
120 | * @param onProgress
121 | * @returns
122 | */
123 | async sendMessage(
124 | text: string,
125 | options?: SendMessageOptions & SendMessageBrowserOptions,
126 | config?: ConfigOptions,
127 | onProgress?: (chat: ChatMessage) => void,
128 | ): Promise {
129 | const api = await this.getApi(options, config);
130 | let resData: ChatMessage;
131 | await api.sendMessage(text, {
132 | ...options,
133 | // print the partial response as the AI is "typing"
134 | onProgress: (partialResponse) => {
135 | // console.log('gpt:', partialResponse);
136 | resData = partialResponse;
137 | onProgress?.(partialResponse);
138 | },
139 | });
140 | console.log('sendMessage result:', resData);
141 | return output({ code: 0, data: resData });
142 | }
143 |
144 | /**
145 | * get account bill token
146 | */
147 | async getBill(): Promise {
148 | // throw new Error('Method not implemented.');
149 | return output({ code: 0, data: null });
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/components/pages/chat/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import ReactMarkdown from 'react-markdown'
2 | import { CopyOutlined, CheckOutlined } from '@ant-design/icons'
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
4 | import { oneDark, oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism' // 代码高亮主题风格
5 | import styles from '@/styles/github-markdown.module.css'
6 | import remarkGfm from 'remark-gfm'
7 | import remarkMath from 'remark-math'
8 | import rehypeRaw from 'rehype-raw'
9 | import rehypeKatex from 'rehype-katex'
10 | import 'katex/dist/katex.min.css' // `rehype-katex` does not import the CSS for you
11 | import { useEffect, useState } from 'react'
12 | import { Typography } from 'antd'
13 |
14 | const them = {
15 | dark: oneDark,
16 | light: { ...oneLight, backgroundColor: '#fff' },
17 | }
18 | export type MarkdownProps = {
19 | children: string
20 | role?: 'user' | 'system'
21 | theme?: 'dark' | 'light' | 'auto' | 'mix'
22 | token?: any
23 | style?: React.CSSProperties
24 | }
25 |
26 | function Markdown(props: MarkdownProps) {
27 | const { role, theme, token, style } = props
28 |
29 | const backgroundColor = () => {
30 | if (role === 'user') {
31 | if (theme === 'dark') {
32 | return token.colorPrimaryBgHover
33 | } else {
34 | return token.colorPrimaryBgHover
35 | }
36 | } else {
37 | if (theme === 'dark') {
38 | return '#1e1e20'
39 | } else {
40 | return '#ebebeb'
41 | }
42 | }
43 | }
44 |
45 | // Add the CodeCopyBtn component to our PRE element
46 | // @ts-ignore
47 | const Pre = ({ children: _children }) => {
48 | const language = _children?.[0]?.props?.className?.split('-')[1]
49 | return (
50 |
51 |
52 | {_children}
53 |
54 | {_children}
55 |
56 | )
57 | }
58 |
59 | return (
60 |
61 |
82 | {String(_children).replace(/\n$/, '')}
83 |
84 | ) : (
85 |
86 | {_children}
87 |
88 | )
89 | },
90 | p: ({ node, ..._props }) => (
91 |
92 | {_props.children}
93 |
94 | ),
95 | }}
96 | />
97 |
98 | )
99 | }
100 |
101 | // @ts-ignore
102 | function CodeCopyBtn({ theme, style, children: _children }) {
103 | const [copyOk, setCopyOk] = useState(false)
104 | const language = _children?.[0]?.props?.className?.split('-')[1]
105 |
106 | const handleClick = () => {
107 | const _props = _children?.[0].props
108 | let text = _props.children?.[0]
109 | let xcode = ''
110 | // xcode = language ? xcode + '// ' + language + '\n' : ''
111 | xcode = xcode + text + '\n'
112 | navigator.clipboard.writeText(xcode)
113 | console.log('language text:', language)
114 | console.log('copy text:\n', xcode)
115 | setCopyOk(true)
116 | setTimeout(() => {
117 | setCopyOk(false)
118 | }, 1200)
119 | }
120 |
121 | return (
122 | <>
123 |
140 | {language}
141 | {copyOk ? : }
142 |
143 | >
144 | )
145 | }
146 |
147 | export default Markdown
148 |
--------------------------------------------------------------------------------
/src/locales/en_US.json:
--------------------------------------------------------------------------------
1 | {
2 | "lang_name": "English",
3 | "title": "ChatGPT-Plus",
4 | "window": { "title": "ChatGPT-Plus {{title}}" },
5 | "c": {
6 | "message": "Message",
7 | "prompt": "Prompt",
8 | "plugin": "Plugin",
9 | "share": "Share",
10 | "store": "Store",
11 | "readme": "Readme",
12 | "setting": "Setting",
13 | "github": "Github",
14 | "open": "Open",
15 | "close": "Close",
16 | "delete": "Delete",
17 | "deleteSuccess": "Delete succeeded",
18 | "save": "Save",
19 | "saveSuccess": "Save succeeded",
20 | "reset": "Reset",
21 | "action": "Action",
22 | "export": "Export",
23 | "exportSuccess": "Export succeeded",
24 | "import": "Import",
25 | "importSuccess": "Import succeeded",
26 | "clear": "Clear",
27 | "clearSuccess": "Clear succeeded",
28 | "yes": "Yes",
29 | "no": "No",
30 | "confirm": "Confirm",
31 | "download": "Download",
32 | "success": "Operation succeeded",
33 | "failed": "Operation failed",
34 | "devBuilding": "Under development"
35 | },
36 | "chat": {
37 | "newChat": "New message",
38 | "send": "Send",
39 | "setting": "Settings",
40 | "api_warning": "The plugin system is under development. Please do not use the plugin function.",
41 | "inputPlaceholder": "Say something... Type / to view suggested words",
42 | "coiledText": "Continued conversation {{status}}",
43 | "tryGpt4": "Try GPT-4",
44 | "optionTitle": "Settings",
45 | "option": {
46 | "apiType": "Access method",
47 | "apiTypeAPI": "api",
48 | "apiTypeWEB": "web",
49 | "model": "Model",
50 | "modelTip": "Select model",
51 | "max_tokens": "Maximum reply length",
52 | "max_tokensTip": "The maximum number of tokens used in a single interaction",
53 | "top_p": "Reply filter",
54 | "top_pTip": "The higher the top_p value, the more likely the reply is of high quality",
55 | "temperature": "Reply randomness",
56 | "temperatureTip": "The higher the temperature value, the more likely the reply is random",
57 | "presence_penalty": "Topic repetition",
58 | "presence_penaltyTip": "The higher the presence_penalty value, the more likely to talk about new content topics",
59 | "frequency_penalty": "Vocabulary repetition",
60 | "frequency_penaltyTip": "The higher the frequency_penalty value, the more likely to avoid repeated content",
61 | "stop": "Stop character",
62 | "stopTip": "When this character appears in the reply, stop the reply"
63 | }
64 | },
65 | "prompt": {
66 | "newPrompt": "Create prompt",
67 | "editPrompt": "Edit prompt",
68 | "save": "Save",
69 | "contextEmpty": "Context prompt cannot be empty",
70 | "onlinePrompt": "Online prompt",
71 | "promptStoreTitle": "Recommended Prompts",
72 | "importExport": "Import and Export",
73 | "search": "Search",
74 | "searchPlaceholder": "Enter a keyword ",
75 | "languagePlaceholder": "Select language",
76 | "tag": {
77 | "all": "All",
78 | "recommend": "Recommend",
79 | "star": "Star",
80 | "delete": "Delete",
81 | "deleteSuccess": "Delete succeeded",
82 | "deleteFailed": "Delete failed",
83 | "starSuccess": "Star succeeded",
84 | "starFailed": "Star failed",
85 | "unstarSuccess": "Unstar succeeded",
86 | "unstarFailed": "Unstar failed"
87 | }
88 | },
89 | "plugin": {
90 | "newPlugin": "Create plugin",
91 | "editPlugin": "Edit plugin",
92 | "save": "Save",
93 | "onlinePlugin": "Online plugin",
94 | "pluginStoreTitle": "Recommended plugins",
95 | "importExport": "Import and Export",
96 | "search": "Search",
97 | "searchPlaceholder": "Enter a plugin",
98 | "tag": {
99 | "all": "All",
100 | "new": "New",
101 | "recommend": "Recommend",
102 | "star": "Star",
103 | "install": "Install",
104 | "uninstall": "Uninstall",
105 | "delete": "Delete",
106 | "deleteSuccess": "Delete succeeded",
107 | "deleteFailed": "Delete failed",
108 | "starSuccess": "Star succeeded",
109 | "starFailed": "Star failed",
110 | "unstarSuccess": "Unstar succeeded",
111 | "unstarFailed": "Unstar failed",
112 | "installSuccess": "Install succeeded",
113 | "installFailed": "Install failed",
114 | "uninstallSuccess": "Uninstall succeeded",
115 | "uninstallFailed": "Uninstall failed"
116 | }
117 | },
118 | "setting": {
119 | "m_surface": "Surface",
120 | "m_surface_option": {
121 | "theme": "theme",
122 | "colorPrimary": "colorPrimary",
123 | "radius": "radius",
124 | "loose": "loose",
125 | "compact": "compact",
126 | "default": "default"
127 | },
128 | "m_common": "Common",
129 | "m_common_option": {
130 | "lang": "Language",
131 | "send_message_style": "Send Message Style"
132 | },
133 | "m_network": "Network",
134 | "m_network_option": {
135 | "DEFAULT_API_TYPE": "API_TYPE",
136 | "apiTypeAPI": "api",
137 | "apiTypeWEB": "web",
138 | "BACKEND_URL": "BACKEND_URL",
139 | "OPENAI_API_KEY": "OpenAI API Key",
140 | "OPENAI_API_BASE_URL": "OpenAI API Base URL",
141 | "OPENAI_ACCESS_TOKEN": "OpenAI Access Token",
142 | "API_REVERSE_PROXY": "API Reverse Proxy"
143 | },
144 | "m_feedback": "Feedback",
145 | "m_feedback_option": {},
146 | "m_about": "About",
147 | "m_about_option": {
148 | "now_version": "now_version",
149 | "check_update": "check_update",
150 | "update_log": "update_log"
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/components/pages/chat/Option.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Checkbox, Form, Input, InputNumber, Slider, Radio, Select, Switch, theme as antdTheme, Typography } from 'antd'
3 | import { PlusOutlined } from '@ant-design/icons'
4 | import { Chat } from '@/types/chat'
5 | import { useChatContext } from '@/contexts'
6 | import { useTranslation } from '@/locales'
7 | import { useEffect, useState } from 'react'
8 |
9 | function Option(props: { chat: Chat }) {
10 | const { chat } = props
11 | const { t } = useTranslation()
12 | const { upChat } = useChatContext()
13 | const { theme } = useSiteContext()
14 | const { token } = antdTheme.useToken()
15 | const [apitype, setApitype] = useState<'chatgpt-web' | 'chatgpt-api'>('chatgpt-api')
16 | const [modelList, setModelList] = useState<{ [key: string]: any }[]>([])
17 | const [option, setOption] = useState<{ [key: string]: string | number }>({
18 | apitype: 'chatgpt-api',
19 | model: 'chatgpt-web' == apitype ? 'text-davinci-002-render-sha' : 'gpt-3.5-turbo',
20 | max_tokens: 2000,
21 | temperature: 0.8,
22 | top_p: 1.0,
23 | presence_penalty: 1.0,
24 | frequency_penalty: 0,
25 | })
26 | const [form] = Form.useForm()
27 |
28 | useEffect(() => {
29 | setApitype(chat?.option?.apitype || 'chatgpt-api')
30 | setOption({ ...option, ...chat?.option })
31 | form.setFieldsValue({ ...option, ...chat?.option })
32 | // eslint-disable-next-line react-hooks/exhaustive-deps
33 | }, [])
34 |
35 | useEffect(() => {
36 | const _apitype = apitype
37 | if ('chatgpt-web' == _apitype) {
38 | setModelList([
39 | {
40 | label: 'text-davinci-002-render-sha',
41 | value: 'text-davinci-002-render-sha',
42 | disabled: true,
43 | },
44 | ])
45 | }
46 | if ('chatgpt-api' == _apitype) {
47 | setModelList([
48 | {
49 | label: 'gpt-3.5-turbo',
50 | value: 'gpt-3.5-turbo',
51 | },
52 | {
53 | label: 'gpt-3.5-turbo-0301',
54 | value: 'gpt-3.5-turbo-0301',
55 | },
56 | {
57 | label: 'gpt-4',
58 | value: 'gpt-4',
59 | // disabled: true,
60 | },
61 | ])
62 | }
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | }, [apitype])
65 |
66 | const onValuesChange = (changedValues: any, values: any) => {
67 | console.log('changedValues', changedValues)
68 | const _option = { ...option, ...changedValues }
69 | if (changedValues?.apitype) {
70 | setApitype(changedValues.apitype)
71 | if ('chatgpt-api' == changedValues.apitype) {
72 | form.setFieldsValue({
73 | model: 'gpt-3.5-turbo',
74 | })
75 | _option.model = 'gpt-3.5-turbo'
76 | }
77 | if ('chatgpt-web' == changedValues.apitype) {
78 | form.setFieldsValue({
79 | model: 'text-davinci-002-render-sha',
80 | })
81 | _option.model = 'text-davinci-002-render-sha'
82 | }
83 | }
84 | setOption({ ..._option })
85 | chat?.uuid && upChat(chat?.uuid, { option: { ..._option } })
86 | }
87 |
88 | function onFieldsChange(changedFields: any, allFields: any): void {
89 | console.log('changedFields', changedFields)
90 | }
91 |
92 | return (
93 | <>
94 |
106 |
107 | {t('chat.option.apiTypeWEB')}
108 | {t('chat.option.apiTypeAPI')}
109 |
110 |
111 |
112 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | >
137 | )
138 | }
139 |
140 | export default Option
141 |
--------------------------------------------------------------------------------
/src/contexts/chat.tsx:
--------------------------------------------------------------------------------
1 | import { Chat, Message } from '@/types/chat'
2 | import { uuidv4 } from '@/utils/uuid'
3 | import { createContext, useContext, useEffect, useRef, useState } from 'react'
4 | import { useRouter } from 'next/router'
5 | import storage from '@/utils/storage'
6 |
7 | export type ChatContentType = {
8 | chatList: Array
9 | setChatList: (chatList: Array) => void
10 | activeChat?: Chat | null
11 | setActiveChat: (chat: Chat) => void
12 | newChat: (chat: Chat, message?: Message) => void
13 | delChat: (uuid: string) => void
14 | upChat: (uuid: string, obj: { [key: string]: string }) => void
15 | newMessage: (uuid: string, message: Message) => void
16 | delMessage: (uuid: string, message: Message) => void
17 | }
18 |
19 | const Context = createContext({
20 | chatList: [],
21 | setChatList: () => {},
22 | setActiveChat: () => {},
23 | activeChat: null,
24 | newChat: () => {},
25 | delChat: () => {},
26 | upChat: () => {},
27 | newMessage: () => {},
28 | delMessage: () => {},
29 | })
30 |
31 | // @ts-ignore
32 | export function ChatProvider({ children }) {
33 | const router = useRouter()
34 |
35 | const [chatList, setChatList] = useState>([])
36 | const [activeChat, setActiveChat] = useState()
37 | let refList = useRef()
38 |
39 | useEffect(() => {
40 | storage.get('chatList').then((res) => {
41 | let _chatList = res || []
42 | // 如果是string,则转换成数组
43 | if (typeof _chatList == 'string') {
44 | _chatList = JSON.parse(_chatList)
45 | }
46 | setChatList(_chatList as any[])
47 | })
48 | }, [])
49 |
50 | useEffect(() => {
51 | refList.current = chatList || []
52 | // 存储
53 | storage.set('chatList', chatList)
54 | }, [chatList])
55 |
56 | const newChat = (chat?: Chat, message?: Message) => {
57 | const _chatList = [...(refList.current as Chat[])]
58 | if (!chat) {
59 | chat = {
60 | uuid: uuidv4(),
61 | name: 'ChatGPT',
62 | lastMessageText: 'No message',
63 | }
64 | }
65 | const index = _chatList.findIndex((item) => item.uuid == chat?.uuid)
66 | if (index > -1) {
67 | _chatList.splice(index, 1)
68 | }
69 | _chatList.unshift(chat)
70 | setChatList(_chatList)
71 | if (message) {
72 | newMessage(chat.uuid, message)
73 | }
74 | }
75 | const delChat = (uuid: string) => {
76 | const _chatList = [...(refList.current as Chat[])]
77 | const _list = _chatList.filter((item) => item.uuid !== uuid)
78 | setChatList(_list)
79 | // 如果删除的是当前聊天,跳转到第一个聊天
80 | if (_list && _list.length > 0) {
81 | setActiveChat(_list[0])
82 | router.push(`/chat?uuid=${_list[0].uuid}`)
83 | } else {
84 | setActiveChat(undefined)
85 | router.push(`/chat`)
86 | }
87 | }
88 |
89 | const upChat = (uuid: string, obj: { [key: string]: string }) => {
90 | const _chatList = [...(refList.current as Chat[])]
91 | const index = _chatList.findIndex((item) => item.uuid == uuid)
92 | if (index > -1) {
93 | const _chat = _chatList[index]
94 | const _nChat = Object.assign({}, { ..._chat }, { ...obj })
95 | console.log(_nChat, obj)
96 | _chatList[index] = _nChat
97 | setChatList(_chatList)
98 | setActiveChat(_nChat)
99 | }
100 | }
101 | const newMessage = (uuid: string, message: Message) => {
102 | const _chatList = [...(refList.current as Chat[])]
103 | const index = _chatList.findIndex((item) => item.uuid == uuid)
104 | if (index > -1) {
105 | const _chat = _chatList[index]
106 | if (!_chat.messageList) {
107 | _chat.messageList = []
108 | }
109 | if (message) {
110 | _chat.lastMessage = message
111 | _chat.lastMessageText = message.text
112 | _chat.lastMessageTime = message.dateTime
113 | }
114 | _chat.messageList.push(message)
115 | const _nChat = Object.assign({}, { ..._chat })
116 | _chatList[index] = _nChat
117 | setChatList(_chatList)
118 | setActiveChat(_nChat)
119 | }
120 | }
121 |
122 | const delMessage = (uuid: string, message: Message) => {
123 | const _chatList = [...(refList.current as Chat[])]
124 | const index = _chatList.findIndex((item) => item.uuid == uuid)
125 | if (index > -1) {
126 | const _chat = _chatList[index]
127 | if (!_chat.messageList) {
128 | _chat.messageList = []
129 | }
130 | const _index = _chat.messageList.findIndex((item) => item.dateTime == message.dateTime && item.text == message.text)
131 | if (_index > -1) {
132 | _chat.messageList.splice(_index, 1)
133 | }
134 | const _nChat = Object.assign({}, { ..._chat })
135 | if (!_nChat.messageList) {
136 | _nChat.lastMessage = undefined
137 | _nChat.lastMessageText = ''
138 | _nChat.lastMessageTime = ''
139 | } else {
140 | // 取最后一个数组对象
141 | const _lastMessage = _nChat.messageList[_nChat.messageList.length - 1]
142 | if (_lastMessage) {
143 | _nChat.lastMessage = _lastMessage
144 | _nChat.lastMessageText = _lastMessage.text
145 | _nChat.lastMessageTime = _lastMessage.dateTime
146 | }
147 | }
148 | _chatList[index] = _nChat
149 | setChatList(_chatList)
150 | setActiveChat(_nChat)
151 | console.log(_nChat.messageList, uuid, index)
152 | }
153 | }
154 | return (
155 |
168 | {children}
169 |
170 | )
171 | }
172 |
173 | export function useChatContext() {
174 | return useContext(Context)
175 | }
176 |
--------------------------------------------------------------------------------
/src/components/pages/chat/List.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Button, List, Typography, App, Popconfirm, theme as antdTheme, Divider } from 'antd'
2 | import { DeleteOutlined, MessageOutlined } from '@ant-design/icons'
3 | import { useTranslation } from '@/locales'
4 | import Image from 'next/image'
5 | import { useRouter } from 'next/router'
6 | import { useCallback, useEffect, useState } from 'react'
7 | import { useSiteContext } from '@/contexts/site'
8 | import { Chat } from '@/types/chat'
9 | import { useChatContext } from '@/contexts/chat'
10 | import { uuidv4 } from '@/utils/uuid'
11 |
12 | function IndexPage(props: { style?: React.CSSProperties }) {
13 | const router = useRouter()
14 | const { token } = antdTheme.useToken()
15 | const { theme } = useSiteContext()
16 | const { message, modal, notification } = App.useApp()
17 | const { chatList, setChatList, activeChat, newChat, delChat } = useChatContext()
18 | const { t } = useTranslation()
19 | const [uuid, setUuid] = useState('')
20 | const [list, setList] = useState([])
21 |
22 | const openChat = useCallback(
23 | (uuid: string) => {
24 | console.log(uuid)
25 | router.push(`/chat?uuid=${uuid}`)
26 | },
27 | [router]
28 | )
29 |
30 | useEffect(() => {
31 | setList(chatList)
32 | }, [chatList])
33 |
34 | useEffect(() => {
35 | setUuid(activeChat?.uuid as string)
36 | }, [activeChat])
37 |
38 | const confirm = (e: React.MouseEvent, uuid: string) => {
39 | e.stopPropagation()
40 | console.log(e)
41 | deleteChat(uuid)
42 | message.success('Click on Yes')
43 | }
44 |
45 | const cancel = (e: React.MouseEvent) => {
46 | e.stopPropagation()
47 | console.log(e)
48 | message.error('Click on No')
49 | }
50 |
51 | const addChat = () => {
52 | // 创建新聊天
53 | const chat: Chat = {
54 | uuid: uuidv4(),
55 | name: 'ChatGPT',
56 | lastMessageText: 'No message',
57 | }
58 | newChat(chat)
59 | router.push(`/chat?uuid=${chat.uuid}`)
60 | console.log('newChat', chat)
61 | }
62 |
63 | const deleteChat = (uuid: string) => {
64 | // 从数据中删除聊天
65 | delChat(uuid)
66 | console.log('delChat', uuid)
67 | }
68 | return (
69 |
70 |
73 |
(
79 |
134 | )}
135 | />
136 |
150 |
155 |
156 |
157 | )
158 | }
159 |
160 | export default IndexPage
161 |
--------------------------------------------------------------------------------
/src/components/pages/prompt/Export.tsx:
--------------------------------------------------------------------------------
1 | import { useSiteContext } from '@/contexts/site'
2 | import { Avatar, Button, Card, Drawer, FloatButton, Input, InputRef, App, Popconfirm, Space, theme as antdTheme, Tooltip, Typography } from 'antd'
3 | import { ExpandAltOutlined, DeleteOutlined, SendOutlined, ApiOutlined, DisconnectOutlined, LinkOutlined, ControlOutlined, EllipsisOutlined, MoreOutlined } from '@ant-design/icons'
4 | import { useTranslation } from '@/locales'
5 | import { useRouter } from 'next/router'
6 | import Image from 'next/image'
7 | import { useEffect, useRef, useState } from 'react'
8 | import dayjs from 'dayjs'
9 | import { Prompt } from '@/types/prompt'
10 | import { useEventTarget } from 'ahooks'
11 | import { usePromptContext } from '@/contexts/prompt'
12 | import { uuidv4 } from '@/utils/uuid'
13 |
14 | const _data: Prompt = {
15 | uuid: '1679282990940',
16 | name: '小冰',
17 | description: '你的私人小秘书',
18 | prompt: '你好,我是小冰,你的私人小秘书',
19 | datetime: '2023/3/20 11:32:26',
20 | type: 'text',
21 | status: 'online',
22 | private: true,
23 | star: 0,
24 | historyList: [
25 | {
26 | uuid: '1679282990940',
27 | datetime: '2023/3/20 11:32:26',
28 | name: '小冰',
29 | description: '你的私人小秘书',
30 | prompt: '你好,我是小冰,你的私人小秘书',
31 | type: 'text',
32 | status: 'online',
33 | },
34 | ],
35 | }
36 |
37 | function OnlinePrompt() {
38 | const router = useRouter()
39 | const { token } = antdTheme.useToken()
40 | const { theme } = useSiteContext()
41 | const { message, modal, notification } = App.useApp()
42 | const { addPrompt, delPrompt, upPrompt } = usePromptContext()
43 | const { t } = useTranslation()
44 | const refInput = useRef(null)
45 | // const [input, setInput] = useState('')
46 | const [input, { reset, onChange }] = useEventTarget({ initialValue: '' })
47 | const [canSend, setCanSend] = useState(false)
48 | const [coiled, setCoiled] = useState(true)
49 | const [openSet, setOpenSet] = useState(false)
50 | const [uuid, setUuid] = useState('')
51 | const [info, setInfo] = useState()
52 | const [list, setList] = useState([])
53 |
54 | const containerStyle: React.CSSProperties = {
55 | position: 'relative',
56 | // height: 200,
57 | // padding: 48,
58 | // overflow: 'hidden',
59 | // textAlign: 'center',
60 | background: token.colorFillAlter,
61 | border: `1px solid ${token.colorBorderSecondary}`,
62 | borderRadius: token.borderRadiusLG,
63 | }
64 |
65 | const editName = (_name: string) => {
66 | upPrompt(uuid, {
67 | name: _name,
68 | })
69 | }
70 | const editDesc = (_description: string) => {
71 | upPrompt(uuid, {
72 | description: _description,
73 | })
74 | }
75 |
76 | return (
77 |
78 |
98 |
102 |
103 | }
108 | onClick={() => {
109 | message.warning(t('prompt.api_warning'))
110 | }}
111 | >
112 | : }
117 | onClick={() => setCoiled(!coiled)}
118 | >
119 | ) => {
124 | delPrompt(uuid)
125 | return
126 | }}
127 | onCancel={(e?: React.MouseEvent) => {}}
128 | okText="Yes"
129 | cancelText="No"
130 | >
131 | }>
132 |
133 | }
138 | onClick={() => {
139 | // setOpenSet(!openSet)
140 | }}
141 | >
142 |
143 |
144 |
145 | {/* {list.length <= 0 ? (
146 |
147 | ) : (
148 |
149 | {list.map((item: Message) => {
150 | return
151 | })}
152 |
153 | )} */}
154 |
155 |
setOpenSet(false)} getContainer={false}>
156 |
{
160 | return document.getElementById('messageBox')
161 | }}
162 | />
163 |
164 |
165 | )
166 | }
167 |
168 | export default OnlinePrompt
169 |
--------------------------------------------------------------------------------