Commit 0ecf576e by archer

doc gpt V0.2

parent cc5cf99e
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
.yalc/
yalc.lock
\ No newline at end of file
AXIOS_PROXY_HOST=127.0.0.1
AXIOS_PROXY_PORT=33210
MONGODB_UR=
MY_MAIL=
MAILE_CODE=
TOKEN_KEY=
\ No newline at end of file
...@@ -34,3 +34,5 @@ yarn-error.log* ...@@ -34,3 +34,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
public/trainData/
.vscode/
\ No newline at end of file
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
if command -v npx >/dev/null 2>&1; then
npx lint-staged
fi
\ No newline at end of file
dist
.vscode
**/.DS_Store
node_modules
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
quoteProps: 'as-needed',
jsxSingleQuote: false,
trailingComma: 'none',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'always',
rangeStart: 0,
rangeEnd: Infinity,
requirePragma: false,
insertPragma: false,
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'css',
endOfLine: 'lf'
};
\ No newline at end of file
{
"editor.formatOnType": true,
"editor.formatOnSave": true ,
"prettier.tabWidth": 2
}
\ No newline at end of file
# Install dependencies only when needed
FROM node:current-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat && npm install -g pnpm
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json pnpm-lock.yaml* ./
RUN \
[ -f pnpm-lock.yaml ] && pnpm install || \
(echo "Lockfile not found." && exit 1)
# Rebuild the source code only when needed
FROM node:current-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm install -g pnpm && pnpm run build
# Production image, copy all the files and run next
FROM node:current-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN sed -i 's/https/http/' /etc/apk/repositories
RUN apk add curl \
&& apk add ca-certificates \
&& update-ca-certificates
# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
# COPY --from=builder /app/.env* .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
SERVICE_NAME=doc-gpt
# Image URL to use all building/pushing image targets
IMG ?= $(SERVICE_NAME):latest
.PHONY: all
all: build
##@ General
# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Build
.PHONY: build
build: ## Build desktop-frontend binary.
pnpm run build
.PHONY: run
run: ## Run a dev service from host.
pnpm run start
.PHONY: docker-build
docker-build: ## Build docker image with the desktop-frontend.
docker build -t c121914yu/doc-gpt:latest .
##@ Deployment
.PHONY: docker-run
docker-run: ## Push docker image.
docker run -d -p 8008:3000 --name doc-gpt -v /web_project/yjl/doc-gpt/logs:/app/.next/logs c121914yu/doc-gpt:latest
#TODO: add support of docker push
#TODO: add support of sealos apply
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). # Doc GPT
## Getting Started ## 初始化
复制 .env.template 成 .env.local ,填写核心参数
First, run the development server: ```
AXIOS_PROXY_HOST=axios代理地址,目前 openai 接口都需要走代理,本机的话就填 127.0.0.1
AXIOS_PROXY_PORT=代理端口
MONGODB_UR=mongo数据库地址
MY_MAIL=发送验证码邮箱
MAILE_CODE=邮箱秘钥
TOKEN_KEY=随便填一个,用于生成和校验token
```
```bash ```bash
npm run dev
# or
yarn dev
# or
pnpm dev pnpm dev
``` ```
## 部署
```bash
# 本地 docker 打包
docker build -t imageName .
docker push imageName
# 服务器拉取部署
docker pull imageName
docker stop doc-gpt || true
docker rm doc-gpt || true
# 运行时才把参数写入
docker run -d --network=host --name doc-gpt -e AXIOS_PROXY_HOST= -e AXIOS_PROXY_PORT= -e MAILE_CODE= -e TOKEN_KEY= -e MONGODB_UR= imageName
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. # 介绍页
## 欢迎使用 Doc GPT
时间比较赶,介绍没来得及完善,先直接上怎么使用:
1. 使用邮箱注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
### 模型配置
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More 3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
To learn more about Next.js, take a look at the following resources: 4. **过期时间**:生成对话框后,这个对话框多久过期。
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ### 对话框介绍
## Deploy on Vercel 1. 每个对话框以 windowId 作为标识。
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如:
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. * 当前网页链接:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. ### 其他问题
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
\ No newline at end of file
{"prompt": "sealos的介绍", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
{"prompt": "sealos是什么", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
{"prompt": "sealos安装的先决条件", "completion": "sealos 是一个简单的 go 二进制文件,可以安装在大多数 Linux 操作系统中。"}
{"prompt": "sealos的CPU架构", "completion": "目前支持 amd64 和 arm64 架构。"}
\ No newline at end of file
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const path = require('path');
const isDev = process.env.NODE_ENV === 'development';
const nextConfig = { const nextConfig = {
reactStrictMode: true, output: 'standalone',
} reactStrictMode: false,
compress: true
};
module.exports = nextConfig module.exports = nextConfig;
...@@ -6,18 +6,61 @@ ...@@ -6,18 +6,61 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"prepare": "husky install",
"format": "prettier --config \"./.prettierrc.js\" --write \"./src/**/*.{ts,tsx,scss}\""
}, },
"dependencies": { "dependencies": {
"@chakra-ui/icons": "^2.0.17",
"@chakra-ui/react": "^2.5.1",
"@chakra-ui/system": "^2.5.1",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@next/font": "13.1.6", "@next/font": "13.1.6",
"@types/node": "18.14.0", "@reduxjs/toolkit": "^1.9.3",
"@types/react": "18.0.28", "@tanstack/react-query": "^4.24.10",
"@types/react-dom": "18.0.11", "axios": "^1.3.3",
"crypto": "^1.0.1",
"dayjs": "^1.11.7",
"eslint": "8.34.0", "eslint": "8.34.0",
"eslint-config-next": "13.1.6", "eslint-config-next": "13.1.6",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"hyperdown": "^2.4.29",
"immer": "^9.0.19",
"jsonwebtoken": "^9.0.0",
"mongoose": "^6.10.0",
"next": "13.1.6", "next": "13.1.6",
"nodemailer": "^6.9.1",
"openai": "^3.2.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"typescript": "4.9.5" "react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1",
"sass": "^1.58.3",
"sharp": "^0.31.3",
"tunnel": "^0.0.6",
"typescript": "4.9.5",
"uuid": "^9.0.0",
"zustand": "^4.3.5"
},
"devDependencies": {
"@types/formidable": "^2.0.5",
"@types/jsonwebtoken": "^9.0.1",
"@types/node": "18.14.0",
"@types/nodemailer": "^6.4.7",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/tunnel": "^0.0.3",
"@types/uuid": "^9.0.1",
"husky": "^8.0.3",
"lint-staged": "^13.1.2",
"prettier": "^2.8.4"
},
"lint-staged": {
"./src/**/*.{ts,tsx,scss}": "npm run format"
} }
} }
This source diff could not be displayed because it is too large. You can view the blob instead.
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"}
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"}
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"}
{"prompt":"什么是lru算法?\n\n###\n\n","completion":" lru算法是一种页面置换算法,它的全称是least recently used,即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录,当需要替换一页时,选择最近最少被访问的页面进行替换。###"}
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"}
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"}
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"}
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"}
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数,它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"}
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"}
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"}
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"}
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"}
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"}
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"}
{"prompt":"什么是线程?\n\n###\n\n","completion":" 线程是操作系统能够进行运算调度的最小单位,它是进程的一个执行流程,共享进程的地址空间。###"}
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存,而实际上它通常是被分成多个物理内存碎片。###"}
{"prompt":"什么是死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有进程都无法继续执行。###"}
{"prompt":"什么是缓存?\n\n###\n\n","completion":" 缓存是指在计算机中暂存数据的高速存储器,它可以提高计算机对数据的访问速度,避免频繁地访问较慢的主存储器。###"}
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是计算机中用来管理和组织文件的一种机制,它通过一系列的数据结构来描述文件和目录的组织方式,以及文件如何存储和访问。###"}
{"prompt":"什么是调度算法?\n\n###\n\n","completion":" 调度算法是指操作系统中用来决定进程或线程在cpu上执行顺序的一种算法,它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}
No preview for this file type
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="204px" height="204px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="84" cy="50" r="10" fill="#e15b64">
<animate attributeName="r" repeatCount="indefinite" dur="0.5681818181818182s" calcMode="spline" keyTimes="0;1" values="15;0" keySplines="0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="fill" repeatCount="indefinite" dur="2.272727272727273s" calcMode="discrete" keyTimes="0;0.25;0.5;0.75;1" values="#e15b64;#abbd81;#f8b26a;#f47e60;#e15b64" begin="0s"></animate>
</circle><circle cx="16" cy="50" r="10" fill="#e15b64">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="0s"></animate>
</circle><circle cx="50" cy="50" r="10" fill="#f47e60">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.5681818181818182s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-0.5681818181818182s"></animate>
</circle><circle cx="84" cy="50" r="10" fill="#f8b26a">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.1363636363636365s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.1363636363636365s"></animate>
</circle><circle cx="16" cy="50" r="10" fill="#abbd81">
<animate attributeName="r" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="0;0;15;15;15" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7045454545454546s"></animate>
<animate attributeName="cx" repeatCount="indefinite" dur="2.272727272727273s" calcMode="spline" keyTimes="0;0.25;0.5;0.75;1" values="16;16;16;50;84" keySplines="0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1;0 0.5 0.5 1" begin="-1.7045454545454546s"></animate>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1440" height="900" viewBox="0 0 1440 900" fill="none"><defs><path id="path_0"
transform="translate(0 0) rotate(0 720 450)"
d="M0,900L1440,900L1440,0L0,0L0,900Z" /><linearGradient id="linear_0" x1="45.776671171188354%" y1="3.4534157006028896%" x2="45.77667520943754%" y2="95.15074645348403%" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#4D3AF9" stop-opacity="1" /><stop offset="0.4571722174852774" stop-color="#3B4DFE" stop-opacity="1" /><stop offset="1" stop-color="#1A68FF" stop-opacity="1" /></linearGradient><linearGradient id="linear_1" x1="50%" y1="2.535076530612245%" x2="51%" y2="100%" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#4581FF" stop-opacity="1" /><stop offset="1" stop-color="#1C57FE" stop-opacity="1" /></linearGradient><linearGradient id="linear_2" x1="50%" y1="2.535076530612245%" x2="51%" y2="100%" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#4581FF" stop-opacity="1" /><stop offset="1" stop-color="#1C57FE" stop-opacity="1" /></linearGradient><linearGradient id="linear_3" x1="50%" y1="0%" x2="51%" y2="100%" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#29BFA8" stop-opacity="1" /><stop offset="1" stop-color="#34D1B6" stop-opacity="1" /></linearGradient><linearGradient id="linear_4" x1="50%" y1="2.535076530612245%" x2="51%" y2="100%" gradientUnits="objectBoundingBox"><stop offset="0" stop-color="#4581FF" stop-opacity="1" /><stop offset="1" stop-color="#1C57FE" stop-opacity="1" /></linearGradient></defs><g opacity="1" transform="translate(0 0) rotate(0 720 450)"><mask id="mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#mask-0)"><path id="Mask" fill-rule="evenodd" fill="url(#linear_0)" transform="translate(0 0) rotate(0 720 450)" opacity="1" d="M0,900L1440,900L1440,0L0,0L0,900Z " /></g><g mask="url(#mask-0)"><path id="Oval" fill-rule="evenodd" fill="url(#linear_1)" transform="translate(51.66666666666667 412.5) rotate(0 37.5 37.5)" opacity="1" d="M75,37.5C75,16.79 58.21,0 37.5,0C16.79,0 0,16.79 0,37.5C0,58.21 16.79,75 37.5,75C58.21,75 75,58.21 75,37.5Z " /></g><g mask="url(#mask-0)"><path id="Oval Copy 2" fill-rule="evenodd" fill="url(#linear_2)" transform="translate(1049.166666666667 -3.333333333333333) rotate(0 30.833333333333336 30.833333333333336)" opacity="0.69" d="M61.67,30.83C61.67,13.81 47.86,0 30.83,0C13.81,0 0,13.81 0,30.83C0,47.86 13.81,61.67 30.83,61.67C47.86,61.67 61.67,47.86 61.67,30.83Z " /></g><g mask="url(#mask-0)"><g opacity="0.55" transform="translate(-64.16666666666667 547.4999999999999) rotate(0 50 226.5)"><path id="Rectangle 2" fill-rule="evenodd" style="fill:#5088FF" transform="translate(14.63769413070223 15.28431509376342) rotate(-45 35.20283757354478 34.876453649023716)" opacity="1" d="M8.17,61.88L70.41,69.75L47.22,22.97L0,0L8.17,61.88Z " /><path id="Rectangle 3" fill-rule="evenodd" fill="url(#linear_3)" transform="translate(0 50.20419426048581) rotate(0 26.487523940001577 201.14790286975716)" opacity="1" d="M0,402.3L51.51,402.3L52.98,23.25L0,0L0,402.3Z " /><path id="Rectangle 3 Copy" fill-rule="evenodd" fill="url(#linear_4)" transform="translate(48.48484848484848 50.20419426048581) rotate(0 25.75757575757576 201.14790286975716)" opacity="1" d="M0,402.3L51.52,402.3L51.52,0L0,21.68L0,402.3Z " /></g></g></g></svg>
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="17" height="12" viewBox="0 0 17 12" fill="none"><g opacity="1" transform="translate(0.70001220703125 0.2001953125) rotate(0 7.5 5.5)"><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0 5) rotate(0 7.5 0.5)" d="M0,0.5L15,0.5 " /><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(0 0) rotate(0 7.5 0.5)" d="M0,0.5L15,0.5 " /><path id="Path" style="stroke:#A0A5BA; stroke-width:1.4; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(7 10) rotate(0 4 0.5)" d="M0,0.5L8,0.5 " /></g></svg>
\ No newline at end of file
window._iconfont_svg_string_3913924='<svg><symbol id="icon-fuzhi" viewBox="0 0 1024 1024"><path d="M267.3 834.6h-96.5c-27.4 0-49.7-22.3-49.7-49.7V115.2c0-27.4 22.3-49.7 49.7-49.7H727c27.4 0 49.7 22.3 49.7 49.7v96.5h-42.6v-96.5c0-3.9-3.2-7.1-7.1-7.1H170.8c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h96.5v42.6z" ></path><path d="M851.9 959.5H295.7c-27.4 0-49.7-22.3-49.7-49.7V240.1c0-27.4 22.3-49.7 49.7-49.7h556.2c27.4 0 49.7 22.3 49.7 49.7v669.7c-0.1 27.4-22.3 49.7-49.7 49.7zM295.7 233c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h556.2c3.9 0 7.1-3.2 7.1-7.1V240.1c0-3.9-3.2-7.1-7.1-7.1H295.7z" ></path></symbol><symbol id="icon-zhongzhi" viewBox="0 0 1024 1024"><path d="M181.731 569.606l63.275-8.713A331.732 331.732 0 0 1 242.255 501l-63.809-2.835a395.86 395.86 0 0 0 3.285 71.441z" ></path><path d="M569.205 906.945c52.791 0 104.019-10.346 152.263-30.752 46.581-19.703 88.409-47.902 124.321-83.814 35.912-35.912 64.111-77.74 83.814-124.322 20.405-48.244 30.752-99.471 30.752-152.263 0-52.791-10.346-104.019-30.752-152.263-19.703-46.581-47.902-88.409-83.814-124.321-35.912-35.912-77.74-64.111-124.321-83.814-48.244-20.405-99.472-30.752-152.263-30.752-101.406 0-197.547 38.644-270.712 108.813-72.938 69.952-115.572 163.96-120.047 264.706L242.255 501c7.783-175.224 151.397-312.483 326.95-312.483 180.462 0 327.278 146.817 327.278 327.278S749.667 843.074 569.205 843.074c-69.625 0-136.091-21.563-192.21-62.357L339.44 832.38c67.106 48.781 146.558 74.565 229.765 74.565z" ></path><path d="M222.367 629.329c8.173 0 16.346-3.118 22.581-9.354l117.233-117.232c12.472-12.471 12.472-32.692 0-45.164-12.471-12.472-32.692-12.472-45.163 0L199.786 574.811c-12.472 12.472-12.472 32.693 0 45.164 6.236 6.236 14.409 9.354 22.581 9.354z" ></path><path d="M215.632 631.398c8.174 0 16.345-3.117 22.581-9.354 12.472-12.471 12.472-32.692 0-45.164L120.981 459.648c-12.47-12.471-32.691-12.471-45.163 0-12.472 12.472-12.472 32.692 0 45.164l117.233 117.232c6.234 6.235 14.409 9.354 22.581 9.354z" ></path></symbol><symbol id="icon-xiangxiazhankai-xianxingyuankuang" viewBox="0 0 1024 1024"><path d="M512 128c212 0 384 172 384 384s-172 384-384 384-384-172-384-384 172-384 384-384m0-64C264.8 64 64 264.8 64 512s200.8 448 448 448 448-200.8 448-448S759.2 64 512 64z m226.4 322.4L512 613.6 285.6 386.4 240 432l226.4 226.4L512 704l45.6-45.6L784 432l-45.6-45.6z" ></path></symbol><symbol id="icon-yunxiazai" viewBox="0 0 1024 1024"><path d="M783.36 369.066667a277.333333 277.333333 0 0 0-542.72 0A256 256 0 0 0 298.666667 874.666667v-85.333334a170.666667 170.666667 0 0 1-16.64-341.333333l35.413333-3.413333 3.413333-35.413334a192 192 0 0 1 384 0l2.986667 35.413334 35.413333 3.413333a170.666667 170.666667 0 0 1-16.213333 341.333333v85.333334a256 256 0 0 0 56.32-505.6z" ></path><path d="M554.666667 490.666667h-85.333334v224H364.373333L512 874.666667l147.626667-160H554.666667V490.666667z" ></path></symbol><symbol id="icon-fasong" viewBox="0 0 1024 1024"><path d="M416.583186 1022.194004c-5.417989 0-10.835979-1.203998-16.253968-3.611993-15.049971-6.621987-24.681952-21.069959-24.681952-37.323927l0-299.795414c0-12.641975 5.417989-24.079953 15.651969-31.905938 9.631981-7.825985 22.273956-10.23398 34.915932-7.825985l417.787184 99.931805 84.279835-599.590829c1.203998-9.631981-8.427984-16.253968-16.855967-11.437978L147.489712 573.102881l139.061728 35.517931c19.865961 4.815991 34.313933 22.875955 32.507937 43.343915-2.407995 25.885949-26.487948 42.139918-50.567901 36.119929L30.70194 627.282775c-16.253968-4.213992-27.691946-17.457966-30.099941-33.711934-2.407995-16.253968 5.417989-32.507937 19.865961-40.93592L962.59612 6.621987c13.243974-7.825985 30.099941-7.223986 43.343915 1.203998 12.641975 8.427984 19.865961 24.079953 17.457966 39.129924l-105.349794 750.090535c-1.805996 11.437978-7.825985 21.671958-17.457966 28.293945-9.631981 6.621987-21.069959 8.427984-32.507937 6.019988l-411.165197-98.125808 0 154.111699 81.87184-76.453851c15.049971-13.845973 37.925926-16.855967 54.179894-4.213992 20.46796 15.651969 21.069959 45.149912 3.009994 62.005879L444.275132 1011.358025C436.449148 1018.582011 426.817166 1022.194004 416.583186 1022.194004L416.583186 1022.194004z" ></path><path d="M416.583186 722.398589c-9.631981 0-19.263962-3.611993-27.089947-10.23398-16.855967-15.049971-18.059965-40.93592-3.009994-57.791887l216.117578-242.003527c15.049971-16.855967 40.93592-18.059965 57.791887-3.009994 16.855967 15.049971 18.059965 40.93592 3.009994 57.791887l-216.117578 242.003527C438.857143 718.184597 427.419165 722.398589 416.583186 722.398589L416.583186 722.398589z" ></path></symbol><symbol id="icon-caidan" viewBox="0 0 1305 1024"><path d="M0.837818 75.218317c0 19.642164 8.098902 39.191237 21.969435 53.06177 13.963624 13.963624 33.512697 22.062525 53.247951 22.062525a76.055204 76.055204 0 0 0 53.06177-21.969434c13.963624-13.963624 22.062525-33.512697 22.062526-53.154861A76.055204 76.055204 0 0 0 129.303156 21.970365 76.055204 76.055204 0 0 0 76.055204 0.000931a76.055204 76.055204 0 0 0-53.247951 21.969434A76.706839 76.706839 0 0 0 0.837818 75.218317M0.837818 476.160498c0 19.642164 8.005811 39.377419 21.969435 53.247952 13.963624 13.963624 33.419606 21.969435 53.247951 21.969434a76.241385 76.241385 0 0 0 53.154861-21.969434 75.962113 75.962113 0 0 0 21.969435-53.247952 76.241385 76.241385 0 0 0-21.969435-53.154861 75.962113 75.962113 0 0 0-53.154861-21.969434 76.241385 76.241385 0 0 0-53.247951 21.969434 75.962113 75.962113 0 0 0-21.969435 53.154861M0.837818 877.19577c0 19.642164 8.005811 39.284328 21.969435 53.247951 13.963624 13.963624 33.419606 21.969435 53.247951 21.969435a76.241385 76.241385 0 0 0 53.154861-21.969435 75.962113 75.962113 0 0 0 21.969435-53.247951 76.241385 76.241385 0 0 0-21.969435-53.247952 75.962113 75.962113 0 0 0-53.154861-21.969434 76.241385 76.241385 0 0 0-53.247951 21.969434 76.520658 76.520658 0 0 0-21.969435 53.247952M1304.109361 75.218317c0 41.518508-32.395607 75.124295-72.331571 75.124295H373.945843c-40.029055 0-72.331571-33.512697-72.331571-75.124295C301.521181 33.513628 333.916788 0.000931 373.945843 0.000931h857.831947c40.029055 0 72.331571 33.605788 72.331571 75.217386M1231.77779 551.377884H373.945843c-40.029055 0-72.331571-33.605788-72.331571-75.217386 0-41.518508 32.302516-75.124295 72.331571-75.124295h857.831947c40.029055-0.186182 72.331571 33.512697 72.331571 75.124295 0 41.425417-32.395607 75.217386-72.331571 75.217386zM1304.109361 877.102679c0 41.611599-32.395607 75.310477-72.331571 75.310477H373.945843c-40.029055 0-72.331571-33.698878-72.331571-75.310477 0-41.425417 32.302516-75.124295 72.331571-75.124295h857.831947c40.029055-0.093091 72.331571 33.698878 72.331571 75.124295" ></path></symbol><symbol id="icon-gongzuotai-01" viewBox="0 0 1024 1024"><path d="M896 771.413333h-768c-51.2 0-93.866667-42.666667-93.866667-93.866666V209.92c0-51.2 42.666667-93.866667 93.866667-93.866667h768c51.2 0 93.866667 42.666667 93.866667 93.866667v465.92c0 52.906667-42.666667 95.573333-93.866667 95.573333zM128 167.253333C104.106667 167.253333 85.333333 186.026667 85.333333 209.92v465.92c0 23.893333 18.773333 42.666667 42.666667 42.666667h768c23.893333 0 42.666667-18.773333 42.666667-42.666667V209.92c0-23.893333-18.773333-42.666667-42.666667-42.666667h-768z" ></path><path d="M512 907.946667c-13.653333 0-25.6-11.946667-25.6-25.6v-136.533334c0-13.653333 11.946667-25.6 25.6-25.6s25.6 11.946667 25.6 25.6v136.533334c0 13.653333-11.946667 25.6-25.6 25.6z" ></path><path d="M680.96 907.946667H343.04c-13.653333 0-25.6-11.946667-25.6-25.6s11.946667-25.6 25.6-25.6h337.92c13.653333 0 25.6 11.946667 25.6 25.6s-11.946667 25.6-25.6 25.6zM776.533333 648.533333h-529.066666c-13.653333 0-25.6-11.946667-25.6-25.6s11.946667-25.6 25.6-25.6h530.773333c13.653333 0 25.6 11.946667 25.6 25.6s-11.946667 25.6-27.306667 25.6z" ></path></symbol><symbol id="icon-datafull" viewBox="0 0 1024 1024"><path d="M512 64C323.2 64 120 120.96 120 246.016l0 531.968C120 902.976 323.2 960 512 960s392-57.024 392-182.016L904 246.016C904 120.96 700.736 64 512 64zM848 777.984c0 69.568-150.464 126.016-336 126.016s-336-56.448-336-126.016l0-104.64c57.856 59.648 197.376 90.624 336 90.624s278.144-30.976 336-90.624L848 777.984zM848 609.984l-0.128 0c0 0.256 0.128 0.64 0.128 0.896 0 69.12-150.464 125.12-336 125.12s-336-56-336-125.12c0-0.256 0.128-0.64 0.128-0.896L176 609.984 176 505.344C233.856 564.992 373.376 595.968 512 595.968s278.144-30.976 336-90.624L848 609.984zM848 441.984l-0.128 0c0 0.256 0.128 0.64 0.128 0.896C848 512 697.536 568 512 568S176 512 176 442.88c0-0.256 0.128-0.64 0.128-0.896L176 441.984 176 345.792C249.344 401.664 383.872 428.032 512 428.032s262.656-26.304 336-82.176L848 441.984zM512 371.968c-185.536 0-336-56.448-336-126.016 0-69.632 150.464-126.016 336-126.016s336 56.384 336 126.016C848 315.584 697.536 371.968 512 371.968zM764.032 764.032c15.488 0 28.032 12.48 28.032 28.032s-12.48 28.032-28.032 28.032S736 807.488 736 792 748.48 764.032 764.032 764.032zM764.032 595.968c15.488 0 28.032 12.48 28.032 28.032s-12.48 28.032-28.032 28.032S736 639.488 736 624 748.48 595.968 764.032 595.968zM764.032 428.032c15.488 0 28.032 12.48 28.032 28.032s-12.48 28.032-28.032 28.032S736 471.488 736 456 748.48 428.032 764.032 428.032z" ></path></symbol><symbol id="icon-yonghu-yuan" viewBox="0 0 1024 1024"><path d="M511.333 63.333c-247.424 0-448 200.576-448 448s200.576 448 448 448 448-200.576 448-448-200.576-448-448-448z m0 832c-51.868 0-102.15-10.144-149.451-30.15-36.011-15.231-69.123-35.67-98.812-60.897 12.177-31.985 42.226-63.875 84.223-88.903C396.189 686.243 456.222 669.53 512 669.53c55.631 0 115.416 16.658 164.026 45.703 41.762 24.953 71.689 56.812 83.863 88.804-29.764 25.342-62.976 45.865-99.106 61.146-47.299 20.006-97.582 30.15-149.45 30.15z m296.268-139.658c-20.493-35.937-54.353-68.855-98.747-95.381C649.75 624.979 579.839 605.53 512 605.53c-67.964 0-138.094 19.488-197.471 54.875-44.644 26.606-78.656 59.594-99.195 95.586-23.835-28.755-43.234-60.652-57.85-95.208-20.006-47.3-30.15-97.583-30.15-149.451s10.144-102.15 30.15-149.451c19.337-45.719 47.034-86.792 82.321-122.078 35.286-35.287 76.359-62.983 122.078-82.321 47.3-20.006 97.583-30.15 149.451-30.15 51.868 0 102.15 10.144 149.451 30.15 45.719 19.337 86.792 47.034 122.078 82.321 35.287 35.286 62.983 76.359 82.321 122.078 20.006 47.3 30.15 97.583 30.15 149.451s-10.144 102.15-30.15 149.451c-14.563 34.429-33.869 66.22-57.583 94.892z" ></path><path d="M512 220.223c-88.224 0-160 71.776-160 160s71.776 160 160 160c88.225 0 160-71.775 160-160s-71.775-160-160-160z m0 256c-52.935 0-96-43.065-96-96s43.065-96 96-96 96 43.065 96 96-43.065 96-96 96z" ></path></symbol><symbol id="icon-tongzhi" viewBox="0 0 1024 1024"><path d="M870.4 844.8h-204.8a153.6 153.6 0 0 1-307.2 0H153.6a51.2 51.2 0 0 1-51.2-51.2v-51.2h102.4v-307.2a307.2 307.2 0 0 1 256-302.08V76.8a51.2 51.2 0 0 1 102.4 0v56.32a307.2 307.2 0 0 1 256 302.08v307.2h102.4v51.2a51.2 51.2 0 0 1-51.2 51.2z m-358.4 51.2a51.2 51.2 0 0 0 51.2-51.2H460.8a51.2 51.2 0 0 0 51.2 51.2z m204.8-460.8a204.8 204.8 0 0 0-204.8-204.8 204.8 204.8 0 0 0-204.8 204.8v307.2h409.6z" ></path></symbol><symbol id="icon-moxing" viewBox="0 0 1024 1024"><path d="M910.13 243.22L545.97 32.97c-19.82-11.46-44.41-11.4-64.16 0.13L115.54 246.51c-19.5 11.36-31.68 32.43-31.76 54.99L82.1 725.44c-0.08 22.87 12.16 44.16 31.97 55.6l364.16 210.25c9.86 5.7 20.92 8.55 31.97 8.55 11.13 0 22.27-2.89 32.19-8.67l366.27-213.41c19.5-11.36 31.66-32.43 31.75-54.99l1.69-423.93c0.08-22.88-12.16-44.18-31.97-55.62zM513.68 88.9l335.28 193.58-332.93 192.2c-1.38 0.8-2.63 1.76-3.94 2.64-1.32-0.88-2.56-1.85-3.94-2.64L178.66 284.46 513.68 88.9zM146.69 725.68l1.24-384.39 327.91 189.32c1.59 0.92 2.74 2.31 3.54 3.89-0.09 1.49-0.29 2.95-0.28 4.45l0.7 175.55-0.8 202.69-332.31-191.51z m398.5 189.44l-0.8-200.61 0.7-175.54c0.01-1.5-0.2-2.97-0.28-4.46 0.8-1.59 1.95-2.98 3.53-3.9l329.03-189.96-1.23 381.29-330.95 193.18z" ></path></symbol></svg>',function(e){var t=(t=document.getElementsByTagName("script"))[t.length-1],c=t.getAttribute("data-injectcss"),t=t.getAttribute("data-disable-injectsvg");if(!t){var a,o,n,i,s,l=function(t,c){c.parentNode.insertBefore(t,c)};if(c&&!e.__iconfont__svg__cssinject__){e.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}a=function(){var t,c=document.createElement("div");c.innerHTML=e._iconfont_svg_string_3913924,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(t=document.body).firstChild?l(c,t.firstChild):t.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(a,0):(o=function(){document.removeEventListener("DOMContentLoaded",o,!1),a()},document.addEventListener("DOMContentLoaded",o,!1)):document.attachEvent&&(n=a,i=e.document,s=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,h())})}function h(){s||(s=!0,n())}function d(){try{i.documentElement.doScroll("left")}catch(t){return void setTimeout(d,50)}h()}}(window);
\ No newline at end of file
<svg viewBox="0 0 1024 1024" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" id="图层_1"><defs><style>.cls-1{fill:url(#未命名的渐变_46);}.cls-2{fill:url(#未命名的渐变_46-2);}.cls-3{fill:url(#未命名的渐变_59);}.cls-4{fill:url(#未命名的渐变_46-3);}.cls-5{fill:url(#未命名的渐变_61);}.cls-6{fill:url(#未命名的渐变_46-4);}.cls-7{fill:url(#未命名的渐变_59-2);}.cls-8{fill:url(#未命名的渐变_61-2);}.cls-9{fill:url(#未命名的渐变_46-5);}.cls-10{fill:url(#未命名的渐变_46-6);}.cls-11{fill:url(#未命名的渐变_37);}.cls-12{fill:url(#未命名的渐变_37-2);}.cls-13{fill:url(#未命名的渐变_37-3);}.cls-14{fill:url(#未命名的渐变_37-4);}.cls-15{fill:url(#未命名的渐变_37-5);}.cls-16{fill:url(#未命名的渐变_37-6);}.cls-17{fill:url(#未命名的渐变_46-7);}</style><linearGradient gradientUnits="userSpaceOnUse" y2="473.26" x2="878.62" y1="655.89" x1="807.82" id="未命名的渐变_46"><stop stop-color="#5496e8" offset="0"></stop><stop stop-color="#3462bf" offset="1"></stop></linearGradient><linearGradient xlink:href="#未命名的渐变_46" y2="490.59" x2="619.58" y1="586.01" x1="571.36" id="未命名的渐变_46-2"></linearGradient><linearGradient gradientUnits="userSpaceOnUse" y2="540.54" x2="784.89" y1="749.85" x1="340.28" id="未命名的渐变_59"><stop stop-color="#35a2e6" offset="0"></stop><stop stop-color="#2256bd" offset="1"></stop></linearGradient><linearGradient xlink:href="#未命名的渐变_46" y2="634.74" x2="555.72" y1="708.62" x1="417.21" id="未命名的渐变_46-3"></linearGradient><linearGradient gradientUnits="userSpaceOnUse" y2="347.11" x2="586.71" y1="363.53" x1="748.14" id="未命名的渐变_61"><stop stop-color="#5496e8" offset="0"></stop><stop stop-color="#2a56b0" offset="1"></stop></linearGradient><linearGradient xlink:href="#未命名的渐变_46" y2="509.85" x2="404.48" y1="622.02" x1="373.02" id="未命名的渐变_46-4"></linearGradient><linearGradient xlink:href="#未命名的渐变_59" y2="179.17" x2="550.55" y1="227.77" x1="501.24" id="未命名的渐变_59-2"></linearGradient><linearGradient xlink:href="#未命名的渐变_61" y2="282.77" x2="492.74" y1="354.59" x1="459.22" id="未命名的渐变_61-2"></linearGradient><linearGradient xlink:href="#未命名的渐变_46" y2="88.18" x2="508.63" y1="279.71" x1="478.54" id="未命名的渐变_46-5"></linearGradient><linearGradient xlink:href="#未命名的渐变_46" y2="352.71" x2="464.27" y1="815.1" x1="231.7" id="未命名的渐变_46-6"></linearGradient><linearGradient gradientUnits="userSpaceOnUse" y2="401.04" x2="1033.08" y1="1071.37" x1="598.05" id="未命名的渐变_37"><stop stop-color="#8ffbff" offset="0"></stop><stop stop-color="#4a83ff" offset="1"></stop></linearGradient><linearGradient xlink:href="#未命名的渐变_37" y2="282.69" x2="850.73" y1="953.03" x1="415.7" id="未命名的渐变_37-2"></linearGradient><linearGradient xlink:href="#未命名的渐变_37" y2="274.94" x2="838.77" y1="945.27" x1="403.74" id="未命名的渐变_37-3"></linearGradient><linearGradient xlink:href="#未命名的渐变_37" y2="249.95" x2="800.28" y1="920.28" x1="365.24" id="未命名的渐变_37-4"></linearGradient><linearGradient xlink:href="#未命名的渐变_37" y2="194.17" x2="714.32" y1="864.5" x1="279.29" id="未命名的渐变_37-5"></linearGradient><linearGradient xlink:href="#未命名的渐变_37" y2="109.99" x2="584.6" y1="780.32" x1="149.57" id="未命名的渐变_37-6"></linearGradient><linearGradient xlink:href="#未命名的渐变_46" y2="558.04" x2="166" y1="775.55" x1="110.6" id="未命名的渐变_46-7"></linearGradient></defs><title>2.5D</title><path d="M874.19,517.9l.07-.16a48,48,0,0,1,7.53-13.13l.5-.63A44.59,44.59,0,0,1,893,494.62c.41-.26.84-.49,1.26-.73a37.5,37.5,0,0,1,13.38-4.81,32.39,32.39,0,0,1,11.66.59,30.26,30.26,0,0,1,7.92,2.76l-89.8-48.85c-.37-.2-.74-.39-1.11-.58a30.1,30.1,0,0,0-5.77-2.13c-.32-.08-.71,0-1-.05a32.21,32.21,0,0,0-11.66-.59,36.81,36.81,0,0,0-10.54,3.17c-1,.46-1.9,1.1-2.85,1.64-.43.24-.84.46-1.25.73-.58.36-1.21.63-1.77,1a43.66,43.66,0,0,0-4.39,3.47,45.81,45.81,0,0,0-3.71,3.75c-.31.36-.58.76-.89,1.13-.17.21-.32.41-.49.62-.64.8-1.31,1.57-1.9,2.41a47.14,47.14,0,0,0-5.64,10.74l-.06.14,0,.11a45.65,45.65,0,0,0-2.83,15.64l-.24,131.71c0,14.11,6.75,25.49,16.93,31L888,696.38c-10.18-5.54-17-16.92-16.93-31l.24-131.71A45.61,45.61,0,0,1,874.19,517.9Z" class="cls-1"></path><path d="M613.08,563a68.2,68.2,0,0,1,2-7.16c.18-.5.46-1,.65-1.45a68.93,68.93,0,0,1,3.93-8.34c.42-.74.9-1.45,1.34-2.18a69.61,69.61,0,0,1,4.62-6.67c.5-.64,1-1.28,1.49-1.9a66.92,66.92,0,0,1,6.87-7.07c.5-.44,1.05-.81,1.56-1.24a62.68,62.68,0,0,1,6.22-4.51c1.24-.79,2.52-1.5,3.82-2.2,1.47-.8,3-1.49,4.52-2.15A53.5,53.5,0,0,1,663.89,514a46.57,46.57,0,0,1,13.25.2c.87.13,1.75.22,2.6.4a43.58,43.58,0,0,1,12.09,4.23L602,469.95q-.8-.43-1.61-.83a43.17,43.17,0,0,0-8.3-3.07c-.7-.18-1.47-.19-2.18-.33s-1.72-.27-2.59-.4a46.4,46.4,0,0,0-13.26-.2,53.55,53.55,0,0,0-13.79,4.15c-.53.23-1.14.25-1.66.5-1,.47-1.9,1.13-2.87,1.65-1.29.7-2.56,1.4-3.81,2.19-.62.4-1.31.68-1.92,1.11-1.5,1-2.9,2.24-4.32,3.42-.51.42-1.05.79-1.55,1.23-.17.15-.35.26-.52.41a66.57,66.57,0,0,0-5.39,5.45c-.34.38-.62.82-1,1.21-.52.62-1,1.26-1.49,1.9-.77,1-1.6,1.91-2.31,2.93-.84,1.19-1.54,2.49-2.3,3.74-.44.73-.92,1.43-1.34,2.18-.21.37-.46.7-.66,1.07a68.64,68.64,0,0,0-3.31,7.35c-.18.46-.44.89-.61,1.36,0,.06-.06.12-.08.18a67.57,67.57,0,0,0-2,7.46c-.28,1.16-.72,2.26-.94,3.43a64.26,64.26,0,0,0-1.09,11.57,54.1,54.1,0,0,0,6.69,26.76,45.3,45.3,0,0,0,17.78,17.71l89.8,48.85c-14.75-8-24.51-24.39-24.48-44.48A64.15,64.15,0,0,1,612,566.9C612.26,565.56,612.76,564.3,613.08,563Z" class="cls-2"></path><path d="M766,687.29c.12-.34.16-.71.29-1A223.94,223.94,0,0,0,776,650.8c.05-.28.07-.56.12-.84a213.87,213.87,0,0,0,3.5-37.69c.13-67.83-32.77-122.94-82.29-149.87l-89.8-48.85c49.51,26.93,82.41,82,82.29,149.87a214,214,0,0,1-3.5,37.74c0,.26-.06.53-.11.78a224,224,0,0,1-9.72,35.46c-.18.49-.3,1-.48,1.49a230.45,230.45,0,0,1-12,26.6c-.88,1.69-1.73,3.39-2.65,5.05a233.55,233.55,0,0,1-12.79,20.73c-.61.87-1.29,1.67-1.91,2.53q-5.89,8.2-12.45,15.8c-1.49,1.74-3,3.43-4.58,5.13q-6.11,6.69-12.7,12.83c-1.31,1.23-2.58,2.52-3.92,3.72a214.75,214.75,0,0,1-18.81,14.88c-1.26.89-2.57,1.69-3.85,2.55a200.5,200.5,0,0,1-26,15c-.68.32-1.38.55-2.06.86a185.55,185.55,0,0,1-20.36,7.83c-2,.64-4,1.26-6,1.83a174.34,174.34,0,0,1-23.47,5.18l-91.56,12.87a161.8,161.8,0,0,1-28.77,1.28c-3-.11-5.83-.56-8.75-.84a150.88,150.88,0,0,1-18.76-2.87c-3.09-.7-6.12-1.51-9.13-2.39a145.43,145.43,0,0,1-18-6.64c-3.57-1.59-7.29-2.89-10.71-4.75l89.8,48.85q2.66,1.45,5.39,2.79c1.69.83,3.6,1.2,5.32,2a144.81,144.81,0,0,0,18,6.64c1.58.46,3,1.29,4.6,1.7s3.06.36,4.53.69a150.51,150.51,0,0,0,18.76,2.87c2.93.28,5.77.72,8.75.84a161.65,161.65,0,0,0,28.78-1.28l91.56-12.87a174.28,174.28,0,0,0,23.46-5.18c2-.57,4-1.19,6-1.83a185.6,185.6,0,0,0,20.38-7.84c.55-.25,1.13-.39,1.67-.64.13-.06.24-.15.36-.21a199.65,199.65,0,0,0,26.05-15c.75-.5,1.59-.83,2.33-1.34.53-.36,1-.82,1.51-1.19a214.42,214.42,0,0,0,18.83-14.9c.36-.32.76-.57,1.12-.88,1-.88,1.82-1.91,2.78-2.8q6.6-6.15,12.72-12.84c.84-.92,1.8-1.72,2.63-2.66.69-.78,1.26-1.68,1.94-2.47q6.54-7.61,12.45-15.8c.5-.7,1.11-1.33,1.6-2,.11-.16.19-.33.3-.49a233.55,233.55,0,0,0,12.79-20.73c.42-.77,1-1.46,1.38-2.24s.79-1.91,1.27-2.82a230.91,230.91,0,0,0,12-26.6C765.85,687.58,765.93,687.44,766,687.29Z" class="cls-3"></path><path d="M633.5,672.12l-89.8-48.85-.89-.46a24.1,24.1,0,0,0-4.63-1.71c-.25-.07-.56,0-.82,0a25.69,25.69,0,0,0-9.31-.48L406,637.73a30,30,0,0,0-8.58,2.58c-.75.35-1.44.84-2.17,1.25-.35.2-.69.38-1,.6-.51.31-1.06.55-1.55.89a35.48,35.48,0,0,0-3.52,2.79,36.72,36.72,0,0,0-3,3c-.22.25-.4.53-.61.78l-.36.46c-.56.69-1.13,1.35-1.64,2.08a36.62,36.62,0,0,0-4.41,8.38c0,.07-.07.13-.09.2l-.06.14a36.33,36.33,0,0,0-2.25,12.47c0,11.17,5.43,20.27,13.61,24.72l89.8,48.85c-8.18-4.45-13.63-13.55-13.61-24.72a36.39,36.39,0,0,1,2.32-12.61l.08-.18a38,38,0,0,1,6-10.47l.36-.46A35.79,35.79,0,0,1,484,691c.34-.21.69-.4,1-.6a30.47,30.47,0,0,1,10.75-3.83l122-17.15a25.87,25.87,0,0,1,9.31.48A24.29,24.29,0,0,1,633.5,672.12Z" class="cls-4"></path><path d="M642,365.21a217.11,217.11,0,0,1,29.95,2.87c1.75.3,3.45.72,5.19,1.06A208.62,208.62,0,0,1,703.14,376c1.41.47,2.87.84,4.27,1.33a205.92,205.92,0,0,1,29,12.89l-89.8-48.85q-3.78-2.06-7.64-4a206.15,206.15,0,0,0-21.4-8.94c-1.4-.5-2.86-.86-4.27-1.33-4.6-1.53-9.16-3.13-13.9-4.34-4-1-8.09-1.7-12.16-2.49-1.74-.34-3.43-.77-5.19-1.06a216.49,216.49,0,0,0-29.94-2.87c-2.15-.06-4.31,0-6.47,0a230.81,230.81,0,0,0-32.35,2l-9.23,1.3,89.8,48.85,9.23-1.3a231.12,231.12,0,0,1,32.35-2C637.64,365.18,639.81,365.15,642,365.21Z" class="cls-5"></path><path d="M399.8,592.92a68,68,0,0,1,2-7.14c.16-.44.4-.84.57-1.27a69.26,69.26,0,0,1,4-8.52c.41-.74.89-1.43,1.32-2.15a69.67,69.67,0,0,1,4.61-6.69c.5-.64,1-1.29,1.5-1.91a67,67,0,0,1,6.86-7.08c.47-.41,1-.76,1.46-1.15a62.59,62.59,0,0,1,6.3-4.59c1.22-.79,2.49-1.48,3.76-2.17,1.49-.81,3-1.51,4.57-2.18A52.91,52.91,0,0,1,450.33,544a47.23,47.23,0,0,1,13.42.19c.88.13,1.76.23,2.62.4a43.73,43.73,0,0,1,12.2,4.26L388.77,500q-.8-.43-1.61-.83a43.61,43.61,0,0,0-8.36-3.09c-.72-.18-1.5-.19-2.23-.34s-1.73-.27-2.62-.4a47.07,47.07,0,0,0-13.42-.19,52.78,52.78,0,0,0-13.59,4.09c-.53.23-1.12.25-1.65.49-1,.48-1.94,1.15-2.93,1.69-1.27.69-2.53,1.38-3.75,2.16-.6.38-1.26.66-1.85,1.07-1.55,1.07-3,2.31-4.46,3.53-.48.4-1,.74-1.45,1.15-.15.13-.31.23-.46.36a66.52,66.52,0,0,0-5.39,5.45c-.35.4-.65.86-1,1.27-.52.62-1,1.27-1.5,1.91-.75,1-1.56,1.87-2.26,2.87-.86,1.22-1.57,2.54-2.35,3.82-.44.72-.91,1.41-1.32,2.15-.2.36-.45.68-.64,1a69.26,69.26,0,0,0-3.37,7.48c-.17.43-.41.83-.57,1.27l-.07.17a68.41,68.41,0,0,0-1.91,7c-.33,1.33-.83,2.6-1.08,4a64.34,64.34,0,0,0-1.09,11.56c0,15.1,5.48,28.1,14.45,37a44.1,44.1,0,0,0,10,7.51L422.13,653c-14.73-8-24.54-24.4-24.5-44.54a64.34,64.34,0,0,1,1.09-11.56C399,595.52,399.46,594.25,399.8,592.92Z" class="cls-6"></path><path d="M595.12,247.9a27.73,27.73,0,0,0,1.73-9.48c0-8.95-4.25-16.14-10.68-19.64L461,150.72c6.44,3.5,10.7,10.69,10.68,19.64a27.66,27.66,0,0,1-1.81,9.67c0,.07-.07.15-.1.22a30,30,0,0,1-4.73,8.13l-.27.34a28.4,28.4,0,0,1-6.82,5.88c-.27.17-.54.32-.82.47a24,24,0,0,1-8.5,3,20.06,20.06,0,0,1-12.5-2.14L561.29,264l.71.37a19.23,19.23,0,0,0,3.68,1.36,20.83,20.83,0,0,0,8.11.41,23.62,23.62,0,0,0,6.76-2c.61-.28,1.16-.67,1.75-1,.28-.16.54-.3.81-.47s.82-.43,1.2-.69a27.84,27.84,0,0,0,2.79-2.21,29.12,29.12,0,0,0,2.35-2.38c.17-.19.31-.4.47-.6s.18-.24.28-.36c.45-.55.91-1.09,1.32-1.67a28.18,28.18,0,0,0,3.39-6.42c0-.08.08-.16.11-.25S595.09,248,595.12,247.9Z" class="cls-7"></path><polygon points="431.13 258.66 430.99 329.93 520.8 378.78 520.93 307.51 431.13 258.66" class="cls-8"></polygon><path d="M597.89,158.31l-.08,0,.08,0-89.8-48.85q-1.13-.61-2.29-1.18a61.88,61.88,0,0,0-11.87-4.38c-1-.26-2.13-.27-3.17-.48-1.23-.25-2.47-.39-3.73-.57a66.86,66.86,0,0,0-19-.27,76.16,76.16,0,0,0-19.65,5.91c-.76.33-1.62.36-2.36.71-1.37.65-2.62,1.56-4,2.28-1.86,1-3.69,2-5.47,3.15-.91.58-1.9,1-2.79,1.61-2.05,1.42-4,3.07-5.89,4.66-.77.64-1.59,1.19-2.35,1.86-.27.23-.57.42-.83.66a94.92,94.92,0,0,0-7.65,7.72c-.45.51-.83,1.1-1.27,1.62-.72.85-1.36,1.73-2.05,2.61-1.14,1.44-2.35,2.82-3.41,4.33s-2.13,3.44-3.18,5.16c-.64,1-1.31,2-1.91,3.1-.31.55-.69,1-1,1.6a97.58,97.58,0,0,0-4.52,10c-.31.77-.72,1.47-1,2.25,0,.1-.09.19-.13.29a96.34,96.34,0,0,0-2.75,10c-.45,1.82-1.13,3.55-1.48,5.39A91.21,91.21,0,0,0,392.8,194c-.05,28.52,13.9,51.51,34.79,62.87l89.8,48.85c-20.9-11.37-34.84-34.35-34.79-62.87a91.32,91.32,0,0,1,1.55-16.46c.32-1.75,1-3.39,1.4-5.11a95.69,95.69,0,0,1,3-10.6c.29-.78.71-1.49,1-2.26A97.46,97.46,0,0,1,495,196.81c.6-1.07,1.29-2.08,1.93-3.13a97.92,97.92,0,0,1,6.57-9.46c.69-.87,1.35-1.78,2.07-2.63a94.69,94.69,0,0,1,9.75-10c.76-.67,1.58-1.23,2.35-1.87a89.15,89.15,0,0,1,8.67-6.26c1.78-1.14,3.62-2.16,5.47-3.16,2.06-1.11,4.19-2.07,6.33-3a76.25,76.25,0,0,1,19.64-5.91,67,67,0,0,1,19,.27c1.25.18,2.5.32,3.73.57A62.18,62.18,0,0,1,597.89,158.31Z" class="cls-9"></path><path d="M263.77,648.58c.75-6,1.44-12.09,2.54-18,.67-3.59,1.65-7.1,2.44-10.66,1.18-5.3,2.24-10.63,3.69-15.84,1-3.75,2.38-7.39,3.55-11.09,1.53-4.83,3-9.71,4.71-14.45a326.25,326.25,0,0,1,16.92-37.59c1.23-2.37,2.42-4.76,3.72-7.1a329.68,329.68,0,0,1,18.06-29.28c.88-1.27,1.88-2.43,2.78-3.69q8.27-11.5,17.47-22.18c2.12-2.46,4.29-4.87,6.48-7.26q8.63-9.46,18-18.14c1.83-1.71,3.59-3.51,5.46-5.18a304,304,0,0,1,26.57-21c1.78-1.25,3.63-2.38,5.43-3.59a282.22,282.22,0,0,1,36.76-21.2c.94-.44,1.93-.77,2.88-1.21A262.85,262.85,0,0,1,470,390c2.79-.9,5.6-1.77,8.42-2.58a246.28,246.28,0,0,1,33.17-7.32l9.23-1.3L431,329.93l-9.23,1.3a246.35,246.35,0,0,0-33.17,7.32c-2.83.81-5.62,1.67-8.42,2.58a262.44,262.44,0,0,0-28.79,11.07c-.77.35-1.6.55-2.36.91-.18.08-.33.21-.51.3a281.63,281.63,0,0,0-36.76,21.2c-1.06.71-2.24,1.17-3.29,1.9-.75.52-1.4,1.17-2.14,1.7a302.65,302.65,0,0,0-26.56,21c-.5.44-1.07.79-1.57,1.24-1.38,1.24-2.57,2.7-3.93,4q-9.3,8.68-17.93,18.11c-1.19,1.3-2.54,2.43-3.71,3.75-1,1.11-1.8,2.39-2.77,3.52q-9.16,10.66-17.43,22.12c-.75,1-1.64,2-2.37,3-.17.24-.29.5-.46.74a329.93,329.93,0,0,0-18,29.26c-.59,1.07-1.35,2-1.93,3.13-.69,1.28-1.12,2.69-1.79,4a326,326,0,0,0-16.92,37.59c-.08.2-.19.39-.27.6-1.67,4.55-3,9.24-4.45,13.88-1.17,3.7-2.51,7.32-3.55,11.07-1.44,5.22-2.51,10.54-3.69,15.85-.79,3.56-1.78,7.07-2.44,10.66-1.07,5.76-1.72,11.62-2.46,17.46-.41,3.22-1,6.4-1.34,9.64q-1.28,13.65-1.33,27.49c-.18,95.89,46.18,173.62,116,211.62l89.8,48.85c-69.85-38-116.2-115.73-116-211.62q0-13.84,1.33-27.51C262.81,654.58,263.4,651.6,263.77,648.58Z" class="cls-10"></path><path d="M907.68,489.08c-20,2.81-36.33,22.7-36.37,44.56l-.24,131.71c0,21.86,16.23,37.17,36.22,34.36,20.25-2.85,36.59-22.74,36.63-44.6l.24-131.71C944.21,501.54,927.93,486.23,907.68,489.08Z" class="cls-11"></path><path d="M663.89,514c-29.19,4.1-52.91,33-53,64.49-.06,31.23,23.56,53.45,52.76,49.35,28.93-4.07,52.65-32.94,52.71-64.17C716.45,532.13,692.82,509.91,663.89,514Z" class="cls-12"></path><path d="M617.85,669.43l-122,17.15c-16,2.25-29.22,18-29.25,35.61,0,17.32,13.09,29.66,29.14,27.41l122-17.15c16-2.26,29.22-18.3,29.25-35.61C647,679.24,633.9,667.18,617.85,669.43Z" class="cls-13"></path><path d="M603.13,367.21l-9.23,1.3.13-71.27c22.89-17,38.53-45.36,38.58-75.46.08-44.93-33.58-76.16-74.8-70.36s-75.12,46.52-75.21,91.45c-.06,30.1,15.48,54,38.32,64.65l-.13,71.27-9.23,1.3c-137.81,19.37-250.1,156.3-250.38,305S372.76,939.07,510.57,919.7l91.56-12.87c137.81-19.37,250.31-156,250.58-304.85S740.94,347.84,603.13,367.21ZM557.72,208.43c12.18-1.71,22,7.54,22,20.84,0,12.85-9.9,24.87-22.08,26.59s-22.15-7.52-22.13-20.37C535.53,222.19,545.41,210.16,557.72,208.43Zm44.55,619.82-91.56,12.87c-97.51,13.7-176.51-60.94-176.31-166.3s79.51-202.14,177-215.84L603,446.12C700.5,432.41,779.8,507,779.61,612.26S699.79,814.55,602.28,828.26Z" class="cls-14"></path><path d="M503.09,593.61c.06-31.51-23.56-53.73-52.76-49.63-28.93,4.07-52.65,32.94-52.71,64.45-.06,31.23,23.56,53.45,52.5,49.38C479.32,653.71,503,624.84,503.09,593.61Z" class="cls-15"></path><path d="M206.51,587.62c-20.25,2.85-36.59,22.74-36.63,44.6l-.24,131.71c0,21.86,16.23,37.17,36.49,34.32,20-2.81,36.59-22.74,36.63-44.6L243,621.94C243,600.08,226.5,584.81,206.51,587.62Z" class="cls-16"></path><path d="M172.77,616.48c0-.06.06-.12.08-.18a47.78,47.78,0,0,1,7.55-13.13c.16-.2.32-.41.48-.61a44.76,44.76,0,0,1,10.82-9.37c.42-.26.86-.5,1.29-.75a38.15,38.15,0,0,1,13.53-4.83,31.94,31.94,0,0,1,11.55.6A30.43,30.43,0,0,1,226,591l-89.8-48.85c-.37-.2-.74-.39-1.12-.58a30.14,30.14,0,0,0-5.8-2.14c-.31-.08-.69,0-1-.05a31.84,31.84,0,0,0-11.55-.6A37.6,37.6,0,0,0,106,542c-1,.45-1.85,1.07-2.78,1.6-.44.25-.86.47-1.28.74-.62.38-1.28.67-1.87,1.08a44,44,0,0,0-4.4,3.48,45.86,45.86,0,0,0-3.71,3.75c-.29.33-.55.71-.83,1.05s-.32.41-.48.62c-.66.82-1.35,1.62-2,2.48a47.86,47.86,0,0,0-3,4.81A48.58,48.58,0,0,0,83,567.45c0,.06-.06.11-.08.18l-.06.12a45.54,45.54,0,0,0-2.83,15.61l-.24,131.71a40.62,40.62,0,0,0,1.2,10,32.43,32.43,0,0,0,15.71,21l89.8,48.85c-10.2-5.55-16.93-16.9-16.91-31l.24-131.71A45.53,45.53,0,0,1,172.77,616.48Z" class="cls-17"></path></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>
\ No newline at end of file
{"prompt": "什么是高潜公司\n\n###\n\n", "completion": " 高潜公司是指高潜力的公司,其所处的赛道有发展潜力和前景。###"}
{"prompt": "高潜公司的优势是什么\n\n###\n\n", "completion": " 高潜公司所处的整个行业和赛道有一定的增量空间,头部vc通过严格的筛选和加持为高潜公司背书,高潜公司的创始者一般都特别优秀。加入高潜公司可以获得很多机会来锻炼自己、强化自己的认知,快速成长、展现自己,并有可能成为公司扩张后的管理层###"}
{"prompt": "高潜公司的风险是什么\n\n###\n\n", "completion": " 高潜公司的高风险问题也很明显。一旦公司失败,很多边缘人就会被淘汰掉。但宏观来讲,只要赛道是有潜力的,即便这家公司失败了,也可以跳槽到成功的公司去。###"}
{"prompt": "高潜公司的例子有哪些\n\n###\n\n", "completion": " 很多高潜公司在做最近比较火的AIGC,包括去年的元宇宙,前两年比较火的新消费、企业服务和出海之类的。###"}
{"prompt": "为什么现在加入大厂的前景不广阔?\n\n###\n\n", "completion": " 今年的大形势之下,互联网大厂纷纷开始裁员、取消普调、晋升比例严重缩水,HC也急剧减少。###"}
{"prompt": "加入高潜公司可以获得什么样的机会?\n\n###\n\n", "completion": " 加入高潜公司可以获得很多机会来锻炼自己、强化自己的认知,快速成长、展现自己,并有可能成为公司扩张后的管理层。###"}
{"prompt": "什么是高潜公司?\n\n###\n\n", "completion": " 高潜公司的定义是“高潜力的公司”,所处的行业和赛道有发展潜力和前景,头部vc为其背书,创始者一般都特别优秀。###"}
{"prompt": "加入高潜公司有哪些优点\n\n###\n\n", "completion": " 加入高潜公司能够获得锻炼自己、强化认知的机会,快速成长、展现自己,在公司扩张后成为团队的管理层。同时,所处的行业还处于上升期,发展前景广阔。###"}
{"prompt": "高潜公司存在哪些风险?\n\n###\n\n", "completion": " 高潜公司存在高风险问题,一旦公司失败,很多边缘人就会被淘汰掉。但宏观来讲,只要赛道是有潜力的,即便这家公司失败了,也可以跳槽到成功的公司去。###"}
{"prompt": "高潜公司和互联网有什么相似之处?\n\n###\n\n", "completion": " 目前来说,高潜公司的风格大部分都比较像互联网大厂,有风格,但又没有那么内卷,因为大家每天都在开拓边疆,突破和探索市场的边界。同时,互联网产业的蓬勃发展也给其他行业,包括新能源汽车、新消费等,带来了用户增长和运营的经验###"}
{"prompt": "互联网大厂的制度和分工如何?\n\n###\n\n", "completion": " 目前的互联网大厂的制度和分工非常明确,基本是按照一些标准SOP做事。同时,由于已经没有用户增量了,内卷和向上管理的问题也比较严重。###"}
{"prompt": "高潜公司对于哪些毕业生是很好的选择\n\n###\n\n", "completion": " 对于追求长期创造价值而非想找份安稳的工作的毕业生来说,高潜公司是很好的选择。###"}
{"prompt": "互联网大厂的晋升方式有哪些?\n\n###\n\n", "completion": " 互联网大厂的晋升方式有两种:老板特别喜欢你或者你的战功支撑你的竞争###"}
{"prompt": "为什么目前更多优秀的人都会加入创业公司?\n\n###\n\n", "completion": " 目前社会的大趋势是更多优秀的人都会加入创业公司,因为真正在创造价值的其实永远是创业公司###"}
{"prompt": "为什么选择高潜公司主要是因为我们迎来了什么三个繁荣?\n\n###\n\n", "completion": " 选择高潜公司主要是因为我们迎来了创新生态的三个繁荣:人才繁荣、资本繁荣和环境繁荣###"}
{"prompt": "资本繁荣是如何推动创业生态的崛起的?\n\n###\n\n", "completion": " 资本繁荣,无论中国还是美国,创业生态的崛起都是伴随着移动互联网的发展。中国这一代VC的崛起,主要是通过投资移动互联网项目所积累的战绩扩大了资金池,通过这些成本低、增长快、回报率高的项目,才有了底气去推动更多行业发展,尝试着投资toB和硬科技这类成本高、增长慢、回报率低、风险大的项目。###"}
{"prompt": "环境繁荣是指什么?\n\n###\n\n", "completion": " 环境繁荣是指互联网带动起经济发展后,各地政府也开始了对于创业进行培育,免费场地、巨额无偿补贴、各地送钱竞赛,大幅度降低了创业成本。###"}
{"prompt": "高潜公司对于追求长期创造价值的毕业生来说为什么是好的选择?\n\n###\n\n", "completion": " 因为在高潜公司,毕业生可以通过参与创业公司的发展,获得更多的机会和成长空间,而不是被固定在某个职位上。此外,高潜公司通常会注重团队建设和文化建设,能够提供更好的工作环境和发展机会。###"}
{"prompt": "如果想要寻找高潜公司,有哪些渠道可以利用?\n\n###\n\n", "completion": " 可以利用领英职场app和知潜微信小程序。领英职场app中入驻了很多高潜公司及高管,而知潜小程序现在也已经有100+高潜力创业公司入驻,都是经过筛选的、top VC投资过且赛道有前景的公司。此外,红杉和真格等VC机构自己的公众号也会经常披露投资的企业。###"}
{"prompt": "为什么现在更多优秀的人才会加入创业公司?\n\n###\n\n", "completion": " 因为在当前社会的大趋势下,创业公司成为了创新生态的重要组成部分。创业公司在创造价值和推动行业发展方面具有更大的空间和潜力,而且随着移动互联网的发展,创业公司的崛起也得到了资本和政策的支持。因此,更多优秀的人才选择加入创业公司,以实现自己的创业梦想和获得更好的发展机会。###"}
{"prompt": "高潜公司的优势在哪些方面?\n\n###\n\n", "completion": " 高潜公司在团队建设、文化建设和发展机会等方面具有优势。高潜公司通常会注重团队建设和文化建设,能够提供更好的工作环境和发展机会。同时,高潜公司的发展潜力和创新能力也很强,可以为毕业生提供更多的机会和成长空间。###"}
\ No newline at end of file
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"}
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"}
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"}
{"prompt":"什么是lru算法?\n\n###\n\n","completion":" lru算法是一种页面置换算法,它的全称是least recently used,即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录,当需要替换一页时,选择最近最少被访问的页面进行替换。###"}
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"}
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"}
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"}
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"}
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数,它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"}
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"}
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"}
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"}
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"}
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"}
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"}
{"prompt":"什么是线程?\n\n###\n\n","completion":" 线程是操作系统能够进行运算调度的最小单位,它是进程的一个执行流程,共享进程的地址空间。###"}
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是一种计算机系统内存管理技术,它使得应用程序认为它拥有连续的可用的内存,而实际上它通常是被分成多个物理内存碎片。###"}
{"prompt":"什么是死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有进程都无法继续执行。###"}
{"prompt":"什么是缓存?\n\n###\n\n","completion":" 缓存是指在计算机中暂存数据的高速存储器,它可以提高计算机对数据的访问速度,避免频繁地访问较慢的主存储器。###"}
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是计算机中用来管理和组织文件的一种机制,它通过一系列的数据结构来描述文件和目录的组织方式,以及文件如何存储和访问。###"}
{"prompt":"什么是调度算法?\n\n###\n\n","completion":" 调度算法是指操作系统中用来决定进程或线程在cpu上执行顺序的一种算法,它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
\ No newline at end of file
import { GET, POST, DELETE } from './request';
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat';
/**
* 获取一个聊天框的ID
*/
export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?modelId=${modelId}`);
/**
* 获取初始化聊天内容
*/
export const getInitChatSiteInfo = (chatId: string, windowId: string = '') =>
GET<{
windowId: string;
chatSite: ChatSiteType;
history: ChatItemType[];
}>(`/chat/init?chatId=${chatId}&windowId=${windowId}`);
/**
* 发送 GPT3 prompt
*/
export const postGPT3SendPrompt = ({
chatId,
prompt
}: {
prompt: ChatSiteItemType[];
chatId: string;
}) =>
POST<string>(`/chat/gpt3`, {
chatId,
prompt: prompt.map((item) => ({
obj: item.obj,
value: item.value
}))
});
/**
* 预发 prompt 进行存储
*/
export const postChatGptPrompt = ({
prompt,
windowId,
chatId
}: {
prompt: ChatSiteItemType;
windowId: string;
chatId: string;
}) =>
POST<string>(`/chat/preChat`, {
windowId,
prompt: {
obj: prompt.obj,
value: prompt.value
},
chatId
});
/* 获取 Chat 的 Event 对象,进行持续通信 */
export const getChatGPTSendEvent = (chatId: string, windowId: string) =>
new EventSource(`/api/chat/chatGpt?chatId=${chatId}&windowId=${windowId}`);
/**
* 删除最后一句
*/
export const delLastMessage = (windowId?: string) =>
windowId ? DELETE(`/chat/delLastMessage?windowId=${windowId}`) : null;
import { GET, POST, DELETE, PUT } from './request';
import type { ModelType } from '@/types/model';
import { ModelUpdateParams } from '@/types/model';
import { TrainingItemType } from '../types/training';
export const getMyModels = () => GET<ModelType[]>('/model/list');
export const postCreateModel = (data: { name: string; serviceModelName: string }) =>
POST<ModelType>('/model/create', data);
export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`);
export const getModelById = (id: string) => GET<ModelType>(`/model/detail?modelId=${id}`);
export const putModelById = (id: string, data: ModelUpdateParams) =>
PUT(`/model/update?modelId=${id}`, data);
export const postTrainModel = (id: string, form: FormData) =>
POST(`/model/train?modelId=${id}`, form, {
headers: {
'content-type': 'multipart/form-data'
}
});
export const putModelTrainingStatus = (id: string) => PUT(`/model/putTrainStatus?modelId=${id}`);
export const getModelTrainings = (id: string) =>
GET<TrainingItemType[]>(`/model/getTrainings?modelId=${id}`);
import axios, { Method, InternalAxiosRequestConfig, AxiosResponse } from 'axios';
import { getToken, clearToken } from '@/utils/user';
import { TOKEN_ERROR_CODE } from '@/constants/responseCode';
interface ConfigType {
headers?: { [key: string]: string };
hold?: boolean;
}
interface ResponseDataType {
code: number;
message: string;
data: any;
}
/**
* 请求开始
*/
function requestStart(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig {
if (config.headers) {
config.headers.Authorization = getToken();
}
return config;
}
/**
* 请求成功,检查请求头
*/
function responseSuccess(response: AxiosResponse<ResponseDataType>) {
return response;
}
/**
* 响应数据检查
*/
function checkRes(data: ResponseDataType) {
if (data === undefined) {
console.log(data, 'data is empty');
return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data.message);
}
return data.data;
}
/**
* 响应错误
*/
function responseError(err: any) {
console.error('请求错误', err);
if (!err) {
return Promise.reject('未知错误');
}
if (typeof err === 'string') {
return Promise.reject(err);
}
if (err.response) {
// 有报错响应
const res = err.response;
/* token过期,判断请求token与本地是否相同,若不同需要重发 */
if (res.data.code in TOKEN_ERROR_CODE) {
clearToken();
return Promise.reject('token过期,重新登录');
}
}
return Promise.reject('未知错误');
}
/* 创建请求实例 */
const instance = axios.create({
timeout: 60000, // 超时时间
headers: {
'content-type': 'application/json'
}
});
/* 请求拦截 */
instance.interceptors.request.use(requestStart, (err) => Promise.reject(err));
/* 响应拦截 */
instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err));
function request(url: string, data: any, config: ConfigType, method: Method): any {
/* 去空 */
for (const key in data) {
if (data[key] === null || data[key] === undefined) {
delete data[key];
}
}
return instance
.request({
baseURL: '/api',
url,
method,
data: method === 'GET' ? null : data,
params: method === 'GET' ? data : null, // get请求不携带data,params放在url上
...config // 用户自定义配置,可以覆盖前面的配置
})
.then((res) => checkRes(res.data))
.catch((err) => responseError(err));
}
/**
* api请求方式
* @param {String} url
* @param {Any} params
* @param {Object} config
* @returns
*/
export function GET<T>(url: string, params = {}, config: ConfigType = {}): Promise<T> {
return request(url, params, config, 'GET');
}
export function POST<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'POST');
}
export function PUT<T>(url: string, data = {}, config: ConfigType = {}): Promise<T> {
return request(url, data, config, 'PUT');
}
export function DELETE<T>(url: string, config: ConfigType = {}): Promise<T> {
return request(url, {}, config, 'DELETE');
}
import type { UserType } from '@/types/user';
export interface ResLogin {
token: string;
user: UserType;
}
import { GET, POST, PUT } from './request';
import { createHashPassword } from '@/utils/tools';
import { ResLogin } from './response/user';
import { EmailTypeEnum } from '@/constants/common';
import { UserType, UserUpdateParams } from '@/types/user';
export const sendCodeToEmail = ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) =>
GET('/user/sendEmail', { email, type });
export const getTokenLogin = () => GET<UserType>('/user/tokenLogin');
export const postRegister = ({
email,
password,
code
}: {
email: string;
code: string;
password: string;
}) =>
POST<ResLogin>('/user/register', {
email,
code,
password: createHashPassword(password)
});
export const postFindPassword = ({
email,
code,
password
}: {
email: string;
code: string;
password: string;
}) =>
POST<ResLogin>('/user/updatePasswordByCode', {
email,
code,
password: createHashPassword(password)
});
export const postLogin = ({ email, password }: { email: string; password: string }) =>
POST<ResLogin>('/user/loginByPassword', {
email,
password: createHashPassword(password)
});
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
type TIconfont = {
name: string;
color?: string;
width?: number | string;
height?: number | string;
className?: string;
};
function Icon({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) {
const style = {
fill: color,
width,
height
};
return (
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
<use xlinkHref={`#${name}`}></use>
</svg>
);
}
export default Icon;
import React from 'react';
import { useRouter } from 'next/router';
import { useToast } from '@chakra-ui/react';
import { getTokenLogin } from '@/api/user';
import { useUserStore } from '@/store/user';
import { useGlobalStore } from '@/store/global';
import { useQuery } from '@tanstack/react-query';
const unAuthPage: { [key: string]: boolean } = {
'/login': true,
'/chat': true
};
const Auth = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const toast = useToast({
title: '请先登录',
position: 'top',
status: 'warning'
});
const { userInfo, setUserInfo } = useUserStore();
const { setLoading } = useGlobalStore();
useQuery(
[router.pathname, userInfo],
() => {
setLoading(true);
if (unAuthPage[router.pathname] === true || userInfo) {
return setLoading(false);
} else {
return getTokenLogin();
}
},
{
onSuccess(user) {
if (user) {
setUserInfo(user);
}
},
onError(error) {
console.log(error);
router.push('/login');
toast();
},
onSettled() {
setLoading(false);
}
}
);
return userInfo || unAuthPage[router.pathname] === true ? <>{children}</> : null;
};
export default Auth;
import React from 'react';
import { Box } from '@chakra-ui/react';
import Navbar from './navbar';
import NavbarPhone from './navbarPhone';
import { useRouter } from 'next/router';
import { useScreen } from '@/hooks/useScreen';
import { useLoading } from '@/hooks/useLoading';
import Auth from './auth';
import { useGlobalStore } from '@/store/global';
const unShowLayoutRoute: { [key: string]: boolean } = {
'/login': true,
'/chat': true
};
const navbarList = [
{
label: '介绍',
icon: 'icon-gongzuotai-01',
link: '/',
activeLink: ['/']
},
{
label: '模型',
icon: 'icon-moxing',
link: '/model/list',
activeLink: ['/model/list', '/model/detail']
},
// {
// label: '数据',
// icon: 'icon-datafull',
// link: '/training/dataList',
// activeLink: ['/training/dataList']
// },
{
label: '账号',
icon: 'icon-yonghu-yuan',
link: '/number/setting',
activeLink: ['/number/setting']
}
];
const Layout = ({ children }: { children: JSX.Element }) => {
const { isPc } = useScreen();
const router = useRouter();
const { Loading } = useLoading({
defaultLoading: true
});
const { loading } = useGlobalStore();
return (
<>
{!unShowLayoutRoute[router.pathname] ? (
<Box minHeight={'100vh'} backgroundColor={'gray.100'}>
{isPc ? (
<>
<Box h={'100vh'} position={'fixed'} left={0} top={0} w={'80px'}>
<Navbar navbarList={navbarList} />
</Box>
<Box ml={'80px'} p={7}>
<Box maxW={'1100px'} m={'auto'}>
<Auth>{children}</Auth>
</Box>
</Box>
</>
) : (
<Box pt={'60px'}>
<Box
h={'60px'}
position={'fixed'}
top={0}
left={0}
right={0}
zIndex={100}
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<NavbarPhone navbarList={navbarList} />
</Box>
<Box py={3} px={4}>
<Auth>{children}</Auth>
</Box>
</Box>
)}
</Box>
) : (
<Auth>
<>{children}</>
</Auth>
)}
{loading && <Loading />}
</>
);
};
export default Layout;
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import Icon from '../Icon';
import styles from './style.module.scss';
export enum NavbarTypeEnum {
normal = 'normal',
small = 'small'
}
const Navbar = ({
navbarList
}: {
navbarList: {
label: string;
icon: string;
link: string;
activeLink: string[];
}[];
}) => {
const router = useRouter();
return (
<Flex
flexDirection={'column'}
alignItems={'center'}
py={3}
backgroundColor={'white'}
h={'100%'}
w={'100%'}
boxShadow={'4px 0px 4px 0px rgba(43, 45, 55, 0.01)'}
userSelect={'none'}
>
{/* logo */}
<Box pb={4}>
<Image src={'/logo.svg'} width={50} height={100} alt=""></Image>
</Box>
{/* 导航列表 */}
<Box flex={1}>
{navbarList.map((item) => (
<Flex
key={item.label}
mb={4}
flexDirection={'column'}
alignItems={'center'}
justifyContent={'center'}
onClick={() =>
router.push(item.link, undefined, {
shallow: true
})
}
cursor={'pointer'}
fontSize={'sm'}
w={'60px'}
h={'70px'}
borderRadius={'sm'}
{...(item.activeLink.includes(router.pathname)
? {
color: '#2B6CB0',
backgroundColor: '#BEE3F8'
}
: {
color: '#4A5568',
backgroundColor: 'transparent'
})}
>
<Icon
name={item.icon}
width={24}
height={24}
color={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
/>
<Box mt={1}>{item.label}</Box>
</Flex>
))}
</Box>
{/* 通知 icon */}
{/* <Flex className={styles.informIcon} mb={5} justifyContent={'center'}>
<Icon name={'icon-tongzhi'} width={28} height={28} color={'#718096'}></Icon>
</Flex> */}
</Flex>
);
};
export default Navbar;
import React from 'react';
import { useRouter } from 'next/router';
import Icon from '../Icon';
import {
Flex,
Drawer,
DrawerBody,
DrawerFooter,
DrawerOverlay,
DrawerContent,
Box,
useDisclosure,
Button,
Image
} from '@chakra-ui/react';
const NavbarPhone = ({
navbarList
}: {
navbarList: {
label: string;
icon: string;
link: string;
activeLink: string[];
}[];
}) => {
const router = useRouter();
const { isOpen, onClose, onOpen } = useDisclosure();
return (
<>
<Flex
alignItems={'center'}
h={'100%'}
justifyContent={'space-between'}
backgroundColor={'white'}
position={'relative'}
px={7}
>
<Box onClick={onOpen}>
<Icon name="icon-caidan" width={20} height={20}></Icon>
</Box>
{/* <Icon name="icon-tongzhi" width={20} height={20}></Icon> */}
</Flex>
<Drawer isOpen={isOpen} placement="left" size={'xs'} onClose={onClose}>
<DrawerOverlay />
<DrawerContent maxWidth={'60vw'}>
<DrawerBody p={4}>
<Box pb={4}>
<Image src={'/logo.svg'} w={'100%'} h={'70px'} pt={2} alt=""></Image>
</Box>
{navbarList.map((item) => (
<Flex
key={item.label}
mb={4}
alignItems={'center'}
justifyContent={'center'}
onClick={() => {
router.push(item.link);
onClose();
}}
cursor={'pointer'}
fontSize={'sm'}
h={'65px'}
borderRadius={'md'}
{...(item.activeLink.includes(router.pathname)
? {
color: '#2B6CB0',
backgroundColor: '#BEE3F8'
}
: {
color: '#4A5568',
backgroundColor: 'transparent'
})}
>
<Icon
name={item.icon}
width={24}
height={24}
color={item.activeLink.includes(router.pathname) ? '#2B6CB0' : '#4A5568'}
/>
<Box ml={5}>{item.label}</Box>
</Flex>
))}
</DrawerBody>
<DrawerFooter px={2}>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
</>
);
};
export default NavbarPhone;
.informIcon {
svg {
cursor: pointer;
margin: 0;
}
}
import React from 'react';
export const codeLight: { [key: string]: React.CSSProperties } = {
'code[class*=language-]': {
color: '#d4d4d4',
fontSize: '13px',
textShadow: 'none',
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
direction: 'ltr',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none',
MozHyphens: 'none',
msHyphens: 'none',
hyphens: 'none'
},
'pre[class*=language-]': {
color: '#d4d4d4',
fontSize: '13px',
textShadow: 'none',
fontFamily: 'Menlo,Monaco,Consolas,"Andale Mono","Ubuntu Mono","Courier New",monospace',
direction: 'ltr',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
lineHeight: '1.5',
MozTabSize: '4',
OTabSize: '4',
tabSize: '4',
WebkitHyphens: 'none',
MozHyphens: 'none',
msHyphens: 'none',
hyphens: 'none',
padding: '1em',
margin: '.5em 0',
overflow: 'auto',
background: '#1e1e1e'
},
'code[class*=language-] ::selection': {
textShadow: 'none',
background: '#264f78'
},
'code[class*=language-]::selection': {
textShadow: 'none',
background: '#264f78'
},
'pre[class*=language-] ::selection': {
textShadow: 'none',
background: '#264f78'
},
'pre[class*=language-]::selection': {
textShadow: 'none',
background: '#264f78'
},
':not(pre)>code[class*=language-]': {
padding: '.1em .3em',
borderRadius: '.3em',
color: '#db4c69',
background: '#1e1e1e'
},
'.namespace': {
opacity: '0.7'
},
'doctype.doctype-tag': {
color: '#569cd6'
},
'doctype.name': {
color: '#9cdcfe'
},
comment: {
color: '#6a9955'
},
prolog: {
color: '#6a9955'
},
'.language-html .language-css .token.punctuation': {
color: '#d4d4d4'
},
'.language-html .language-javascript .token.punctuation': {
color: '#d4d4d4'
},
punctuation: {
color: '#d4d4d4'
},
boolean: {
color: '#569cd6'
},
constant: {
color: '#9cdcfe'
},
inserted: {
color: '#b5cea8'
},
number: {
color: '#b5cea8'
},
property: {
color: '#9cdcfe'
},
symbol: {
color: '#b5cea8'
},
tag: {
color: '#569cd6'
},
unit: {
color: '#b5cea8'
},
'attr-name': {
color: '#9cdcfe'
},
builtin: {
color: '#ce9178'
},
char: {
color: '#ce9178'
},
deleted: {
color: '#ce9178'
},
selector: {
color: '#d7ba7d'
},
string: {
color: '#ce9178'
},
'.language-css .token.string.url': {
textDecoration: 'underline'
},
entity: {
color: '#569cd6'
},
operator: {
color: '#d4d4d4'
},
'operator.arrow': {
color: '#569cd6'
},
atrule: {
color: '#ce9178'
},
'atrule.rule': {
color: '#c586c0'
},
'atrule.url': {
color: '#9cdcfe'
},
'atrule.url.function': {
color: '#dcdcaa'
},
'atrule.url.punctuation': {
color: '#d4d4d4'
},
keyword: {
color: '#569cd6'
},
'keyword.control-flow': {
color: '#c586c0'
},
'keyword.module': {
color: '#c586c0'
},
function: {
color: '#dcdcaa'
},
'function.maybe-class-name': {
color: '#dcdcaa'
},
regex: {
color: '#d16969'
},
important: {
color: '#569cd6'
},
italic: {
fontStyle: 'italic'
},
'class-name': {
color: '#4ec9b0'
},
'maybe-class-name': {
color: '#4ec9b0'
},
console: {
color: '#9cdcfe'
},
parameter: {
color: '#9cdcfe'
},
interpolation: {
color: '#9cdcfe'
},
'punctuation.interpolation-punctuation': {
color: '#569cd6'
},
'exports.maybe-class-name': {
color: '#9cdcfe'
},
'imports.maybe-class-name': {
color: '#9cdcfe'
},
variable: {
color: '#9cdcfe'
},
escape: {
color: '#d7ba7d'
},
'tag.punctuation': {
color: 'grey'
},
cdata: {
color: 'grey'
},
'attr-value': {
color: '#ce9178'
},
'attr-value.punctuation': {
color: '#ce9178'
},
'attr-value.punctuation.attr-equals': {
color: '#d4d4d4'
},
namespace: {
color: '#4ec9b0'
},
'code[class*=language-javascript]': {
color: '#9cdcfe'
},
'code[class*=language-jsx]': {
color: '#9cdcfe'
},
'code[class*=language-tsx]': {
color: '#9cdcfe'
},
'code[class*=language-typescript]': {
color: '#9cdcfe'
},
'pre[class*=language-javascript]': {
color: '#9cdcfe'
},
'pre[class*=language-jsx]': {
color: '#9cdcfe'
},
'pre[class*=language-tsx]': {
color: '#9cdcfe'
},
'pre[class*=language-typescript]': {
color: '#9cdcfe'
},
'code[class*=language-css]': {
color: '#ce9178'
},
'pre[class*=language-css]': {
color: '#ce9178'
},
'code[class*=language-html]': {
color: '#d4d4d4'
},
'pre[class*=language-html]': {
color: '#d4d4d4'
},
'.language-regex .token.anchor': {
color: '#dcdcaa'
},
'.language-html .token.punctuation': {
color: 'grey'
},
'pre[class*=language-]>code[class*=language-]': {
position: 'relative',
zIndex: '1'
},
'.line-highlight.line-highlight': {
background: '#f7ebc6',
boxShadow: 'inset 5px 0 0 #f7d87c',
zIndex: '0'
}
};
.waitingAnimation::after {
display: inline-block;
content: '';
width: 4px;
height: 14px;
transform: translate(4px, 2px) scaleY(1.3);
background-color: rgba(0, 0, 0, 0.7);
animation: blink 0.6s infinite;
}
.animation {
:last-child::after {
display: inline-block;
content: '';
width: 4px;
height: 14px;
transform: translate(4px, 2px) scaleY(1.3);
background-color: rgba(0, 0, 0, 0.7);
animation: blink 0.6s infinite;
}
}
@keyframes blink {
from,
to {
opacity: 0;
}
50% {
opacity: 1;
}
}
.markdown {
/* 标题样式 */
h1 {
font-size: 1.8rem;
}
h2 {
font-size: 1.6rem;
}
h3 {
font-size: 1.4rem;
}
h4 {
font-size: 1.2rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.83rem;
}
/* 列表样式 */
ol,
ul {
padding-left: 1.5rem;
margin-left: 1rem;
}
ul {
list-style: inside;
}
ol {
list-style: decimal;
}
/* 链接样式 */
a {
color: #0077cc;
text-decoration: none;
border-bottom: 1px solid #0077cc;
}
a:hover {
color: #005580;
border-bottom-color: #005580;
}
/* 图片样式 */
img {
max-width: 100%;
max-height: 200px;
margin: auto;
}
/* 强调样式 */
em,
i {
font-style: italic;
}
strong,
b {
font-weight: bold;
}
/* 代码样式 */
code {
border-radius: 3px;
width: 100%;
}
pre {
padding: 10px 15px;
width: 100%;
background-color: #222 !important;
overflow-x: auto;
}
pre code {
display: block;
border: none;
background-color: #222;
color: #fff;
}
p {
line-height: 1.7;
}
}
import React, { useMemo, memo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import styles from './index.module.scss';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { codeLight } from './codeLight';
import { Box, Flex } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon';
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => {
// const formatSource = useMemo(() => source.replace(/\n/g, '\n'), [source]);
const { copyData } = useCopyData();
return (
<ReactMarkdown
className={`${styles.markdown} ${
isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''
}`}
rehypePlugins={[remarkGfm]}
skipHtml={true}
components={{
p: 'div',
pre: 'div',
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
const code = String(children).replace(/\n$/, '');
return (
<Box my={3} borderRadius={'md'} overflow={'hidden'}>
<Flex py={2} px={5} backgroundColor={'#323641'} color={'#fff'} fontSize={'sm'}>
<Box flex={1}>{match?.[1]}</Box>
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon>
<Box ml={1}>复制代码</Box>
</Flex>
</Flex>
<SyntaxHighlighter
style={codeLight as any}
showLineNumbers
language={match?.[1]}
{...props}
>
{code}
</SyntaxHighlighter>
</Box>
);
}
}}
>
{source}
</ReactMarkdown>
);
};
export default memo(Markdown);
export enum EmailTypeEnum {
register = 'register',
findPassword = 'findPassword'
}
export const introPage = `
## 欢迎使用 Doc GPT
时间比较赶,介绍没来得及完善,先直接上怎么使用:
1. 使用邮箱注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。
4. 在模型列表点击【对话】,即可使用 API 进行聊天。
### 模型配置
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。
3. **上下文最大长度**:每个聊天,最多的轮数除以2,建议设置为偶数。可以持续聊天,但是旧的聊天内容会被截断,AI 就不会知道被截取的内容。
例如:上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
4. **过期时间**:生成对话框后,这个对话框多久过期。
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制,正数代表只能加载 n 次,防止被盗刷。
### 对话框介绍
1. 每个对话框以 windowId 作为标识。
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题。
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如:
* 当前网页链接:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49
* 分享链接应为:http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764
### 其他问题
还有其他问题,可以加我 wx,拉个交流群大家一起聊聊。
![](/imgs/erweima.jpg)
`;
export enum OpenAiModelEnum {
GPT35 = 'gpt-3.5-turbo',
GPT3 = 'text-davinci-003'
}
export const OpenAiList = [
{
name: 'chatGPT',
model: OpenAiModelEnum.GPT35,
trainName: 'turbo',
canTraining: false,
maxToken: 4060
},
{
name: 'GPT3',
model: OpenAiModelEnum.GPT3,
trainName: 'davinci',
canTraining: true,
maxToken: 4060
}
];
export enum TrainingStatusEnum {
pending = 'pending',
succeed = 'succeed',
errored = 'errored',
canceled = 'canceled'
}
export enum ModelStatusEnum {
running = 'running',
training = 'training',
pending = 'pending',
closed = 'closed'
}
export const formatModelStatus = {
[ModelStatusEnum.running]: {
colorTheme: 'green',
text: '运行中'
},
[ModelStatusEnum.training]: {
colorTheme: 'blue',
text: '训练中'
},
[ModelStatusEnum.pending]: {
colorTheme: 'gray',
text: '加载中'
},
[ModelStatusEnum.closed]: {
colorTheme: 'red',
text: '已关闭'
}
};
export const ERROR_CODE: { [key: number]: string } = {
400: '请求失败',
401: '无权访问',
403: '紧张访问',
404: '请求不存在',
405: '请求方法错误',
406: '请求的格式错误',
410: '资源已删除',
422: '验证错误',
500: '服务器发生错误',
502: '网关错误',
503: '服务器暂时过载或维护',
504: '网关超时'
};
export const TOKEN_ERROR_CODE: { [key: number]: string } = {
506: '请先登录',
507: '请重新登录',
508: '登录已过期'
};
import { extendTheme, defineStyleConfig } from '@chakra-ui/react';
// @ts-ignore
import { modalAnatomy as parts } from '@chakra-ui/anatomy';
// @ts-ignore
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(parts.keys);
// modal 弹窗
const ModalTheme = defineMultiStyleConfig({
baseStyle: definePartsStyle({
dialog: {
width: '90%'
}
})
});
// 按键
const Button = defineStyleConfig({
baseStyle: {},
sizes: {
sm: {
fontSize: 'sm',
px: 3,
py: 0,
fontWeight: 'normal',
height: '26px'
},
md: {
fontSize: 'md',
px: 6,
py: 0,
height: '34px',
fontWeight: 'normal'
},
lg: {
fontSize: 'lg',
px: 8,
py: 0,
height: '42px',
fontWeight: 'normal'
}
},
variants: {
outline: {
borderWidth: '1.5px'
}
},
defaultProps: {
size: 'md',
colorScheme: 'blue'
}
});
// 全局主题
export const theme = extendTheme({
styles: {
global: {
'html, body': {
color: 'blackAlpha.800',
fontSize: '14px'
}
}
},
fonts: {
body: 'system-ui, sans-serif'
},
fontSizes: {
xs: '0.8rem',
sm: '0.9rem',
md: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
'4xl': '2.25rem',
'5xl': '3rem',
'6xl': '3.75rem',
'7xl': '4.5rem',
'8xl': '6rem',
'9xl': '8rem'
},
components: {
Modal: ModalTheme,
Button
}
});
export enum PageTypeEnum {
login = 'login',
register = 'register',
forgetPassword = 'forgetPassword'
}
import { useState, useRef } from 'react';
import {
AlertDialog,
AlertDialogBody,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogContent,
AlertDialogOverlay,
useDisclosure,
Button
} from '@chakra-ui/react';
export const useConfirm = ({ title = '提示', content }: { title?: string; content: string }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const cancelRef = useRef(null);
const confirmCb = useRef<any>();
const cancelCb = useRef<any>();
return {
openConfirm: (confirm?: any, cancel?: any) => {
onOpen();
confirmCb.current = confirm;
cancelCb.current = cancel;
},
ConfirmChild: () => (
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize="lg" fontWeight="bold">
{title}
</AlertDialogHeader>
<AlertDialogBody>{content}</AlertDialogBody>
<AlertDialogFooter>
<Button
colorScheme={'gray'}
onClick={() => {
onClose();
typeof cancelCb.current === 'function' && cancelCb.current();
}}
>
取消
</Button>
<Button
colorScheme="blue"
ml={3}
onClick={() => {
onClose();
typeof confirmCb.current === 'function' && confirmCb.current();
}}
>
确认
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
)
};
};
import { useState } from 'react';
import { Spinner, Flex } from '@chakra-ui/react';
export const useLoading = (props?: { defaultLoading: boolean }) => {
const [isLoading, setIsLoading] = useState(props?.defaultLoading || false);
const Loading = ({
loading,
fixed = true
}: {
loading?: boolean;
fixed?: boolean;
}): JSX.Element | null => {
return isLoading || loading ? (
<Flex
position={fixed ? 'fixed' : 'absolute'}
zIndex={100}
backgroundColor={'rgba(255,255,255,0.5)'}
top={0}
left={0}
right={0}
bottom={0}
alignItems={'center'}
justifyContent={'center'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="gray.200" color="blue.500" size="xl" />
</Flex>
) : null;
};
return {
isLoading,
setIsLoading,
Loading
};
};
import { useMemo } from 'react';
import { useMediaQuery } from '@chakra-ui/react';
export function useScreen() {
const [isPc] = useMediaQuery('(min-width: 900px)', {
ssr: true,
fallback: false
});
return {
isPc,
mediaLgMd: useMemo(() => (isPc ? 'lg' : 'md'), [isPc]),
mediaMdSm: useMemo(() => (isPc ? 'md' : 'sm'), [isPc]),
media: (pc: number | string, phone: number | string) => (isPc ? pc : phone)
};
}
import { useState, useMemo, useCallback } from 'react';
import { sendCodeToEmail } from '@/api/user';
import { EmailTypeEnum } from '@/constants/common';
import { useToast } from '@chakra-ui/react';
let timer: any;
export const useSendCode = () => {
const toast = useToast({
position: 'top',
duration: 2000
});
const [codeSending, setCodeSending] = useState(false);
const [codeCountDown, setCodeCountDown] = useState(0);
const sendCodeText = useMemo(() => {
if (codeCountDown >= 10) {
return `${codeCountDown}s后重新获取`;
}
if (codeCountDown > 0) {
return `0${codeCountDown}s后重新获取`;
}
return '获取验证码';
}, [codeCountDown]);
const sendCode = useCallback(
async ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) => {
setCodeSending(true);
try {
await sendCodeToEmail({
email,
type
});
setCodeCountDown(60);
timer = setInterval(() => {
setCodeCountDown((val) => {
if (val <= 0) {
clearInterval(timer);
}
return val - 1;
});
}, 1000);
toast({
title: '验证码已发送',
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
}
setCodeSending(false);
},
[toast]
);
return {
codeSending,
sendCode,
sendCodeText,
codeCountDown
};
};
import { useToast as uToast, UseToastOptions } from '@chakra-ui/react';
export const useToast = (props?: UseToastOptions) => {
const toast = uToast({
position: 'top',
duration: 2000,
...props
});
return {
toast
};
};
import React, { useEffect } from 'react';
import { useRouter } from 'next/router';
const NonePage = () => {
const router = useRouter();
useEffect(() => {
router.push('/model/list');
}, [router]);
return <div></div>;
};
export default NonePage;
import '@/styles/globals.css' import type { AppProps, NextWebVitalsMetric } from 'next/app';
import type { AppProps } from 'next/app' import Head from 'next/head';
import { ChakraProvider } from '@chakra-ui/react';
import Layout from '@/components/Layout';
import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import '../styles/reset.scss';
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} /> // Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: false,
cacheTime: 0
}
}
});
return (
<>
<Head>
<title>Doc GPT</title>
<meta name="description" content="Generated by Doc GPT" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
/>
<link rel="icon" href="/favicon.ico" />
<script src="/iconfont.js" async></script>
</Head>
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<Layout>
<Component {...pageProps} />
</Layout>
</ChakraProvider>
</QueryClientProvider>
</>
);
} }
// export function reportWebVitals(metric: NextWebVitalsMetric) {
// console.log(metric);
// }
import { Html, Head, Main, NextScript } from 'next/document' import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() { export default function Document() {
return ( return (
...@@ -9,5 +9,5 @@ export default function Document() { ...@@ -9,5 +9,5 @@ export default function Document() {
<NextScript /> <NextScript />
</body> </body>
</Html> </Html>
) );
} }
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { openaiProxy } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Encoding': 'none',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
});
const { chatId, windowId } = req.query as { chatId: string; windowId: string };
try {
if (!windowId || !chatId) {
throw new Error('缺少参数');
}
await connectToDatabase();
const { chat, userApiKey } = await authChat(chatId);
const model: ModelType = chat.modelId;
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
// 读取对话内容
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content;
// 长度过滤
const maxContext = model.security.contextMaxLen;
const filterPrompts =
prompts.length > maxContext + 2
? [prompts[0], ...prompts.slice(prompts.length - maxContext)]
: prompts.slice(0, prompts.length);
// 格式化文本内容
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
(item: ChatItemType) => ({
role: map[item.obj],
content: item.value
})
);
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey);
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: 1,
// max_tokens: model.security.contentMaxLen,
messages: formatPrompts,
stream: true
},
openaiProxy
);
// 截取字符串内容
const reg = /{"content"(.*)"}/g;
// @ts-ignore
const match = chatResponse.data.match(reg);
let AIResponse = '';
if (match) {
match.forEach((item: string, i: number) => {
try {
const json = JSON.parse(item);
// 开头的换行忽略
if (i === 0 && json.content?.startsWith('\n')) return;
AIResponse += json.content;
const content = json.content.replace(/\n/g, '<br/>'); // 无法直接传输\n
content && res.write(`data: ${content}\n\n`);
} catch (err) {
err;
}
});
}
res.write(`data: [DONE]\n\n`);
// 存入库
await ChatWindow.findByIdAndUpdate(windowId, {
$push: {
content: {
obj: 'AI',
value: AIResponse
}
},
updateTime: Date.now()
});
res.end();
} catch (err: any) {
console.log(err?.response?.data || err);
// 删除最一条数据库记录, 也就是预发送的那一条
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
});
res.end();
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ChatWindow } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { windowId } = req.query as { windowId: string };
if (!windowId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 删除最一条数据库记录, 也就是预发送的那一条
await ChatWindow.findByIdAndUpdate(windowId, {
$pop: { content: 1 },
updateTime: Date.now()
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelType } from '@/types/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query;
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!modelId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 获取模型配置
const model: ModelType | null = await Model.findOne({
_id: modelId,
userId
});
if (!model) {
throw new Error('模型不存在');
}
// 创建 chat 数据
const response = await Chat.create({
userId,
modelId,
expiredTime: Date.now() + model.security.expiredTime,
loadAmount: model.security.maxLoadAmount
});
jsonRes(res, {
data: response._id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { openaiProxy } from '@/service/utils/tools';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { prompt, chatId } = req.body as { prompt: ChatItemType[]; chatId: string };
if (!prompt || !chatId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 获取 chat 数据
const chat = await Chat.findById(chatId)
.populate({
path: 'modelId',
options: {
strictPopulate: false
}
})
.populate({
path: 'userId',
options: {
strictPopulate: false
}
});
if (!chat || !chat.modelId || !chat.userId) {
throw new Error('聊天已过期');
}
const model: ModelType = chat.modelId;
// 获取 user 的 apiKey
const user = chat.userId;
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
if (!userApiKey) {
throw new Error('缺少ApiKey, 无法请求');
}
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey);
// prompt处理
const formatPrompt = prompt.map((item) => `${item.value}\n\n###\n\n`).join('');
// 发送请求
const response = await chatAPI.createCompletion(
{
model: model.service.modelName,
prompt: formatPrompt,
temperature: 0.5,
max_tokens: model.security.contentMaxLen,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0.6,
stop: ['###']
},
openaiProxy
);
const responseMessage = response.data.choices[0]?.text;
jsonRes(res, {
data: responseMessage
});
} catch (err: any) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo';
import type { ModelType } from '@/types/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, windowId } = req.query as { chatId: string; windowId?: string };
if (!chatId) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 获取 chat 数据
const chat = await Chat.findById(chatId).populate({
path: 'modelId',
options: {
strictPopulate: false
}
});
// 安全校验
if (chat.loadAmount === 0 || chat.expiredTime < Date.now()) {
throw new Error('聊天框已过期');
}
if (chat.loadAmount > 0) {
await Chat.updateOne(
{
_id: chat._id
},
{
$inc: { loadAmount: -1 }
}
);
}
const model: ModelType = chat.modelId;
/* 查找是否有记录 */
let history = null;
let responseId = windowId;
try {
history = await ChatWindow.findById(windowId);
} catch (error) {
error;
}
const defaultContent = model.systemPrompt
? [
{
obj: 'SYSTEM',
value: model.systemPrompt
}
]
: [];
if (!history) {
// 没有记录,创建一个
const response = await ChatWindow.create({
chatId,
updateTime: Date.now(),
content: defaultContent
});
responseId = response._id;
}
jsonRes(res, {
data: {
windowId: responseId,
chatSite: {
modelId: model._id,
name: model.name,
avatar: model.avatar,
secret: model.security,
chatModel: model.service.chatModel
},
history: history ? history.content : defaultContent
}
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { ChatItemType } from '@/types/chat';
import { connectToDatabase, ChatWindow } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { authChat } from '@/service/utils/chat';
/* 聊天预请求,存储聊天内容 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { windowId, prompt, chatId } = req.body as {
windowId: string;
prompt: ChatItemType;
chatId: string;
};
if (!windowId || !prompt || !chatId) {
throw new Error('缺少参数');
}
await connectToDatabase();
const { chat } = await authChat(chatId);
// 长度校验
const model: ModelType = chat.modelId;
if (prompt.value.length > model.security.contentMaxLen) {
throw new Error('输入内容超长');
}
await ChatWindow.findByIdAndUpdate(windowId, {
$push: { content: prompt },
updateTime: Date.now()
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelStatusEnum, OpenAiList } from '@/constants/model';
import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, serviceModelName, serviceModelCompany = 'openai' } = req.body;
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!name || !serviceModelName || !serviceModelCompany) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
const modelItem = OpenAiList.find((item) => item.model === serviceModelName);
if (!modelItem) {
throw new Error('模型错误');
}
await connectToDatabase();
// 重名校验
const authRepeatName = await Model.findOne({
name,
userId
});
if (authRepeatName) {
throw new Error('模型名重复');
}
// 上限校验
const authCount = await Model.countDocuments({
userId
});
if (authCount >= 5) {
throw new Error('上限5个模型');
}
// 创建模型
const response = await Model.create({
name,
userId,
status: ModelStatusEnum.running,
service: {
company: serviceModelCompany,
trainId: modelItem.trainName,
chatModel: modelItem.model,
modelName: modelItem.model
}
});
// 根据 id 获取模型信息
const model = await Model.findById(response._id);
jsonRes(res, {
data: model
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat, Model, Training, connectToDatabase } from '@/service/mongo';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { TrainingStatusEnum } from '@/constants/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { TrainingItemType } from '@/types/training';
import { openaiProxy } from '@/service/utils/tools';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId } = req.query;
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!modelId) {
throw new Error('参数错误');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 删除模型
await Model.deleteOne({
_id: modelId,
userId
});
// 删除对应的聊天
await Chat.deleteMany({
modelId
});
// 查看是否正在训练
const training: TrainingItemType | null = await Training.findOne({
modelId,
status: TrainingStatusEnum.pending
});
// 如果正在训练,需要删除openai上的相关信息
if (training) {
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
// 获取训练记录
const tuneRecord = await openai.retrieveFineTune(training.tuneId, openaiProxy);
// 删除训练文件
openai.deleteFile(tuneRecord.data.training_files[0].id, openaiProxy);
// 取消训练
openai.cancelFineTune(training.tuneId, openaiProxy);
}
// 删除对应训练记录
await Training.deleteMany({
modelId
});
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import { ModelType } from '@/types/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query;
if (!modelId) {
throw new Error('参数错误');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 根据 userId 获取模型信息
const model: ModelType | null = await Model.findOne({
userId,
_id: modelId
});
if (!model) {
throw new Error('模型不存在');
}
jsonRes(res, {
data: model
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import formidable from 'formidable';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { join } from 'path';
import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { openaiProxy } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {
api: {
bodyParser: false
}
};
/* 上传文件,开始微调 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query;
if (!modelId) {
throw new Error('参数错误');
}
const userId = await authToken(authorization);
await connectToDatabase();
/* 获取 modelId 下的 training 记录 */
const records = await Training.find({
modelId
});
jsonRes(res, {
data: records
});
} catch (err: any) {
/* 清除上传的文件,关闭训练记录 */
// @ts-ignore
if (openai) {
// @ts-ignore
uploadFileId && openai.deleteFile(uploadFileId);
// @ts-ignore
trainId && openai.cancelFineTune(trainId);
}
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 根据 userId 获取模型信息
const models = await Model.find({
userId
});
jsonRes(res, {
data: models
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import type { ModelType } from '@/types/model';
import { TrainingItemType } from '@/types/training';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
import { openaiProxy } from '@/service/utils/tools';
/* 更新训练状态 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 获取模型
const model: ModelType | null = await Model.findById(modelId);
if (!model || model.status !== 'training') {
throw new Error('模型不在训练中');
}
// 查询正在训练中的训练记录
const training: TrainingItemType | null = await Training.findOne({
modelId,
status: 'pending'
});
if (!training) {
throw new Error('找不到训练记录');
}
// 用户的 openai 实例
const openai = getOpenAIApi(await getUserOpenaiKey(userId));
// 获取 openai 的训练情况
const { data } = await openai.retrieveFineTune(training.tuneId, openaiProxy);
if (data.status === OpenAiTuneStatusEnum.succeeded) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
// 更新模型
await Model.findByIdAndUpdate(modelId, {
status: ModelStatusEnum.running,
updateTime: new Date(),
service: {
...model.service,
trainId: data.fine_tuned_model, // 训练完后,再次训练和对话使用的 model 是一样的
chatModel: data.fine_tuned_model
}
});
// 更新训练数据
await Training.findByIdAndUpdate(training._id, {
status: TrainingStatusEnum.succeed
});
return jsonRes(res, {
data: '模型微调完成'
});
}
if (data.status === OpenAiTuneStatusEnum.cancelled) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
// 更新模型
await Model.findByIdAndUpdate(modelId, {
status: ModelStatusEnum.running,
updateTime: new Date()
});
// 更新训练数据
await Training.findByIdAndUpdate(training._id, {
status: TrainingStatusEnum.canceled
});
return jsonRes(res, {
data: '模型微调取消'
});
}
throw new Error('模型还在训练中');
} catch (err: any) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import formidable from 'formidable';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools';
import { join } from 'path';
import fs from 'fs';
import type { ModelType } from '@/types/model';
import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { openaiProxy } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式
export const config = {
api: {
bodyParser: false
}
};
/* 上传文件,开始微调 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let openai: OpenAIApi, trainId: string, uploadFileId: string;
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
const { modelId } = req.query;
if (!modelId) {
throw new Error('参数错误');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 获取模型的状态
const model: ModelType | null = await Model.findById(modelId);
if (!model || model.status !== 'running') {
throw new Error('模型正忙');
}
// const trainingType = model.service.modelType
const trainingType = model.service.trainId; // 目前都默认是 openai text-davinci-03
// 获取用户的 API Key 实例化后的对象
openai = getOpenAIApi(await getUserOpenaiKey(userId));
// 接收文件并保存
const form = formidable({
uploadDir: join(process.cwd(), 'public/trainData'),
keepExtensions: true
});
const { files } = await new Promise<{
fields: formidable.Fields;
files: formidable.Files;
}>((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) return reject(err);
resolve({ fields, files });
});
});
const file = files.file;
// 上传文件
// @ts-ignore
const uploadRes = await openai.createFile(
// @ts-ignore
fs.createReadStream(file.filepath),
'fine-tune',
openaiProxy
);
uploadFileId = uploadRes.data.id; // 记录上传文件的 ID
// 开始训练
const trainRes = await openai.createFineTune(
{
training_file: uploadFileId,
model: trainingType,
suffix: model.name
},
openaiProxy
);
trainId = trainRes.data.id; // 记录训练 ID
// 创建训练记录
await Training.create({
serviceName: 'openai',
tuneId: trainId,
status: TrainingStatusEnum.pending,
modelId
});
// 修改模型状态
await Model.findByIdAndUpdate(modelId, {
$inc: {
trainingTimes: +1
},
updateTime: new Date(),
status: ModelStatusEnum.training
});
jsonRes(res, {
data: 'start training'
});
} catch (err: any) {
/* 清除上传的文件,关闭训练记录 */
// @ts-ignore
if (openai) {
// @ts-ignore
uploadFileId && openai.deleteFile(uploadFileId, openaiProxy);
// @ts-ignore
trainId && openai.cancelFineTune(trainId, openaiProxy);
}
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model';
import type { ModelUpdateParams } from '@/types/model';
/* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { name, service, security, systemPrompt } = req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!name || !service || !security || !modelId) {
throw new Error('参数错误');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
// 更新模型
await Model.updateOne(
{
_id: modelId,
userId
},
{
name,
service,
systemPrompt,
security
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
if (req.method !== 'GET') return;
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Encoding': 'none',
'Cache-Control': 'no-cache',
'Content-Type': 'text/event-stream'
});
let val = 0;
const timer = setInterval(() => {
console.log('发送消息', val);
res.write(`data: ${val++}\n\n`);
if (val > 30) {
clearInterval(timer);
res.write(`data: [DONE]\n\n`);
res.end();
}
}, 500);
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const authCode = await AuthCode.deleteMany({
expiredTime: { $lt: Date.now() }
});
jsonRes(res, {
message: `删除了${authCode.deletedCount}条记录`
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ChatWindow } from '@/service/mongo';
/* 定时删除那些不活跃的内容 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const response = await ChatWindow.deleteMany(
{ $expr: { $lt: [{ $size: '$content' }, 5] } },
// 使用 $pull 操作符删除数组中的元素
{ $pull: { content: { $exists: true } } }
);
jsonRes(res, {
message: `删除了${response.deletedCount}条记录`
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Training, Model } from '@/service/mongo';
import type { TrainingItemType } from '@/types/training';
import { TrainingStatusEnum, ModelStatusEnum } from '@/constants/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { getUserOpenaiKey } from '@/service/utils/tools';
import { OpenAiTuneStatusEnum } from '@/service/constants/training';
import { sendTrainSucceed } from '@/service/utils/sendEmail';
import { openaiProxy } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
// 查询正在训练中的训练记录
const trainingRecords: TrainingItemType[] = await Training.find({
status: TrainingStatusEnum.pending
});
const openai = getOpenAIApi(await getUserOpenaiKey('63f9a14228d2a688d8dc9e1b'));
const response = await Promise.all(
trainingRecords.map(async (item) => {
const { data } = await openai.retrieveFineTune(item.tuneId, openaiProxy);
if (data.status === OpenAiTuneStatusEnum.succeeded) {
// 删除训练文件
openai.deleteFile(data.training_files[0].id, openaiProxy);
const model = await Model.findById(item.modelId).populate({
path: 'userId',
options: {
strictPopulate: false
}
});
if (!model) {
throw new Error('模型不存在');
}
// 更新模型
await Model.findByIdAndUpdate(item.modelId, {
status: ModelStatusEnum.running,
updateTime: new Date(),
service: {
...model.service,
trainId: data.fine_tuned_model, // 训练完后,再次训练和对话使用的 model 是一样的
chatModel: data.fine_tuned_model
}
});
// 更新训练数据
await Training.findByIdAndUpdate(item._id, {
status: TrainingStatusEnum.succeed
});
// 发送邮件通知
await sendTrainSucceed(model.userId.email as string, model.name);
return 'succeed';
}
return 'pending';
})
);
jsonRes(res, {
data: `${response.length}个训练线程,${
response.filter((item) => item === 'succeed').length
}个完成`
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { User } from '@/service/models/user';
import { generateToken } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { email, password } = req.body;
if (!email || !password) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 检测邮箱是否存在
const authEmail = await User.findOne({
email
});
if (!authEmail) {
throw new Error('邮箱未注册');
}
const user = await User.findOne({
email,
password
});
if (!user) {
throw new Error('密码错误');
}
jsonRes(res, {
data: {
token: generateToken(user._id),
user
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { User } from '@/service/models/user';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { generateToken } from '@/service/utils/tools';
import { EmailTypeEnum } from '@/constants/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { email, code, password } = req.body;
if (!email || !code || !password) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 验证码校验
const authCode = await AuthCode.findOne({
email,
code,
type: EmailTypeEnum.register,
expiredTime: { $gte: Date.now() }
});
if (!authCode) {
throw new Error('验证码错误');
}
// 重名校验
const authRepeat = await User.findOne({
email
});
if (authRepeat) {
throw new Error('邮箱已被注册');
}
const response = await User.create({
email,
password
});
// 根据 id 获取用户信息
const user = await User.findById(response._id);
jsonRes(res, {
data: {
token: generateToken(user._id),
user
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { sendCode } from '@/service/utils/sendEmail';
import { EmailTypeEnum } from '@/constants/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { email, type } = req.query;
if (!email || !type) {
throw new Error('缺少参数');
}
await connectToDatabase();
let code = '';
for (let i = 0; i < 6; i++) {
code += Math.floor(Math.random() * 10);
}
// 判断 1 分钟内是否有重复数据
const authCode = await AuthCode.findOne({
email,
type,
expiredTime: { $gte: Date.now() + 4 * 60 * 1000 } // 如果有一个记录的过期时间,大于当前+4分钟,说明距离上次发送还没到1分钟。(因为默认创建时,过期时间是未来5分钟)
});
if (authCode) {
throw new Error('请勿频繁获取验证码');
}
// 创建 auth 记录
await AuthCode.create({
email,
type,
code
});
// 发送验证码
await sendCode(email as string, code, type as `${EmailTypeEnum}`);
jsonRes(res, {
message: '发送验证码成功'
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { User } from '@/service/models/user';
import { authToken } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少参数');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 根据 id 获取用户信息
const user = await User.findById(userId);
if (!user) {
throw new Error('账号异常');
}
jsonRes(res, {
data: user
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { User } from '@/service/models/user';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { UserUpdateParams } from '@/types/user';
/* 更新一些基本信息 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { accounts } = req.body as UserUpdateParams;
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少参数');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 更新对应的记录
await User.updateOne(
{
_id: userId
},
{
// 限定字段
...(accounts ? { accounts } : {})
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { User } from '@/service/models/user';
import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo';
import { generateToken } from '@/service/utils/tools';
import { EmailTypeEnum } from '@/constants/common';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { email, code, password } = req.body;
if (!email || !code || !password) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 验证码校验
const authCode = await AuthCode.findOne({
email,
code,
type: EmailTypeEnum.findPassword,
expiredTime: { $gte: Date.now() }
});
if (!authCode) {
throw new Error('验证码错误');
}
// 更新对应的记录
await User.updateOne(
{
email
},
{
password
}
);
// 根据 email 获取用户信息
const user = await User.findOne({
email
});
jsonRes(res, {
data: {
token: generateToken(user._id),
user
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
import React, { useCallback, useState, useRef, useMemo } from 'react';
import { useRouter } from 'next/router';
import Image from 'next/image';
import {
getInitChatSiteInfo,
postGPT3SendPrompt,
getChatGPTSendEvent,
postChatGptPrompt,
delLastMessage
} from '@/api/chat';
import { ChatSiteItemType, ChatSiteType } from '@/types/chat';
import { Textarea, Box, Flex, Button } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon';
import { useScreen } from '@/hooks/useScreen';
import Markdown from '@/components/Markdown';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import { OpenAiModelEnum } from '@/constants/model';
const Chat = () => {
const { toast } = useToast();
const router = useRouter();
const { media } = useScreen();
const { chatId, windowId } = router.query as { chatId: string; windowId?: string };
const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null);
const [chatSiteData, setChatSiteData] = useState<ChatSiteType>(); // 聊天框整体数据
const [chatList, setChatList] = useState<ChatSiteItemType[]>([]); // 对话内容
const [inputVal, setInputVal] = useState(''); // 输入的内容
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]);
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]);
const { Loading } = useLoading();
// 滚动到底部
const scrollToBottom = useCallback(() => {
// 滚动到底部
setTimeout(() => {
ChatBox.current &&
ChatBox.current.scrollTo({
top: ChatBox.current.scrollHeight,
behavior: 'smooth'
});
}, 100);
}, []);
// 初始化聊天框
useQuery([chatId, windowId], () => (chatId ? getInitChatSiteInfo(chatId, windowId) : null), {
cacheTime: 5 * 60 * 1000,
onSuccess(res) {
if (!res) return;
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`);
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item,
status: 'finish'
}))
);
scrollToBottom();
},
onError() {
toast({
title: '初始化异常',
status: 'error'
});
}
});
// gpt3 方法
const gpt3ChatPrompt = useCallback(
async (newChatList: ChatSiteItemType[]) => {
// 请求内容
const response = await postGPT3SendPrompt({
prompt: newChatList,
chatId: chatId as string
});
// 更新 AI 的内容
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
value: response
};
})
);
},
[chatId]
);
// chatGPT
const chatGPTPrompt = useCallback(
async (newChatList: ChatSiteItemType[]) => {
if (!windowId) return;
/* 预请求,把消息存入库 */
await postChatGptPrompt({
windowId,
prompt: newChatList[newChatList.length - 1],
chatId
});
return new Promise((resolve, reject) => {
const event = getChatGPTSendEvent(chatId, windowId);
event.onmessage = ({ data }) => {
if (data === '[DONE]') {
event.close();
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
resolve('');
} else if (data) {
const msg = data.replace(/<br\/>/g, '\n');
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + msg
};
})
);
}
};
event.onerror = (err) => {
console.error(err, '===');
event.close();
reject('对话出现错误');
};
});
},
[chatId, windowId]
);
/**
* 发送一个内容
*/
const sendPrompt = useCallback(async () => {
const storeInput = inputVal;
// 去除空行
const val = inputVal
.trim()
.split('\n')
.filter((val) => val)
.join('\n\n');
if (!chatSiteData?.modelId || !val || !ChatBox.current || isChatting) {
return;
}
const newChatList: ChatSiteItemType[] = [
...chatList,
{
obj: 'Human',
value: val,
status: 'finish'
},
{
obj: 'AI',
value: '',
status: 'loading'
}
];
// 插入内容
setChatList(newChatList);
setInputVal('');
// 滚动到底部
setTimeout(() => {
scrollToBottom();
if (TextareaDom.current) {
TextareaDom.current.style.height = 22 + 'px';
}
}, 100);
const fnMap: { [key: string]: any } = {
[OpenAiModelEnum.GPT35]: chatGPTPrompt,
[OpenAiModelEnum.GPT3]: gpt3ChatPrompt
};
try {
/* 对长度进行限制 */
const maxContext = chatSiteData.secret.contextMaxLen;
const requestPrompt =
newChatList.length > maxContext + 2
? [newChatList[0], ...newChatList.slice(newChatList.length - maxContext - 1, -1)]
: newChatList.slice(0, newChatList.length - 1);
if (typeof fnMap[chatSiteData.chatModel] === 'function') {
await fnMap[chatSiteData.chatModel](requestPrompt);
}
} catch (err) {
toast({
title: typeof err === 'string' ? err : '聊天已过期',
status: 'warning',
duration: 5000,
isClosable: true
});
setInputVal(storeInput);
setChatList(newChatList.slice(0, newChatList.length - 2));
}
}, [
chatGPTPrompt,
chatList,
chatSiteData,
gpt3ChatPrompt,
inputVal,
isChatting,
scrollToBottom,
toast
]);
// 重新编辑
const reEdit = useCallback(async () => {
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
// 删除数据库最后一句
delLastMessage(windowId);
const val = chatList[chatList.length - 1].value;
setInputVal(val);
setChatList(chatList.slice(0, -1));
setTimeout(() => {
if (TextareaDom.current) {
TextareaDom.current.style.height = val.split('\n').length * 22 + 'px';
}
}, 100);
}, [chatList, windowId]);
return (
<Flex h={'100vh'} flexDirection={'column'} overflowY={'hidden'}>
{/* 头部 */}
<Flex
px={4}
h={'50px'}
alignItems={'center'}
backgroundColor={'white'}
boxShadow={'0 5px 10px rgba(0,0,0,0.1)'}
zIndex={1}
>
<Box flex={1}>{chatSiteData?.name}</Box>
{/* 重置按键 */}
<Box cursor={'pointer'} onClick={() => router.replace(`/chat?chatId=${chatId}`)}>
<Icon name={'icon-zhongzhi'} width={20} height={20} color={'#718096'}></Icon>
</Box>
{/* 滚动到底部按键 */}
{/* 滚动到底部 */}
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && (
<Box ml={10} cursor={'pointer'} onClick={scrollToBottom}>
<Icon
name={'icon-xiangxiazhankai-xianxingyuankuang'}
width={25}
height={25}
color={'#718096'}
></Icon>
</Box>
)}
</Flex>
{/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}>
{chatList.map((item, index) => (
<Box
key={index}
py={media(9, 6)}
px={media(4, 3)}
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'}
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={4}>
<Image
src={item.obj === 'Human' ? '/imgs/human.png' : '/imgs/modelAvatar.png'}
alt="/imgs/modelAvatar.png"
width={30}
height={30}
></Image>
</Box>
<Box flex={'1 0 0'} w={0} overflowX={'auto'}>
<Markdown
source={item.value}
isChatting={isChatting && index === chatList.length - 1}
/>
</Box>
</Flex>
</Box>
))}
</Box>
<Box
m={media('20px auto', '0 auto')}
w={media('100vw', '100%')}
maxW={'800px'}
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
>
{lastWordHuman ? (
<Box textAlign={'center'}>
<Box color={'red'}>对话出现了异常</Box>
<Flex py={5} justifyContent={'center'}>
<Button
mr={20}
onClick={() => router.replace(`/chat?chatId=${chatId}`)}
colorScheme={'green'}
>
重开对话
</Button>
<Button onClick={reEdit}>重新编辑最后一句</Button>
</Flex>
</Box>
) : (
<Box
py={5}
position={'relative'}
boxShadow={'base'}
overflow={'hidden'}
borderRadius={media('md', 'none')}
>
{/* 输入框 */}
<Textarea
ref={TextareaDom}
w={'100%'}
pr={'45px'}
py={0}
border={'none'}
_focusVisible={{
border: 'none'
}}
placeholder="提问"
resize={'none'}
value={inputVal}
rows={1}
height={'22px'}
lineHeight={'22px'}
maxHeight={'150px'}
maxLength={chatSiteData?.secret.contentMaxLen || -1}
overflowY={'auto'}
onChange={(e) => {
const textarea = e.target;
setInputVal(textarea.value);
textarea.style.height = textarea.value.split('\n').length * 22 + 'px';
}}
onKeyDown={(e) => {
// 触发快捷发送
if (e.keyCode === 13 && !e.shiftKey) {
sendPrompt();
e.preventDefault();
}
// 全选内容
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
}}
/>
{/* 发送和等待按键 */}
<Box position={'absolute'} bottom={5} right={media('20px', '10px')}>
{isChatting ? (
<Image
style={{ transform: 'translateY(4px)' }}
src={'/icon/chatting.svg'}
width={30}
height={30}
alt={''}
/>
) : (
<Box cursor={'pointer'} onClick={sendPrompt}>
<Icon name={'icon-fasong'} width={20} height={20} color={'#718096'}></Icon>
</Box>
)}
</Box>
</Box>
)}
</Box>
<Loading loading={!chatSiteData} />
</Flex>
);
};
export default Chat;
import Head from 'next/head' import React, { useEffect } from 'react';
import Image from 'next/image' import { useRouter } from 'next/router';
import { Inter } from '@next/font/google' import { Card, Text, Box, Heading, Flex } from '@chakra-ui/react';
import styles from '@/styles/Home.module.css' import Markdown from '@/components/Markdown';
import { introPage } from '@/constants/common';
const inter = Inter({ subsets: ['latin'] }) const Home = () => {
const router = useRouter();
export default function Home() {
return ( return (
<> <Card p={5} lineHeight={2}>
<Head> <Markdown source={introPage} isChatting={false} />
<title>Create Next App</title> </Card>
<meta name="description" content="Generated by create next app" /> );
<meta name="viewport" content="width=device-width, initial-scale=1" /> };
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.description}>
<p>
Get started by editing&nbsp;
<code className={styles.code}>src/pages/index.tsx</code>
</p>
<div>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className={styles.vercelLogo}
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className={styles.center}> export default Home;
<Image
className={styles.logo}
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
<div className={styles.thirteen}>
<Image
src="/thirteen.svg"
alt="13"
width={40}
height={31}
priority
/>
</div>
</div>
<div className={styles.grid}>
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Docs <span>-&gt;</span>
</h2>
<p className={inter.className}>
Find in-depth information about Next.js features and&nbsp;API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Learn <span>-&gt;</span>
</h2>
<p className={inter.className}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Templates <span>-&gt;</span>
</h2>
<p className={inter.className}>
Discover and deploy boilerplate example Next.js&nbsp;projects.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
target="_blank"
rel="noopener noreferrer"
>
<h2 className={inter.className}>
Deploy <span>-&gt;</span>
</h2>
<p className={inter.className}>
Instantly deploy your Next.js site to a shareable URL
with&nbsp;Vercel.
</p>
</a>
</div>
</main>
</>
)
}
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '../../../constants/user';
import { postFindPassword } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>;
loginSuccess: (e: ResLogin) => void;
}
interface RegisterType {
email: string;
code: string;
password: string;
password2: string;
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { mediaLgMd } = useScreen();
const {
register,
handleSubmit,
getValues,
trigger,
formState: { errors }
} = useForm<RegisterType>({
mode: 'onBlur'
});
const { codeSending, sendCodeText, sendCode, codeCountDown } = useSendCode();
const onclickSendCode = useCallback(async () => {
const check = await trigger('email');
if (!check) return;
sendCode({
email: getValues('email'),
type: 'findPassword'
});
}, [getValues, sendCode, trigger]);
const [requesting, setRequesting] = useState(false);
const onclickFindPassword = useCallback(
async ({ email, code, password }: RegisterType) => {
setRequesting(true);
try {
loginSuccess(
await postFindPassword({
email,
code,
password
})
);
toast({
title: `密码已找回`,
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
}
setRequesting(false);
},
[loginSuccess, toast]
);
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
找回 DocGPT 账号
</Box>
<form onSubmit={handleSubmit(onclickFindPassword)}>
<FormControl mt={8} isInvalid={!!errors.email}>
<Input
placeholder="邮箱"
size={mediaLgMd}
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/,
message: '邮箱错误'
}
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.email && errors.email.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.email}>
<Flex>
<Input
flex={1}
placeholder="验证码"
size={mediaLgMd}
{...register('code', {
required: '验证码不能为空'
})}
></Input>
<Button
ml={5}
w={'145px'}
maxW={'50%'}
size={mediaLgMd}
onClick={onclickSendCode}
isDisabled={codeCountDown > 0}
isLoading={codeSending}
>
{sendCodeText}
</Button>
</Flex>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.code && errors.code.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.password}>
<Input
type={'password'}
placeholder="新密码"
size={mediaLgMd}
{...register('password', {
required: '密码不能为空',
minLength: {
value: 4,
message: '密码最少4位最多12位'
},
maxLength: {
value: 12,
message: '密码最少4位最多12位'
}
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.password2}>
<Input
type={'password'}
placeholder="确认密码"
size={mediaLgMd}
{...register('password2', {
validate: (val) => (getValues('password') === val ? true : '两次密码不一致')
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.password2 && errors.password2.message}
</FormErrorMessage>
</FormControl>
<Box
float={'right'}
fontSize="sm"
mt={2}
color={'blue.600'}
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}
onClick={() => setPageType('login')}
>
去登录
</Box>
<Button
type="submit"
mt={8}
w={'100%'}
size={mediaLgMd}
colorScheme="blue"
isLoading={requesting}
>
找回密码
</Button>
</form>
</>
);
};
export default RegisterForm;
import React, { useState, Dispatch, useCallback } from 'react';
import { FormControl, Flex, Input, Button, FormErrorMessage, Box } from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '@/constants/user';
import { postLogin } from '@/api/user';
import type { ResLogin } from '@/api/response/user';
import { useToast } from '@/hooks/useToast';
import { useScreen } from '@/hooks/useScreen';
interface Props {
setPageType: Dispatch<`${PageTypeEnum}`>;
loginSuccess: (e: ResLogin) => void;
}
interface LoginFormType {
email: string;
password: string;
}
const LoginForm = ({ setPageType, loginSuccess }: Props) => {
const { toast } = useToast();
const { mediaLgMd } = useScreen();
const {
register,
handleSubmit,
formState: { errors }
} = useForm<LoginFormType>();
const [requesting, setRequesting] = useState(false);
const onclickLogin = useCallback(
async ({ email, password }: LoginFormType) => {
setRequesting(true);
try {
loginSuccess(
await postLogin({
email,
password
})
);
toast({
title: '登录成功',
status: 'success'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
}
setRequesting(false);
},
[loginSuccess, toast]
);
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
登录 DocGPT
</Box>
<form onSubmit={handleSubmit(onclickLogin)}>
<FormControl mt={8} isInvalid={!!errors.email}>
<Input
placeholder="邮箱"
size={mediaLgMd}
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/,
message: '邮箱错误'
}
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.email && errors.email.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.password}>
<Input
type={'password'}
size={mediaLgMd}
placeholder="密码"
{...register('password', {
required: '密码不能为空',
minLength: {
value: 4,
message: '密码最少4位最多12位'
},
maxLength: {
value: 12,
message: '密码最少4位最多12位'
}
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<Flex align={'center'} justifyContent={'space-between'} mt={6} color={'blue.600'}>
<Box
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}
onClick={() => setPageType('forgetPassword')}
fontSize="sm"
>
忘记密码?
</Box>
<Box
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}
onClick={() => setPageType('register')}
fontSize="sm"
>
注册账号
</Box>
</Flex>
<Button
type="submit"
mt={8}
w={'100%'}
size={mediaLgMd}
colorScheme="blue"
isLoading={requesting}
>
登录
</Button>
</form>
</>
);
};
export default LoginForm;
import React, { useState, Dispatch, useCallback } from 'react';
import {
FormControl,
Box,
Input,
Button,
FormErrorMessage,
useToast,
Flex
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { PageTypeEnum } from '@/constants/user';
import { postRegister } from '@/api/user';
import { useSendCode } from '@/hooks/useSendCode';
import type { ResLogin } from '@/api/response/user';
import { useScreen } from '@/hooks/useScreen';
interface Props {
loginSuccess: (e: ResLogin) => void;
setPageType: Dispatch<`${PageTypeEnum}`>;
}
interface RegisterType {
email: string;
password: string;
password2: string;
code: string;
}
const RegisterForm = ({ setPageType, loginSuccess }: Props) => {
const toast = useToast();
const { mediaLgMd } = useScreen();
const {
register,
handleSubmit,
getValues,
trigger,
formState: { errors }
} = useForm<RegisterType>({
mode: 'onBlur'
});
const { codeSending, sendCodeText, sendCode, codeCountDown } = useSendCode();
const onclickSendCode = useCallback(async () => {
const check = await trigger('email');
if (!check) return;
sendCode({
email: getValues('email'),
type: 'register'
});
}, [getValues, sendCode, trigger]);
const [requesting, setRequesting] = useState(false);
const onclickRegister = useCallback(
async ({ email, password, code }: RegisterType) => {
setRequesting(true);
try {
loginSuccess(
await postRegister({
email,
code,
password
})
);
toast({
title: `注册成功`,
status: 'success',
position: 'top'
});
} catch (error) {
typeof error === 'string' &&
toast({
title: error,
status: 'error',
position: 'top'
});
}
setRequesting(false);
},
[loginSuccess, toast]
);
return (
<>
<Box fontWeight={'bold'} fontSize={'2xl'} textAlign={'center'}>
注册 DocGPT 账号
</Box>
<form onSubmit={handleSubmit(onclickRegister)}>
<FormControl mt={8} isInvalid={!!errors.email}>
<Input
placeholder="邮箱"
size={mediaLgMd}
{...register('email', {
required: '邮箱不能为空',
pattern: {
value: /^[A-Za-z0-9]+([_\.][A-Za-z0-9]+)*@([A-Za-z0-9\-]+\.)+[A-Za-z]{2,6}$/,
message: '邮箱错误'
}
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.email && errors.email.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.email}>
<Flex>
<Input
flex={1}
size={mediaLgMd}
placeholder="验证码"
{...register('code', {
required: '验证码不能为空'
})}
></Input>
<Button
ml={5}
w={'145px'}
maxW={'50%'}
size={mediaLgMd}
onClick={onclickSendCode}
isDisabled={codeCountDown > 0}
isLoading={codeSending}
>
{sendCodeText}
</Button>
</Flex>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.code && errors.code.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.password}>
<Input
type={'password'}
placeholder="密码"
size={mediaLgMd}
{...register('password', {
required: '密码不能为空',
minLength: {
value: 4,
message: '密码最少4位最多12位'
},
maxLength: {
value: 12,
message: '密码最少4位最多12位'
}
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
<FormControl mt={8} isInvalid={!!errors.password2}>
<Input
type={'password'}
placeholder="确认密码"
size={mediaLgMd}
{...register('password2', {
validate: (val) => (getValues('password') === val ? true : '两次密码不一致')
})}
></Input>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.password2 && errors.password2.message}
</FormErrorMessage>
</FormControl>
<Box
float={'right'}
fontSize="sm"
mt={2}
color={'blue.600'}
cursor={'pointer'}
_hover={{ textDecoration: 'underline' }}
onClick={() => setPageType('login')}
>
已有账号,去登录
</Box>
<Button
type="submit"
mt={8}
w={'100%'}
size={mediaLgMd}
colorScheme="blue"
isLoading={requesting}
>
确认注册
</Button>
</form>
</>
);
};
export default RegisterForm;
.loginPage {
background: url('/icon/login-bg.svg') no-repeat;
background-size: cover;
height: 100vh;
width: 100vw;
user-select: none;
}
import React, { useState, useCallback } from 'react';
import styles from './index.module.scss';
import { Box, Flex, Image } from '@chakra-ui/react';
import { PageTypeEnum } from '@/constants/user';
import LoginForm from './components/LoginForm';
import RegisterForm from './components/RegisterForm';
import ForgetPasswordForm from './components/ForgetPasswordForm';
import { useScreen } from '@/hooks/useScreen';
import type { ResLogin } from '@/api/response/user';
import { useRouter } from 'next/router';
import { useUserStore } from '@/store/user';
const Login = () => {
const router = useRouter();
const { isPc } = useScreen();
const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login);
const { setUserInfo } = useUserStore();
const loginSuccess = useCallback(
(res: ResLogin) => {
setUserInfo(res.user, res.token);
router.push('/');
},
[router, setUserInfo]
);
const map = {
[PageTypeEnum.login]: {
Component: <LoginForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
},
[PageTypeEnum.register]: {
Component: <RegisterForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
},
[PageTypeEnum.forgetPassword]: {
Component: <ForgetPasswordForm setPageType={setPageType} loginSuccess={loginSuccess} />,
img: '/icon/loginLeft.svg'
}
};
return (
<Box className={styles.loginPage} p={isPc ? '10vh 10vw' : 0}>
<Flex
maxW={'1240px'}
m={'auto'}
backgroundColor={'#fff'}
height="100%"
alignItems={'center'}
justifyContent={'center'}
p={10}
borderRadius={isPc ? 'md' : 'none'}
gap={5}
>
{isPc && (
<Image
src={map[pageType].img}
order={pageType === PageTypeEnum.login ? 0 : 2}
flex={'1 0 0'}
w="0"
maxW={'600px'}
height={'100%'}
maxH={'450px'}
alt=""
/>
)}
<Box
order={1}
flex={`0 0 ${isPc ? '400px' : '100%'}`}
height={'100%'}
maxH={'450px'}
border="1px"
borderColor="gray.200"
py={5}
px={10}
borderRadius={isPc ? 'md' : 'none'}
>
{map[pageType].Component}
</Box>
</Flex>
</Box>
);
};
export default Login;
import React, { Dispatch, useState, useCallback } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
FormControl,
FormErrorMessage,
Button,
useToast,
Input,
Select
} from '@chakra-ui/react';
import { useForm } from 'react-hook-form';
import { postCreateModel } from '@/api/model';
import { ModelType } from '@/types/model';
import { OpenAiList } from '@/constants/model';
interface CreateFormType {
name: string;
serviceModelName: string;
}
const CreateModel = ({
isOpen,
setCreateModelOpen,
onSuccess
}: {
isOpen: boolean;
setCreateModelOpen: Dispatch<boolean>;
onSuccess: Dispatch<ModelType>;
}) => {
const [requesting, setRequesting] = useState(false);
const toast = useToast({
duration: 2000,
position: 'top'
});
const {
register,
handleSubmit,
formState: { errors }
} = useForm<CreateFormType>({
defaultValues: {
serviceModelName: OpenAiList[0].model
}
});
const handleCreateModel = useCallback(
async (data: CreateFormType) => {
setRequesting(true);
try {
const res = await postCreateModel(data);
toast({
title: '创建成功',
status: 'success'
});
onSuccess(res);
setCreateModelOpen(false);
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err.message || '出现了意外',
status: 'error'
});
}
setRequesting(false);
},
[onSuccess, setCreateModelOpen, toast]
);
return (
<>
<Modal isOpen={isOpen} onClose={() => setCreateModelOpen(false)}>
<ModalOverlay />
<ModalContent>
<ModalHeader>创建模型</ModalHeader>
<ModalCloseButton />
<ModalBody>
<FormControl mb={8} isInvalid={!!errors.name}>
<Input
placeholder="模型名称"
{...register('name', {
required: '模型名不能为空'
})}
/>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.name && errors.name.message}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={!!errors.serviceModelName}>
<Select
placeholder="选择基础模型类型"
{...register('serviceModelName', {
required: '底层模型不能为空'
})}
>
{OpenAiList.map((item) => (
<option key={item.model} value={item.model}>
{item.name}
</option>
))}
</Select>
<FormErrorMessage position={'absolute'} fontSize="xs">
{!!errors.serviceModelName && errors.serviceModelName.message}
</FormErrorMessage>
</FormControl>
</ModalBody>
<ModalFooter>
<Button mr={3} colorScheme={'gray'} onClick={() => setCreateModelOpen(false)}>
取消
</Button>
<Button isLoading={requesting} onClick={handleSubmit(handleCreateModel)}>
确认创建
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default CreateModel;
import React, { useCallback } from 'react';
import { Grid, Box, Card, Flex, Button, FormControl, Input, Textarea } from '@chakra-ui/react';
import type { ModelType } from '@/types/model';
import { useForm } from 'react-hook-form';
import { useToast } from '@/hooks/useToast';
import { putModelById } from '@/api/model';
import { useScreen } from '@/hooks/useScreen';
import { useGlobalStore } from '@/store/global';
const ModelEditForm = ({ model }: { model: ModelType }) => {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<ModelType>({
defaultValues: model
});
const { setLoading } = useGlobalStore();
const { toast } = useToast();
const { isPc } = useScreen();
const onclickSave = useCallback(
async (data: ModelType) => {
setLoading(true);
try {
await putModelById(data._id, {
name: data.name,
systemPrompt: data.systemPrompt,
service: data.service,
security: data.security
});
toast({
title: '更新成功',
status: 'success'
});
} catch (err) {
console.log(err);
toast({
title: err as string,
status: 'success'
});
}
setLoading(false);
},
[setLoading, toast]
);
const submitError = useCallback(() => {
// deep search message
const deepSearch = (obj: any): string => {
if (!obj) return '提交表单错误';
if (!!obj.message) {
return obj.message;
}
return deepSearch(Object.values(obj)[0]);
};
toast({
title: deepSearch(errors),
status: 'error',
duration: 4000,
isClosable: true
});
}, [errors, toast]);
return (
<Grid gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
<Card p={4}>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<Box fontWeight={'bold'} fontSize={'lg'}>
修改模型信息
</Box>
<Button onClick={handleSubmit(onclickSave, submitError)}>保存</Button>
</Flex>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'}>展示名称:</Box>
<Input
{...register('name', {
required: '展示名称不能为空'
})}
></Input>
</Flex>
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 80px'}>对话模型:</Box>
<Box>{model.service.modelName}</Box>
</Flex>
</FormControl>
<FormControl mt={5}>
<Textarea
rows={4}
maxLength={500}
{...register('systemPrompt')}
placeholder={'系统的提示词,会在进入聊天时放置在第一句,用于限定模型的聊天范围'}
/>
</FormControl>
</Card>
<Card p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}>
安全策略
</Box>
<FormControl mt={2}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'}>单句最大长度:</Box>
<Input
flex={1}
type={'number'}
{...register('security.contentMaxLen', {
required: '单句长度不能为空',
min: {
value: 0,
message: '单句长度最小为0'
},
max: {
value: 4000,
message: '单句长度最长为4000'
},
valueAsNumber: true
})}
></Input>
</Flex>
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'}>上下文最大长度:</Box>
<Input
flex={1}
type={'number'}
{...register('security.contextMaxLen', {
required: '上下文长度不能为空',
min: {
value: 1,
message: '上下文长度最小为5'
},
max: {
value: 400000,
message: '上下文长度最长为 400000'
},
valueAsNumber: true
})}
></Input>
</Flex>
</FormControl>
<FormControl mt={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 120px'}>聊天过期时间:</Box>
<Input
flex={1}
type={'number'}
{...register('security.expiredTime', {
required: '聊天过期时间不能为空',
min: {
value: 0.1,
message: '聊天过期时间最小为0.1小时'
},
max: {
value: 999999,
message: '聊天过期时间最长为 999999 小时'
},
valueAsNumber: true
})}
></Input>
<Box ml={3}>小时</Box>
</Flex>
</FormControl>
<FormControl mt={5} pb={5}>
<Flex alignItems={'center'}>
<Box flex={'0 0 130px'}>聊天最大加载次数:</Box>
<Box flex={1}>
<Input
type={'number'}
{...register('security.maxLoadAmount', {
required: '聊天最大加载次数不能为空',
max: {
value: 999999,
message: '聊天最大加载次数最小为 999999 次'
},
valueAsNumber: true
})}
></Input>
<Box fontSize={'sm'} color={'blackAlpha.400'} position={'absolute'}>
设置为-1代表不限制次数
</Box>
</Box>
<Box ml={3}>次</Box>
</Flex>
</FormControl>
</Card>
</Grid>
);
};
export default ModelEditForm;
import React from 'react';
import { Box, Button, Flex, Heading, Tag } from '@chakra-ui/react';
import type { ModelType } from '@/types/model';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
import { useRouter } from 'next/router';
const ModelPhoneList = ({
models,
handlePreviewChat
}: {
models: ModelType[];
handlePreviewChat: (_: string) => void;
}) => {
const router = useRouter();
return (
<Box borderRadius={'md'} overflow={'hidden'} mb={5}>
{models.map((model) => (
<Box
key={model._id}
_notFirst={{ borderTop: '1px solid rgba(0,0,0,0.1)' }}
px={6}
py={3}
backgroundColor={'white'}
>
<Flex alignItems={'flex-start'}>
<Box flex={'1 0 0'} w={0} fontSize={'lg'} fontWeight={'bold'}>
{model.name}
</Box>
<Tag
colorScheme={formatModelStatus[model.status].colorTheme}
variant="solid"
px={3}
size={'md'}
>
{formatModelStatus[model.status].text}
</Tag>
</Flex>
<Flex mt={5}>
<Box flex={'0 0 100px'}>最后更新时间: </Box>
<Box color={'blackAlpha.500'}>{dayjs(model.updateTime).format('YYYY-MM-DD HH:mm')}</Box>
</Flex>
<Flex mt={5}>
<Box flex={'0 0 100px'}>AI模型: </Box>
<Box color={'blackAlpha.500'}>{model.service.modelName}</Box>
</Flex>
<Flex mt={5}>
<Box flex={'0 0 100px'}>训练次数: </Box>
<Box color={'blackAlpha.500'}>{model.trainingTimes}次</Box>
</Flex>
<Flex mt={5} justifyContent={'flex-end'}>
<Button
mr={3}
variant={'outline'}
w={'100px'}
size={'sm'}
onClick={() => handlePreviewChat(model._id)}
>
体验
</Button>
<Button
size={'sm'}
w={'100px'}
onClick={() => router.push(`/model/detail?modelId=${model._id}`)}
>
编辑
</Button>
</Flex>
</Box>
))}
</Box>
);
};
export default ModelPhoneList;
import React from 'react';
import {
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Tag,
Card,
Box
} from '@chakra-ui/react';
import { formatModelStatus } from '@/constants/model';
import dayjs from 'dayjs';
import type { ModelType } from '@/types/model';
import { useRouter } from 'next/router';
const ModelTable = ({
models = [],
handlePreviewChat
}: {
models: ModelType[];
handlePreviewChat: (_: string) => void;
}) => {
const router = useRouter();
const columns = [
{
title: '模型名',
key: 'name',
dataIndex: 'name'
},
{
title: '最后更新时间',
key: 'updateTime',
render: (item: ModelType) => dayjs(item.updateTime).format('YYYY-MM-DD HH:mm')
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
render: (item: ModelType) => (
<Tag
colorScheme={formatModelStatus[item.status].colorTheme}
variant="solid"
px={3}
size={'md'}
>
{formatModelStatus[item.status].text}
</Tag>
)
},
{
title: 'AI模型',
key: 'service',
render: (item: ModelType) => (
<Box wordBreak={'break-all'} whiteSpace={'pre-wrap'} maxW={'200px'}>
{item.service.modelName}
</Box>
)
},
{
title: '训练次数',
key: 'trainingTimes',
dataIndex: 'trainingTimes'
},
{
title: '操作',
key: 'control',
render: (item: ModelType) => (
<>
<Button mr={3} onClick={() => handlePreviewChat(item._id)}>
对话
</Button>
<Button
colorScheme={'gray'}
onClick={() => router.push(`/model/detail?modelId=${item._id}`)}
>
编辑
</Button>
</>
)
}
];
return (
<Card py={3}>
<TableContainer>
<Table variant={'simple'}>
<Thead>
<Tr>
{columns.map((item) => (
<Th key={item.key}>{item.title}</Th>
))}
</Tr>
</Thead>
<Tbody>
{models.map((item) => (
<Tr key={item._id}>
{columns.map((col) => (
<Td key={col.key}>
{col.render
? col.render(item)
: !!col.dataIndex
? // @ts-ignore nextline
item[col.dataIndex]
: ''}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Card>
);
};
export default ModelTable;
import React, { useEffect, useCallback, useState } from 'react';
import { Box, Card, TableContainer, Table, Thead, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import { ModelType } from '@/types/model';
import { getModelTrainings } from '@/api/model';
import type { TrainingItemType } from '@/types/training';
const Training = ({ model }: { model: ModelType }) => {
const columns: {
title: string;
key: keyof TrainingItemType;
dataIndex: string;
}[] = [
{
title: '训练ID',
key: 'tuneId',
dataIndex: 'tuneId'
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
}
];
const [records, setRecords] = useState<TrainingItemType[]>([]);
const loadTrainingRecords = useCallback(async (id: string) => {
try {
const res = await getModelTrainings(id);
setRecords(res);
} catch (error) {
console.log(error);
}
}, []);
useEffect(() => {
model._id && loadTrainingRecords(model._id);
}, [loadTrainingRecords, model]);
return (
<Card p={4} h={'100%'}>
<Box fontWeight={'bold'} fontSize={'lg'}>
训练记录: {model.trainingTimes}次
</Box>
<TableContainer mt={4}>
<Table variant={'simple'}>
<Thead>
<Tr>
{columns.map((item) => (
<Th key={item.key}>{item.title}</Th>
))}
</Tr>
</Thead>
<Tbody>
{records.map((item) => (
<Tr key={item._id}>
{columns.map((col) => (
// @ts-ignore
<Td key={col.key}>{item[col.dataIndex]}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Card>
);
};
export default Training;
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { useRouter } from 'next/router';
import { getModelById, delModelById, postTrainModel, putModelTrainingStatus } from '@/api/model';
import { getChatSiteId } from '@/api/chat';
import type { ModelType } from '@/types/model';
import { Card, Box, Flex, Button, Tag, Grid } from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useConfirm } from '@/hooks/useConfirm';
import { formatModelStatus, ModelStatusEnum, OpenAiList } from '@/constants/model';
import { useGlobalStore } from '@/store/global';
import { useScreen } from '@/hooks/useScreen';
import ModelEditForm from './components/ModelEditForm';
import Icon from '@/components/Icon';
import Training from './components/Training';
const ModelDetail = () => {
const { toast } = useToast();
const router = useRouter();
const { isPc } = useScreen();
const { setLoading } = useGlobalStore();
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认删除该模型?'
});
const SelectFileDom = useRef<HTMLInputElement>(null);
const { modelId } = router.query as { modelId: string };
const [model, setModel] = useState<ModelType>();
const canTrain = useMemo(() => {
const openai = OpenAiList.find((item) => item.model === model?.service.modelName);
return openai && openai.canTraining === true;
}, [model]);
/* 加载模型数据 */
const loadModel = useCallback(async () => {
if (!modelId) return;
setLoading(true);
try {
const res = await getModelById(modelId as string);
res.security.expiredTime /= 60 * 60 * 1000;
setModel(res);
console.log(res);
} catch (err) {
console.log(err);
}
setLoading(false);
}, [modelId, setLoading]);
useEffect(() => {
loadModel();
}, [loadModel, modelId]);
/* 点击删除 */
const handleDelModel = useCallback(async () => {
if (!model) return;
setLoading(true);
try {
await delModelById(model._id);
toast({
title: '删除成功',
status: 'success'
});
router.replace('/model/list');
} catch (err) {
console.log(err);
}
setLoading(false);
}, [setLoading, model, router, toast]);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(async () => {
if (!model) return;
setLoading(true);
try {
const chatId = await getChatSiteId(model._id);
router.push(`/chat?chatId=${chatId}`);
} catch (err) {
console.log(err);
}
setLoading(false);
}, [setLoading, model, router]);
/* 上传数据集,触发微调 */
const startTraining = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
if (!modelId || !e.target.files || e.target.files?.length === 0) return;
setLoading(true);
try {
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
await postTrainModel(modelId, formData);
toast({
title: '开始训练,大约需要 30 分钟',
status: 'success'
});
// 重新获取模型
loadModel();
} catch (err) {
toast({
title: typeof err === 'string' ? err : '文件格式错误',
status: 'error'
});
console.log(err);
}
setLoading(false);
},
[setLoading, loadModel, modelId, toast]
);
/* 点击更新模型状态 */
const handleClickUpdateStatus = useCallback(async () => {
if (!model || model.status !== ModelStatusEnum.training) return;
setLoading(true);
try {
await putModelTrainingStatus(model._id);
loadModel();
} catch (error) {
console.log(error);
}
setLoading(false);
}, [setLoading, loadModel, model]);
return (
<>
{!!model && (
<>
{/* 头部 */}
<Card px={6} py={3}>
{isPc ? (
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
{model.name} 配置
</Box>
<Tag
ml={2}
variant="solid"
colorScheme={formatModelStatus[model.status].colorTheme}
cursor={model.status === ModelStatusEnum.training ? 'pointer' : 'default'}
onClick={handleClickUpdateStatus}
>
{formatModelStatus[model.status].text}
</Tag>
<Box flex={1} />
<Button variant={'outline'} onClick={handlePreviewChat}>
对话体验
</Button>
</Flex>
) : (
<>
<Flex alignItems={'center'}>
<Box as={'h3'} fontSize={'xl'} fontWeight={'bold'} flex={1}>
{model.name} 配置
</Box>
<Tag ml={2} colorScheme={formatModelStatus[model.status].colorTheme}>
{formatModelStatus[model.status].text}
</Tag>
</Flex>
<Box mt={4} textAlign={'right'}>
<Button variant={'outline'} onClick={handlePreviewChat}>
对话体验
</Button>
</Box>
</>
)}
</Card>
{/* 基本信息编辑 */}
<Box mt={5}>
<ModelEditForm model={model} />
</Box>
{/* 其他配置 */}
<Grid mt={5} gridTemplateColumns={isPc ? '1fr 1fr' : '1fr'} gridGap={5}>
<Training model={model} />
<Card h={'100%'} p={4}>
<Box fontWeight={'bold'} fontSize={'lg'}>
神奇操作
</Box>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>模型微调:</Box>
<Button
size={'sm'}
onClick={() => {
SelectFileDom.current?.click();
}}
title={!canTrain ? '' : '模型不支持微调'}
isDisabled={!canTrain}
>
上传微调数据集
</Button>
<Flex
as={'a'}
href="/TrainingTemplate.jsonl"
download
ml={5}
cursor={'pointer'}
alignItems={'center'}
color={'blue.500'}
>
<Icon name={'icon-yunxiazai'} color={'#3182ce'} />
下载模板
</Flex>
</Flex>
{/* 提示 */}
<Box mt={3} py={3} color={'blackAlpha.500'}>
<Box as={'li'} lineHeight={1.9}>
每行包括一个 prompt 和一个 completion
</Box>
<Box as={'li'} lineHeight={1.9}>
prompt 必须以 \n\n###\n\n 结尾,且尽量保障每个 prompt
内容不都是同一个标点结尾,可以加一个空格打断相同性,
</Box>
<Box as={'li'} lineHeight={1.9}>
completion 开头必须有一个空格,末尾必须以 ### 结尾,同样的不要都是同一个标点结尾。
</Box>
</Box>
<Flex mt={5} alignItems={'center'}>
<Box flex={'0 0 80px'}>删除模型:</Box>
<Button
colorScheme={'red'}
size={'sm'}
onClick={() => {
openConfirm(() => {
handleDelModel();
});
}}
>
删除模型
</Button>
</Flex>
</Card>
</Grid>
</>
)}
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input ref={SelectFileDom} type="file" accept=".jsonl" onChange={startTraining} />
</Box>
<ConfirmChild />
</>
);
};
export default ModelDetail;
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Button, Flex, Card } from '@chakra-ui/react';
import { getMyModels } from '@/api/model';
import { getChatSiteId } from '@/api/chat';
import { ModelType } from '@/types/model';
import CreateModel from './components/CreateModel';
import { useRouter } from 'next/router';
import ModelTable from './components/ModelTable';
import ModelPhoneList from './components/ModelPhoneList';
import { useScreen } from '@/hooks/useScreen';
import { useGlobalStore } from '@/store/global';
const ModelList = () => {
const { isPc } = useScreen();
const router = useRouter();
const [models, setModels] = useState<ModelType[]>([]);
const [openCreateModel, setOpenCreateModel] = useState(false);
const { setLoading } = useGlobalStore();
/* 加载模型 */
const loadModels = useCallback(async () => {
setLoading(true);
try {
const res = await getMyModels();
setModels(res);
} catch (err) {
console.log(err);
}
setLoading(false);
}, [setLoading]);
useEffect(() => {
loadModels();
}, [loadModels]);
/* 创建成功回调 */
const createModelSuccess = useCallback((data: ModelType) => {
setModels((state) => [data, ...state]);
}, []);
/* 点前往聊天预览页 */
const handlePreviewChat = useCallback(
async (modelId: string) => {
setLoading(true);
try {
const chatId = await getChatSiteId(modelId);
router.push(`/chat?chatId=${chatId}`, undefined, {
shallow: true
});
} catch (err) {
console.log(err);
}
setLoading(false);
},
[router, setLoading]
);
return (
<Box position={'relative'}>
{/* 头部 */}
<Card px={6} py={3}>
<Flex alignItems={'center'} justifyContent={'space-between'}>
<Box fontWeight={'bold'} fontSize={'xl'}>
模型列表
</Box>
<Button flex={'0 0 145px'} variant={'outline'} onClick={() => setOpenCreateModel(true)}>
新建模型
</Button>
</Flex>
</Card>
{/* 表单 */}
<Box mt={5} position={'relative'}>
{isPc ? (
<ModelTable models={models} handlePreviewChat={handlePreviewChat} />
) : (
<ModelPhoneList models={models} handlePreviewChat={handlePreviewChat} />
)}
</Box>
{/* 创建弹窗 */}
<CreateModel
isOpen={openCreateModel}
setCreateModelOpen={setOpenCreateModel}
onSuccess={createModelSuccess}
/>
</Box>
);
};
export default ModelList;
import React, { useCallback } from 'react';
import {
Card,
Box,
Flex,
Button,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Select,
Input
} from '@chakra-ui/react';
import { useForm, useFieldArray } from 'react-hook-form';
import { UserUpdateParams } from '@/types/user';
import { putUserInfo } from '@/api/user';
import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import { UserType } from '@/types/user';
const NumberSetting = () => {
const { userInfo, updateUserInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
defaultValues: userInfo as UserType
});
const { toast } = useToast();
const {
fields: accounts,
append: appendAccount,
remove: removeAccount
} = useFieldArray({
control,
name: 'accounts'
});
const onclickSave = useCallback(
async (data: UserUpdateParams) => {
setLoading(true);
try {
await putUserInfo(data);
updateUserInfo(data);
toast({
title: '更新成功',
status: 'success'
});
} catch (error) {}
setLoading(false);
},
[setLoading, toast, updateUserInfo]
);
return (
<>
<Card px={6} py={4}>
<Box fontSize={'xl'} fontWeight={'bold'}>
账号信息
</Box>
<Box mt={6}>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'}>邮箱:</Box>
<Box>{userInfo?.email}</Box>
</Flex>
</Box>
{/* <Box mt={6}>
<Flex alignItems={'center'}>
<Box flex={'0 0 60px'}>余额:</Box>
<Box>
<strong>{userInfo?.balance}</strong> 元
</Box>
<Button size={'sm'} w={'80px'} ml={5}>
充值
</Button>
</Flex>
</Box> */}
</Card>
<Card mt={6} px={6} py={4}>
<Flex mb={5} justifyContent={'space-between'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
关联账号
</Box>
<Box>
{accounts.length === 0 && (
<Button
mr={5}
variant="outline"
onClick={() =>
appendAccount({
type: 'openai',
value: ''
})
}
>
新增
</Button>
)}
<Button onClick={handleSubmit(onclickSave)}>保存</Button>
</Box>
</Flex>
<TableContainer>
<Table>
<Thead>
<Tr>
<Th>账号类型</Th>
<Th>值</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{accounts.map((item, i) => (
<Tr key={item.id}>
<Td minW={'200px'}>
<Select
{...register(`accounts.${i}.type`, {
required: '类型不能为空'
})}
>
<option value="openai">openai</option>
</Select>
</Td>
<Td minW={'200px'} whiteSpace="pre-wrap" wordBreak={'break-all'}>
<Input
{...register(`accounts.${i}.value`, {
required: '账号不能为空'
})}
></Input>
</Td>
<Td>
<Button onClick={() => removeAccount(i)}>删除</Button>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Card>
</>
);
};
export default NumberSetting;
import React from 'react';
import { Card, Box, Flex, Button } from '@chakra-ui/react';
const TrainDataList = () => {
return (
<>
<Card px={6} py={4}>
<Flex alignItems={'center'}>
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
训练数据管理
</Box>
<Button variant={'outline'} mr={6}>
导入数据
</Button>
<Button>插入一条数据</Button>
</Flex>
</Card>
{/* 数据表 */}
</>
);
};
export default TrainDataList;
export enum OpenAiTuneStatusEnum {
cancelled = 'cancelled',
succeeded = 'succeeded',
pending = 'pending'
}
export const openaiError: Record<string, string> = {
context_length_exceeded: '内容超出长度'
};
import { Schema, model, models } from 'mongoose';
const AuthCodeSchema = new Schema({
email: {
type: String,
required: true
},
code: {
type: String,
required: true,
length: 6
},
type: {
type: String,
enum: ['register', 'findPassword'],
required: true
},
expiredTime: {
type: Number,
default: () => Date.now() + 5 * 60 * 1000
}
});
export const AuthCode = models['auth_code'] || model('auth_code', AuthCodeSchema);
import { Schema, model, models } from 'mongoose';
const ChatSchema = new Schema({
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
modelId: {
type: Schema.Types.ObjectId,
ref: 'model',
required: true
},
expiredTime: {
// 过期时间
type: Number,
required: true
},
loadAmount: {
// 剩余加载次数
type: Number,
required: true
}
});
export const Chat = models['chat'] || model('chat', ChatSchema);
import { Schema, model, models } from 'mongoose';
const ChatWindowSchema = new Schema({
chatId: {
type: Schema.Types.ObjectId,
ref: 'chat',
required: true
},
updateTime: {
type: Number,
required: true
},
content: [
{
obj: {
type: String,
required: true,
enum: ['Human', 'AI', 'SYSTEM']
},
value: {
type: String,
required: true
}
}
]
});
export const ChatWindow = models['chatWindow'] || model('chatWindow', ChatWindowSchema);
import { Schema, model, models } from 'mongoose';
const ModelSchema = new Schema({
name: {
type: String,
required: true
},
avatar: {
type: String,
default: '/imgs/modelAvatar.png'
},
systemPrompt: {
type: String,
default: ''
},
userId: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true
},
status: {
type: String,
required: true,
enum: ['waiting', 'running', 'training', 'closed']
},
updateTime: {
type: Date,
default: () => new Date()
},
trainingTimes: {
type: Number,
default: 0
},
service: {
company: {
type: String,
required: true,
enum: ['openai']
},
trainId: {
// 训练时需要的 ID
type: String,
required: true
},
chatModel: {
// 聊天时使用的模型
type: String,
required: true
},
modelName: {
// 底层模型的名称
type: String,
required: true
}
},
security: {
type: {
domain: {
type: [String],
default: ['*']
},
contextMaxLen: {
type: Number,
default: 20
},
contentMaxLen: {
type: Number,
default: 4000
},
expiredTime: {
type: Number,
default: 1,
set: (val: number) => val * (60 * 60 * 1000)
},
maxLoadAmount: {
// 负数代表不限制
type: Number,
default: -1
}
},
default: {},
required: true
}
});
export const Model = models['model'] || model('model', ModelSchema);
import { Schema, model, models } from 'mongoose';
const TrainingSChema = new Schema({
serviceName: {
// 模型厂商名
type: String,
required: true
},
tuneId: {
// 微调进程 ID
type: String,
required: true
},
modelId: {
// 关联模型的 ID
type: Schema.Types.ObjectId,
ref: 'model',
required: true
},
status: {
// 状态值
type: String,
required: true,
enum: ['pending', 'succeed', 'errored', 'canceled']
}
});
export const Training = models['training'] || model('training', TrainingSChema);
import { Schema, model, models } from 'mongoose';
import { hashPassword } from '@/service/utils/tools';
const UserSchema = new Schema({
email: {
type: String,
required: true,
unique: true // 唯一
},
password: {
type: String,
required: true,
set: (val: string) => hashPassword(val),
get: (val: string) => hashPassword(val),
select: false
},
balance: {
type: Number,
default: 0
},
accounts: [
{
type: {
type: String,
required: true,
enum: ['openai'] // 定义允许的type
},
value: {
type: String,
required: true
}
}
],
createTime: {
type: Date,
default: () => new Date()
}
});
export const User = models['user'] || model('user', UserSchema);
import mongoose from 'mongoose';
import type { Mongoose } from 'mongoose';
let cachedClient: Mongoose;
export async function connectToDatabase() {
if (cachedClient && cachedClient.connection.readyState === 1) {
return cachedClient;
}
cachedClient = await mongoose.connect(process.env.MONGODB_UR as string, {
dbName: 'doc_gpt'
});
return cachedClient;
}
export * from './models/authCode';
export * from './models/chat';
export * from './models/model';
export * from './models/user';
export * from './models/training';
export * from './models/chatWindow';
import { ChatItemType } from '../types/chat';
export const chatWindows = new Map<string, ChatItemType[]>();
/**
* 获取聊天窗口信息
*/
export const getWindowMessages = (id: string) => {
return chatWindows.get(id) || [];
};
export const pushWindowMessage = (id: string, prompt: ChatItemType) => {
const messages = chatWindows.get(id) || [];
messages.push(prompt);
chatWindows.set(id, messages);
return messages;
};
export const deleteWindow = (id: string) => {
chatWindows.delete(id);
};
import { NextApiResponse } from 'next';
import { openaiError } from './errorCode';
export interface ResponseType<T = any> {
code: number;
message: string;
data: T;
}
export const jsonRes = (
res: NextApiResponse,
props?: {
code?: number;
message?: string;
data?: any;
error?: any;
}
) => {
const { code = 200, message = '', data = null, error } = props || {};
let msg = message;
if ((code < 200 || code >= 400) && !message) {
msg =
typeof error === 'string'
? error
: openaiError[error?.response?.data?.message] || error?.message || '请求错误';
console.log(msg);
}
res.json({
code,
message: msg,
data
});
};
import { Configuration, OpenAIApi } from 'openai';
import { Chat } from '../mongo';
export const getOpenAIApi = (apiKey: string) => {
const configuration = new Configuration({
apiKey
});
return new OpenAIApi(configuration, undefined);
};
export const authChat = async (chatId: string) => {
// 获取 chat 数据
const chat = await Chat.findById(chatId)
.populate({
path: 'modelId',
options: {
strictPopulate: false
}
})
.populate({
path: 'userId',
options: {
strictPopulate: false
}
});
if (!chat || !chat.modelId || !chat.userId) {
return Promise.reject('聊天已过期');
}
// 获取 user 的 apiKey
const user = chat.userId;
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value;
if (!userApiKey) {
return Promise.reject('该用户缺少ApiKey, 无法请求');
}
return {
userApiKey,
chat
};
};
import * as nodemailer from 'nodemailer';
import { EmailTypeEnum } from '@/constants/common';
import dayjs from 'dayjs';
const myEmail = process.env.MY_MAIL;
let mailTransport = nodemailer.createTransport({
// host: 'smtp.qq.email',
service: 'qq',
secure: true, //安全方式发送,建议都加上
auth: {
user: myEmail,
pass: process.env.MAILE_CODE
}
});
const emailMap: { [key: string]: any } = {
[EmailTypeEnum.register]: {
subject: '注册 DocGPT 账号',
html: (code: string) => `<div>您正在注册 DocGPT 账号,验证码为:${code}</div>`
},
[EmailTypeEnum.findPassword]: {
subject: '修改 DocGPT 密码',
html: (code: string) => `<div>您正在修改 DocGPT 账号密码,验证码为:${code}</div>`
}
};
export const sendCode = (email: string, code: string, type: `${EmailTypeEnum}`) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
to: email,
subject: emailMap[type]?.subject,
html: emailMap[type]?.html(code)
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
reject('邮箱异常');
} else {
resolve('');
}
});
});
};
export const sendTrainSucceed = (email: string, modelName: string) => {
return new Promise((resolve, reject) => {
const options = {
from: `"DocGPT" ${myEmail}`,
to: email,
subject: '模型训练完成通知',
html: `你的模型 ${modelName} 已于 ${dayjs().format('YYYY-MM-DD HH:mm')} 训练完成!`
};
mailTransport.sendMail(options, function (err, msg) {
if (err) {
console.log(err);
reject('邮箱异常');
} else {
resolve('');
}
});
});
};
import crypto from 'crypto';
import jwt from 'jsonwebtoken';
import { User } from '../models/user';
import tunnel from 'tunnel';
/* 密码加密 */
export const hashPassword = (psw: string) => {
return crypto.createHash('sha256').update(psw).digest('hex');
};
/* 生成 token */
export const generateToken = (userId: string) => {
const key = process.env.TOKEN_KEY as string;
const token = jwt.sign(
{
userId,
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7
},
key
);
return token;
};
/* 校验 token */
export const authToken = (token: string): Promise<string> => {
return new Promise((resolve, reject) => {
const key = process.env.TOKEN_KEY as string;
jwt.verify(token, key, function (err, decoded: any) {
if (err || !decoded?.userId) {
reject('凭证无效');
return;
}
resolve(decoded.userId);
});
});
};
/* 获取用户的 openai APIkey */
export const getUserOpenaiKey = async (userId: string) => {
const user = await User.findById(userId);
const userApiKey = user?.accounts?.find((item: any) => item.type === 'openai')?.value;
if (!userApiKey) {
return Promise.reject('缺少ApiKey, 无法请求');
}
return Promise.resolve(userApiKey);
};
/* 代理 */
export const openaiProxy: any =
process.env.AXIOS_PROXY_PORT && process.env.AXIOS_PROXY_HOST
? {
httpsAgent: tunnel.httpsOverHttp({
proxy: {
host: process.env.AXIOS_PROXY_HOST,
port: +process.env.AXIOS_PROXY_PORT
}
}),
proxy: false
}
: undefined;
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
type State = {
loading: boolean;
setLoading: (val: boolean) => null;
};
export const useGlobalStore = create<State>()(
devtools(
immer((set, get) => ({
loading: false,
setLoading: (val: boolean) => {
set((state) => {
state.loading = val;
});
return null;
}
}))
)
);
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import type { UserType, UserUpdateParams } from '@/types/user';
import { setToken } from '@/utils/user';
type State = {
userInfo: UserType | null;
setUserInfo: (user: UserType, token?: string) => void;
updateUserInfo: (user: UserUpdateParams) => void;
};
export const useUserStore = create<State>()(
devtools(
immer((set, get) => ({
userInfo: null,
setUserInfo: (user: UserType, token?: string) => {
set((state) => {
state.userInfo = user;
});
token && setToken(token);
},
updateUserInfo: (user: UserUpdateParams) => {
set((state) => {
if (!state.userInfo) return;
state.userInfo = {
...state.userInfo,
...user
};
});
}
}))
)
);
.main {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 6rem;
min-height: 100vh;
}
.description {
display: inherit;
justify-content: inherit;
align-items: inherit;
font-size: 0.85rem;
max-width: var(--max-width);
width: 100%;
z-index: 2;
font-family: var(--font-mono);
}
.description a {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
}
.description p {
position: relative;
margin: 0;
padding: 1rem;
background-color: rgba(var(--callout-rgb), 0.5);
border: 1px solid rgba(var(--callout-border-rgb), 0.3);
border-radius: var(--border-radius);
}
.code {
font-weight: 700;
font-family: var(--font-mono);
}
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(25%, auto));
width: var(--max-width);
max-width: 100%;
}
.card {
padding: 1rem 1.2rem;
border-radius: var(--border-radius);
background: rgba(var(--card-rgb), 0);
border: 1px solid rgba(var(--card-border-rgb), 0);
transition: background 200ms, border 200ms;
}
.card span {
display: inline-block;
transition: transform 200ms;
}
.card h2 {
font-weight: 600;
margin-bottom: 0.7rem;
}
.card p {
margin: 0;
opacity: 0.6;
font-size: 0.9rem;
line-height: 1.5;
max-width: 30ch;
}
.center {
display: flex;
justify-content: center;
align-items: center;
position: relative;
padding: 4rem 0;
}
.center::before {
background: var(--secondary-glow);
border-radius: 50%;
width: 480px;
height: 360px;
margin-left: -400px;
}
.center::after {
background: var(--primary-glow);
width: 240px;
height: 180px;
z-index: -1;
}
.center::before,
.center::after {
content: '';
left: 50%;
position: absolute;
filter: blur(45px);
transform: translateZ(0);
}
.logo,
.thirteen {
position: relative;
}
.thirteen {
display: flex;
justify-content: center;
align-items: center;
width: 75px;
height: 75px;
padding: 25px 10px;
margin-left: 16px;
transform: translateZ(0);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: 0px 2px 8px -1px #0000001a;
}
.thirteen::before,
.thirteen::after {
content: '';
position: absolute;
z-index: -1;
}
/* Conic Gradient Animation */
.thirteen::before {
animation: 6s rotate linear infinite;
width: 200%;
height: 200%;
background: var(--tile-border);
}
/* Inner Square */
.thirteen::after {
inset: 0;
padding: 1px;
border-radius: var(--border-radius);
background: linear-gradient(
to bottom right,
rgba(var(--tile-start-rgb), 1),
rgba(var(--tile-end-rgb), 1)
);
background-clip: content-box;
}
/* Enable hover only on non-touch devices */
@media (hover: hover) and (pointer: fine) {
.card:hover {
background: rgba(var(--card-rgb), 0.1);
border: 1px solid rgba(var(--card-border-rgb), 0.15);
}
.card:hover span {
transform: translateX(4px);
}
}
@media (prefers-reduced-motion) {
.thirteen::before {
animation: none;
}
.card:hover span {
transform: none;
}
}
/* Mobile */
@media (max-width: 700px) {
.content {
padding: 4rem;
}
.grid {
grid-template-columns: 1fr;
margin-bottom: 120px;
max-width: 320px;
text-align: center;
}
.card {
padding: 1rem 2.5rem;
}
.card h2 {
margin-bottom: 0.5rem;
}
.center {
padding: 8rem 0 6rem;
}
.center::before {
transform: none;
height: 300px;
}
.description {
font-size: 0.8rem;
}
.description a {
padding: 1rem;
}
.description p,
.description div {
display: flex;
justify-content: center;
position: fixed;
width: 100%;
}
.description p {
align-items: center;
inset: 0 0 auto;
padding: 2rem 1rem 1.4rem;
border-radius: 0;
border: none;
border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
background: linear-gradient(
to bottom,
rgba(var(--background-start-rgb), 1),
rgba(var(--callout-rgb), 0.5)
);
background-clip: padding-box;
backdrop-filter: blur(24px);
}
.description div {
align-items: flex-end;
pointer-events: none;
inset: auto 0 0;
padding: 2rem;
height: 200px;
background: linear-gradient(
to bottom,
transparent 0%,
rgb(var(--background-end-rgb)) 40%
);
z-index: 1;
}
}
/* Tablet and Smaller Desktop */
@media (min-width: 701px) and (max-width: 1120px) {
.grid {
grid-template-columns: repeat(2, 50%);
}
}
@media (prefers-color-scheme: dark) {
.vercelLogo {
filter: invert(1);
}
.logo,
.thirteen img {
filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
}
}
@keyframes rotate {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
:root {
--max-width: 1100px;
--border-radius: 12px;
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
--primary-glow: conic-gradient(
from 180deg at 50% 50%,
#16abff33 0deg,
#0885ff33 55deg,
#54d6ff33 120deg,
#0071ff33 160deg,
transparent 360deg
);
--secondary-glow: radial-gradient(
rgba(255, 255, 255, 1),
rgba(255, 255, 255, 0)
);
--tile-start-rgb: 239, 245, 249;
--tile-end-rgb: 228, 232, 233;
--tile-border: conic-gradient(
#00000080,
#00000040,
#00000030,
#00000020,
#00000010,
#00000010,
#00000080
);
--callout-rgb: 238, 240, 241;
--callout-border-rgb: 172, 175, 176;
--card-rgb: 180, 185, 188;
--card-border-rgb: 131, 134, 135;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
--primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
--secondary-glow: linear-gradient(
to bottom right,
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0),
rgba(1, 65, 255, 0.3)
);
--tile-start-rgb: 2, 13, 46;
--tile-end-rgb: 2, 5, 19;
--tile-border: conic-gradient(
#ffffff80,
#ffffff40,
#ffffff30,
#ffffff20,
#ffffff10,
#ffffff10,
#ffffff80
);
--callout-rgb: 20, 20, 20;
--callout-border-rgb: 108, 108, 108;
--card-rgb: 100, 100, 100;
--card-border-rgb: 200, 200, 200;
}
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
body,
h1,
h2,
h3,
h4,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td,
svg {
margin: 0;
}
body,
button,
input,
select,
textarea {
font: 12px/1.5tahoma, arial, \5b8b\4f53;
}
// h1, h2, h3, h4, h5, h6{ font-size:100%; }
address,
cite,
dfn,
em,
var {
font-style: normal;
}
code,
kbd,
pre,
samp {
font-family: couriernew, courier, monospace;
}
small {
font-size: 12px;
}
ul,
ol {
list-style: none;
padding: 0;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
sup {
vertical-align: text-top;
}
sub {
vertical-align: text-bottom;
}
legend {
color: #000;
}
fieldset,
img {
border: 0;
}
button,
input,
select,
textarea {
font-size: 100%;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
::-webkit-scrollbar,
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track,
::-webkit-scrollbar-track {
background: transparent;
border-radius: 2px;
}
::-webkit-scrollbar-thumb,
::-webkit-scrollbar-thumb {
background: #bfbfbf;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover,
::-webkit-scrollbar-thumb:hover {
background: #999;
}
@media (max-width: 900px) {
::-webkit-scrollbar,
::-webkit-scrollbar {
width: 2px;
height: 2px;
}
}
import type { ModelType } from './model';
export interface ChatSiteType {
name: string;
avatar: string;
modelId: string;
chatModel: string;
secret: ModelType.security;
}
export type ChatItemType = {
obj: 'Human' | 'AI' | 'SYSTEM';
value: string;
};
export type ChatSiteItemType = {
status: 'loading' | 'finish';
} & ChatItemType;
import { ModelStatusEnum } from '@/constants/model';
export interface ModelType {
_id: string;
userId: string;
name: string;
avatar: string;
status: `${ModelStatusEnum}`;
updateTime: Date;
trainingTimes: number;
systemPrompt: string;
service: {
company: 'openai'; // 关联的厂商
trainId: string; // 训练时需要的ID
chatModel: string; // 聊天时用的模型
modelName: string; // 关联的模型
};
security: {
domain: string[];
contentMaxLen: number;
contextMaxLen: number;
expiredTime: number;
maxLoadAmount: number;
};
}
export interface ModelUpdateParams {
name: string;
systemPrompt: string;
service: {
company: 'openai'; // 关联的厂商
modelName: string; // 关联的模型
};
security: {
domain: string[];
contentMaxLen: number;
contextMaxLen: number;
expiredTime: number;
maxLoadAmount: number;
};
}
import { TrainingStatusEnum } from '@/constants/model';
export type TrainingItemType = {
_id: string;
serviceName: string;
tuneId: string;
modelId: string;
status: `${TrainingStatusEnum}`;
};
export enum UserNumberEnum {
phone = 'phone',
wx = 'wx'
}
export interface UserType {
_id: string;
email: string;
accounts: {
type: string;
value: string;
}[];
balance: number;
}
export interface UserUpdateParams {
accounts?: {
type: string;
value: string;
}[];
}
import crypto from 'crypto';
import { useToast } from '@/hooks/useToast';
/**
* copy text data
*/
export const useCopyData = () => {
const { toast } = useToast();
return {
copyData: (data: string, title: string = '复制成功') => {
const clipboardObj = navigator.clipboard;
clipboardObj
.writeText(data)
.then(() => {
toast({
title,
status: 'success',
duration: 1000
});
})
.catch((err) => {
console.log(err);
});
}
};
};
export const createHashPassword = (text: string) => {
const hash = crypto.createHash('sha256').update(text).digest('hex');
return hash;
};
/**
* 读取文件内容
*/
export const loadLocalFileContent = (file: File) => {
return new Promise((resolve: (_: string) => void, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result as string);
};
reader.onerror = (err) => {
reject(err);
};
reader.readAsText(file);
});
};
const tokenKey = 'doc-gpt-token';
export const setToken = (val: string) => {
localStorage.setItem(tokenKey, val);
};
export const getToken = () => {
return localStorage.getItem(tokenKey);
};
export const clearToken = () => {
localStorage.removeItem(tokenKey);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment