Compare commits

..

87 Commits
v1.2 ... v2.4

Author SHA1 Message Date
archer
e08e8aa00b feat: 修改模型数据可修改问题 2023-04-04 13:15:34 +08:00
archer
becee69d6a perf: 发送区域样式 2023-04-03 17:28:35 +08:00
archer
042b0c535a perf: 发送按键 2023-04-03 17:14:46 +08:00
archer
f97c29b41e feat: lafgpt请求;fix: 修复发送按键 2023-04-03 16:35:48 +08:00
archer
4d6616cbfa fix: ts 2023-04-03 11:03:51 +08:00
archer
cf37992b5c feat: 封装向量生成和账单 2023-04-03 10:59:32 +08:00
archer
6c4026ccef perf: 文件结构 2023-04-03 10:20:17 +08:00
archer
caf31faf31 perf: 生成qa prompt 2023-04-03 01:39:00 +08:00
archer
a0832af14b perf: 数据集刷新导致页面抖动 2023-04-03 00:51:53 +08:00
archer
677e61416d perf: 版本文案 2023-04-03 00:48:56 +08:00
archer
56ba6fa5f7 feat: 拆分数据自定义prompt 2023-04-03 00:37:40 +08:00
archer
16a31de1c7 feat: 数据集导出 2023-04-03 00:18:21 +08:00
archer
05b2e9e99c feat: 拆分测试环境 2023-04-02 23:38:28 +08:00
archer
ae4243b522 perf: 知识库数据结构 2023-04-01 22:31:56 +08:00
archer
5759cbeae0 perf: 知识库录入 2023-03-31 18:23:07 +08:00
archer
a3d74ec4a6 perrf chat不请求余额 2023-03-31 14:13:00 +08:00
archer
df9ac99ef2 fix: 复制和代理 2023-03-31 12:17:08 +08:00
archer
ef1e8aef5c fix: 发送区按键 2023-03-31 11:45:42 +08:00
archer
56dab7abba perf: api调用和余额校验 2023-03-31 11:20:45 +08:00
archer
ed1f93d836 perf: 改用hash索引 2023-03-31 02:58:09 +08:00
archer
5ec8aac3ac README 2023-03-31 00:41:42 +08:00
archer
837c132d24 feat: 删除模型数据 2023-03-31 00:23:41 +08:00
archer
8239c58494 perf: 知识库优化 2023-03-31 00:05:04 +08:00
archer
456686f3d0 conflict 2023-03-30 22:33:58 +08:00
archer
2099a87908 feat: 模型数据管理
feat: 模型数据导入

feat: redis 向量入库

feat: 向量索引

feat: 文件导入模型

perf: 交互

perf: prompt
2023-03-30 21:45:54 +08:00
archer
2b2c70e53d feat: 模型数据导入 2023-03-30 01:04:52 +08:00
archer
f32c557bdd feat: 模型数据管理 2023-03-29 00:22:48 +08:00
archer
713332522f Merge branch 'dev2.1' into dev2.2 2023-03-28 17:57:57 +08:00
archer
586607a9ce feat: modeldata接口。fix: 部分权限校验bug 2023-03-28 17:56:31 +08:00
archer
8c70205940 README 2023-03-28 00:48:24 +08:00
archer
c3ccbcb7f6 perf: 输入超长提示 2023-03-28 00:36:26 +08:00
archer
7a6d0ea650 perf: 减少聊天内容配置,自动截断上下文 2023-03-28 00:07:32 +08:00
archer
7fb6f62cf6 perf: 文本拆分 2023-03-27 19:19:47 +08:00
archer
af385b1b42 feat: 一次性获取data集合 2023-03-27 18:55:38 +08:00
archer
5249297cb1 perf: bill logs 2023-03-27 13:58:57 +08:00
archer
9280a21d12 perf: 提示词 2023-03-27 01:53:40 +08:00
archer
650a9dd651 fix: 生成词向量代理 2023-03-27 01:06:40 +08:00
archer
42e12d7db1 perf: 文本 2023-03-26 23:52:37 +08:00
archer
249ed18d15 fix: 抽取概要 2023-03-26 22:43:39 +08:00
archer
3e4487ad9a feat: 摘要拆分 2023-03-26 22:09:59 +08:00
archer
888642f154 perf: 参数 2023-03-26 15:47:30 +08:00
archer
963e590dfd perf: 参数 2023-03-26 15:41:50 +08:00
archer
e547893ac1 perf: 微调截止词 2023-03-26 15:27:57 +08:00
archer
9125910cfe fix: 连续滚动重复请求;perf: 训练参数 2023-03-26 14:37:49 +08:00
archer
98c458dcf8 fix: 训练后模型没选中 2023-03-26 13:56:00 +08:00
archer
41b6401c13 feat: 复制和删除对话功能 2023-03-26 13:14:50 +08:00
archer
936e36205e fix: 滚动条样式 2023-03-26 11:52:46 +08:00
archer
e289c4ec53 fix: ts数据 2023-03-26 10:51:47 +08:00
archer
1cbcc62494 perf: 正则提取文本, 2023-03-26 10:31:13 +08:00
archer
da31ef286b perf: 完善拆分数据模式 2023-03-25 23:37:50 +08:00
archer
60f62507bd feat: 版本更新 2023-03-25 23:16:59 +08:00
archer
75cf3d1e9f feat: 拆分文本账单结算 2023-03-25 23:02:55 +08:00
archer
02cee35a45 feat: 数据集管理 2023-03-25 22:13:15 +08:00
archer
0cee404c7f perf: 账单字段 2023-03-25 20:48:21 +08:00
archer
274ece1d91 feat: gpt3流响应 2023-03-25 20:43:03 +08:00
archer
6bba859060 feat: 修改计费模式为tokens 2023-03-25 14:43:32 +08:00
archer
4eaf3a1be0 feat: 滚动加载组件 2023-03-25 13:55:53 +08:00
archer
3db690773f feat: 数据集合管理 2023-03-25 12:55:32 +08:00
archer
8a9f1ed29b feat: 训练数据管理 2023-03-25 01:40:15 +08:00
archer
c0dc5a74c9 fix: conflict 2023-03-24 20:18:27 +08:00
archer
a35cda6873 fix: destroy后仍有push流 2023-03-24 14:39:25 +08:00
archer
30678d8ebf fix: destroy后仍有push流 2023-03-24 14:35:33 +08:00
archer
6ce727f9ea fix: conflict 2023-03-24 12:55:51 +08:00
archer
81e6821174 feat: 使用说明&价格表 2023-03-24 10:22:08 +08:00
archer
d8290f0809 feat: qa生成 2023-03-24 01:19:38 +08:00
archer
8b72dca533 merge dev2.0 2023-03-23 23:07:24 +08:00
archer
4d64068591 feat: 临时data 2023-03-23 21:32:28 +08:00
archer
af35e17fdb feat: 增加中断流.fix: 中断流导致的服务端错误 2023-03-22 22:09:40 +08:00
archer
5ec303610c fix: 修复支付可能存在的缺陷 2023-03-22 12:20:27 +08:00
archer
984baf60f0 feat: share窗口 2023-03-21 23:47:26 +08:00
archer
d065539707 feat: 增加充值功能 2023-03-21 23:14:28 +08:00
archer
129f3a2a30 feat: 账单模块 2023-03-21 18:04:39 +08:00
archer
42c26bd155 feat: 增加账单 2023-03-21 14:01:35 +08:00
archer
dc467c26b5 feat: 聊天页暗夜模式 2023-03-20 21:34:12 +08:00
archer
3aeb510f43 feat: colormode 2023-03-20 18:38:00 +08:00
archer
405a75e23b feat: colormode 2023-03-20 18:19:36 +08:00
archer
be47169fa8 fix: 侧边栏收缩 2023-03-20 09:36:49 +08:00
Archer
58a010c12c perf: 细节样式 2023-03-18 13:30:25 +08:00
Archer
7ba14d2c14 feat: 首页提示 2023-03-18 12:40:05 +08:00
Archer
00b90f071d feat: 模型介绍和温度调整。完善聊天页提示 2023-03-18 12:32:55 +08:00
Archer
1c364eca35 feat: 空内容提醒 2023-03-18 01:09:04 +08:00
Archer
9384419c9d feat: 模型数量增加。滚动条位置 2023-03-18 00:53:00 +08:00
Archer
38c093d9ae feat: 修改chat的数据结构 2023-03-18 00:49:44 +08:00
Archer
e6c9ca540a fix: 聊天侧边栏未滚动 2023-03-17 22:06:43 +08:00
Archer
d85b4c0945 perf: 优化系统提示消息 2023-03-17 00:53:27 +08:00
Archer
1e770088d0 feat: 增加聊天navbar 2023-03-16 23:38:43 +08:00
Archer
7529f51e72 temp 2023-03-15 21:36:56 +08:00
157 changed files with 90397 additions and 1510 deletions

View File

@@ -4,3 +4,5 @@ MONGODB_URI=
MY_MAIL= MY_MAIL=
MAILE_CODE= MAILE_CODE=
TOKEN_KEY= TOKEN_KEY=
OPENAIKEY=
REDIS_URL=

View File

@@ -1,3 +1,6 @@
{ {
"extends": "next/core-web-vitals" "extends": "next/core-web-vitals",
"rules": {
"react-hooks/rules-of-hooks": 0
}
} }

0
.husky/pre-commit Normal file → Executable file
View File

View File

@@ -18,4 +18,3 @@ module.exports = {
htmlWhitespaceSensitivity: 'css', htmlWhitespaceSensitivity: 'css',
endOfLine: 'lf' endOfLine: 'lf'
}; };

View File

@@ -1,6 +1,6 @@
# Fast GPT # Fast GPT
Fast GPT 允许你用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。 Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接口,包括 GPT3 及其微调方法,以及最新的 gpt3.5 接口。
## 初始化 ## 初始化
复制 .env.template 成 .env.local ,填写核心参数 复制 .env.template 成 .env.local ,填写核心参数
@@ -101,3 +101,12 @@ echo "Restart clash"
#### 软件教程Nginx #### 软件教程Nginx
...没写,这个百度吧。 ...没写,这个百度吧。
#### redis
```bash
# 索引
# FT.CREATE idx:model:data ON JSON PREFIX 1 model:data: SCHEMA $.modelId AS modelId TAG $.dataId AS dataId TAG $.vector AS vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
# FT.CREATE idx:model:data:hash ON HASH PREFIX 1 model:data: SCHEMA modelId TAG dataId TAG vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
FT.CREATE idx:model:data ON HASH PREFIX 1 model:data: SCHEMA modelId TAG userId TAG q TEXT text TEXT vector VECTOR FLAT 6 DIM 1536 DISTANCE_METRIC COSINE TYPE FLOAT32
```

View File

@@ -1,4 +0,0 @@
{"prompt": "sealos的介绍", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
{"prompt": "sealos是什么", "completion": "sealos 是以 kubernetes 为内核的云操作系统发行版"}
{"prompt": "sealos安装的先决条件", "completion": "sealos 是一个简单的 go 二进制文件,可以安装在大多数 Linux 操作系统中。"}
{"prompt": "sealos的CPU架构", "completion": "目前支持 amd64 和 arm64 架构。"}

View File

@@ -6,7 +6,18 @@ const isDev = process.env.NODE_ENV === 'development';
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
reactStrictMode: false, reactStrictMode: false,
compress: true compress: true,
webpack(config) {
config.module.rules = config.module.rules.concat([
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack']
}
]);
return config;
}
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@@ -24,10 +24,14 @@
"eventsource-parser": "^0.1.0", "eventsource-parser": "^0.1.0",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"framer-motion": "^9.0.6", "framer-motion": "^9.0.6",
"gpt-token-utils": "^1.2.0",
"hyperdown": "^2.4.29", "hyperdown": "^2.4.29",
"immer": "^9.0.19", "immer": "^9.0.19",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"lodash": "^4.17.21",
"mammoth": "^1.5.1",
"mongoose": "^6.10.0", "mongoose": "^6.10.0",
"nanoid": "^4.0.1",
"next": "13.1.6", "next": "13.1.6",
"nodemailer": "^6.9.1", "nodemailer": "^6.9.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
@@ -37,18 +41,20 @@
"react-hook-form": "^7.43.1", "react-hook-form": "^7.43.1",
"react-markdown": "^8.0.5", "react-markdown": "^8.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"redis": "^4.6.5",
"rehype-katex": "^6.0.2", "rehype-katex": "^6.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"sass": "^1.58.3", "sass": "^1.58.3",
"sharp": "^0.31.3", "sharp": "^0.31.3",
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
"uuid": "^9.0.0",
"zustand": "^4.3.5" "zustand": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@svgr/webpack": "^6.5.1",
"@types/formidable": "^2.0.5", "@types/formidable": "^2.0.5",
"@types/jsonwebtoken": "^9.0.1", "@types/jsonwebtoken": "^9.0.1",
"@types/lodash": "^4.14.191",
"@types/node": "18.14.0", "@types/node": "18.14.0",
"@types/nodemailer": "^6.4.7", "@types/nodemailer": "^6.4.7",
"@types/react": "18.0.28", "@types/react": "18.0.28",

2495
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,15 @@
{"prompt":"什么是死锁?如何避免死锁?\n\n###\n\n","completion":" 死锁是指两个或多个进程在执行过程中,由于互相等待对方释放资源而造成的一种僵局。避免死锁的常用方法包括:破坏互斥条件、破坏请求和保持条件、破坏不剥夺条件、破坏循环等待条件。###"} {"prompt":"什么是百度plato</s>","completion":" 百度PLATO是一种聊天机器人发布于2021年10月参数量为百亿是GPT-3的1/17。</s>"}
{"prompt":"什么是虚拟内存?\n\n###\n\n","completion":" 虚拟内存是指一种计算机内存管理技术,它使得操作系统可以将进程所使用的虚拟地址空间映射到物理内存中,从而可以让多个进程共享物理内存。当物理内存不足时,操作系统会将一部分不常用的内存页面暂时存储到硬盘上,这样就可以释放出更多的物理内存供其他进程使用。###"} {"prompt":"与百度plato相比chatgpt有哪些优点</s>","completion":" 与百度PLATO相比ChatGPT体现出耐心、体贴和妥当被网友们普遍称赞。ChatGPT可以用于个性化聊天机器人例如生成一个很相似的数字人提供一位温柔健谈的朋友指导语言学习等。</s>"}
{"prompt":"什么是分页和分段?有什么区别?\n\n###\n\n","completion":" 分页和分段是虚拟内存管理中常用的两种技术。分页是将虚拟地址空间划分为固定大小的页面,而分段则是将虚拟地址空间划分为不同大小的段。区别在于,分页的页面大小是固定的,而分段的段大小是可变的;另外,分页是为了实现虚拟内存,而分段则是为了实现逻辑地址空间的管理。###"} {"prompt":"replika是什么</s>","completion":" Replika是一个产品使用ChatGPT的技术可以根据自己的需求生成一个虚拟女友、男友或单纯的朋友。目前还引起了不少有关性骚扰、家暴、隐私等问题的争议。</s>"}
{"prompt":"什么是lru算法\n\n###\n\n","completion":" lru算法是一种页面置换算法它的全称是least recently used即最近最少使用算法。lru算法的基本思想是在物理内存中维护一个页面访问历史记录当需要替换一页时选择最近最少被访问的页面进行替换。###"} {"prompt":"聊天机器人可以用于哪些场景?</s>","completion":" 聊天机器人可以用于很多陪伴的场景,例如亲人去世、抑郁症或自闭症患者、语言学习、小朋友、养老院或独居在家的老人等。</s>"}
{"prompt":"什么是文件系统?\n\n###\n\n","completion":" 文件系统是一种用于管理计算机存储设备上文件和目录的组织方式。文件系统通常包括存储管理、目录管理、文件访问控制、文件系统维护等功能。常见的文件系统包括fat、ntfs、ext等。###"} {"prompt":"有人反对使用聊天机器人解决孤独困境,为什么?</s>","completion":" 有人反对使用聊天机器人解决孤独困境,认为孤独困境应该用人与人真正的交往解决,机器是冰冷的。</s>"}
{"prompt":"什么是并发和并行?\n\n###\n\n","completion":" 并发是指多个任务交替执行的情况,而并行是指多个任务同时执行的情况。并发的实现方式是通过时间分片技术,而并行的实现方式是通过多核处理器等硬件设备。###"} {"prompt":"什么聊天机器人仍然需要解决当下的需求?</s>","completion":" 在社会结构性的困局还没有解开时,长夜漫漫的独身打工人、养老院里卧床的老人、父母在外留守在家的儿童,当下的需求仍需要解决。</s>"}
{"prompt":"什么是系统调用?\n\n###\n\n","completion":" 系统调用是操作系统中的一种机制,它允许用户空间程序请求操作系统内核提供一些服务,如读写文件、创建进程等。系统调用通常是通过软中断实现的###"} {"prompt":"ai在未来会被戴上什么样的帽子</s>","completion":" AI在未来各个场景下都会被戴上一个很虚无的帽子“机器是冰冷的”、“没有灵魂”、“它并不是真正在思考”。</s>"}
{"prompt":"什么是进程间通信?\n\n###\n\n","completion":" 进程间通信是指不同进程之间进行数据交换和通信的机制,常见的方式有管道、共享内存、消息队列、信号量等。###"} {"prompt":"怎么回答“ai是否真正思考”的问题</s>","completion":" 引用乔姆斯基老爷子的那句名言“思考是人类特性。AI有天会思考吗那就像是在问潜水艇会不会游泳一样。”</s>"}
{"prompt":"什么是文件描述符?\n\n###\n\n","completion":" 文件描述符是一个非负整数它是操作系统内核为了管理打开的文件而维护的一种抽象概念。应用程序使用文件描述符来访问文件或其他i\/o设备###"} {"prompt":"聊天机器人与人类完成任务的方式有何不同?</s>","completion":" 聊天机器人与人类完成任务的方式不同,但并不重要。它们能跟人一样完成任务,这是最重要的。</s>"}
{"prompt":"什么是中断? \n\n###\n\n","completion":" 中断是指计算机硬件或软件在执行程序时,暂停当前程序的执行,转而去执行另一个程序或处理器所需的其他任务的过程。中断可以是外部中断、内部中断或软中断。###"} {"prompt":"聊天机器人的发展前景如何?</s>","completion":" 聊天机器人的发展前景广阔,可以应用于很多领域,例如客服、教育、医疗等。</s>"}
{"prompt":"什么是页表?\n\n###\n\n","completion":" 页表是一种数据结构,它用于将虚拟地址转换为物理地址。在虚拟内存中,每个进程都有自己的页表,它描述了虚拟地址空间中每个页面对应的物理地址。 ###"} {"prompt":"什么是百度plato</s>","completion":" 百度PLATO是一种聊天机器人发布于2021年10月参数量为百亿是GPT-3的1/17。</s>"}
{"prompt":"什么是僵尸进程?\n\n###\n\n","completion":" 僵尸进程是指一个已经完成执行但其父进程还没有对其进行善后处理的进程,它会一直占用系统资源,直到其父进程将其处理为止。###"} {"prompt":"与百度plato相比chatgpt有哪些优点</s>","completion":" 与百度PLATO相比ChatGPT体现出耐心、体贴和妥当被网友们普遍称赞。ChatGPT还可以用于很多陪伴的场景如亲人去世、抑郁症或自闭症患者、语言学习、小朋友、养老院或独居在家的老人等。</s>"}
{"prompt":"什么是守护进程? \n\n###\n\n","completion":" 守护进程是一种在后台运行的特殊进程,它通常用于提供某种服务或执行某种系统任务。守护进程通常不与任何用户界面交互,也不会终止,直到操作系统关机或重启。###"} {"prompt":"replika是什么产品</s>","completion":" Replika是一个产品用的是ChatGPT的技术。用户可以根据自己的需求生成一个虚拟女友、男友或单纯的朋友。目前还引起了不少有关性骚扰、家暴、隐私等问题的争议。</s>"}
{"prompt":"计算机操作系统是什么?\n\n###\n\n","completion":" 计算机操作系统是一种控制计算机硬件和协调应用程序运行的系统软件。###"} {"prompt":"聊天机器人能够解决哪些现实问题?</s>","completion":" 聊天机器人能够解决现代人面临的孤独困境,如长夜漫漫的独身打工人、养老院里卧床的老人、父母在外留守在家的儿童等。</s>"}
{"prompt":"什么是进程?\n\n###\n\n","completion":" 进程是一个正在执行中的程序实例,它包括了程序计数器、堆栈、数据区等信息。###"} {"prompt":"有人反对使用聊天机器人解决孤独困境,为什么?</s>","completion":" 有人反对使用聊天机器人解决孤独困境,认为孤独困境应该用人与人真正的交往解决,机器是冰冷的。</s>"}
{"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上执行顺序的一种算法它的目标是最大化系统吞吐量、最小化响应时间或最大化资源利用率等。###"}

BIN
public/imgs/wxcode300.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

15792
public/js/pdf.js Normal file

File diff suppressed because it is too large Load Diff

64521
public/js/pdf.worker.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/js/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +1,18 @@
import { GET, POST, DELETE } from './request'; import { GET, POST, DELETE } from './request';
import { ChatItemType, ChatSiteType, ChatSiteItemType } from '@/types/chat'; import type { ChatItemType, ChatSiteItemType } from '@/types/chat';
import axios from 'axios'; import type { InitChatResponse } from './response/chat';
/** /**
* 获取一个聊天框的ID * 获取一个聊天框的ID
*/ */
export const getChatSiteId = (modelId: string) => GET<string>(`/chat/generate?modelId=${modelId}`); export const getChatSiteId = (modelId: string, isShare = false) =>
GET<string>(`/chat/generate?modelId=${modelId}&isShare=${isShare ? 'true' : 'false'}`);
/** /**
* 获取初始化聊天内容 * 获取初始化聊天内容
*/ */
export const getInitChatSiteInfo = (chatId: string, windowId: string = '') => export const getInitChatSiteInfo = (chatId: string) =>
GET<{ GET<InitChatResponse>(`/chat/init?chatId=${chatId}`);
windowId: string;
chatSite: ChatSiteType;
history: ChatItemType[];
}>(`/chat/init?chatId=${chatId}&windowId=${windowId}`);
/** /**
* 发送 GPT3 prompt * 发送 GPT3 prompt
@@ -38,11 +35,11 @@ export const postGPT3SendPrompt = ({
/** /**
* 存储一轮对话 * 存储一轮对话
*/ */
export const postSaveChat = (data: { windowId: string; prompts: ChatItemType[] }) => export const postSaveChat = (data: { chatId: string; prompts: ChatItemType[] }) =>
POST('/chat/saveChat', data); POST('/chat/saveChat', data);
/** /**
* 删除最后一句 * 删除一句对话
*/ */
export const delLastMessage = (windowId?: string) => export const delChatRecordByIndex = (chatId: string, index: number) =>
windowId ? DELETE(`/chat/delLastMessage?windowId=${windowId}`) : null; DELETE(`/chat/delChatRecordByIndex?chatId=${chatId}&index=${index}`);

25
src/api/data.ts Normal file
View File

@@ -0,0 +1,25 @@
import { GET, POST, DELETE, PUT } from './request';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
import type { DataListItem } from '@/types/data';
import type { PagingData } from '../types/index';
import type { DataItemSchema } from '@/types/mongoSchema';
import type { CreateDataProps } from '@/pages/data/components/CreateDataModal';
export const getDataList = () => GET<DataListItem[]>(`/data/getDataList`);
export const postData = (data: CreateDataProps) => POST<string>(`/data/postData`, data);
export const postSplitData = (dataId: string, text: string) =>
POST(`/data/splitData`, { dataId, text });
export const updateDataName = (dataId: string, name: string) =>
PUT(`/data/putDataName?dataId=${dataId}&name=${name}`);
export const delData = (dataId: string) => DELETE(`/data/delData?dataId=${dataId}`);
type GetDataItemsProps = RequestPaging & {
dataId: string;
};
export const getDataItems = (data: GetDataItemsProps) =>
GET<PagingData<DataItemSchema>>(`/data/getDataItems?${Obj2Query(data)}`);

View File

@@ -1,17 +1,21 @@
import { getToken } from '../utils/user';
interface StreamFetchProps { interface StreamFetchProps {
url: string; url: string;
data: any; data: any;
onMessage: (text: string) => void; onMessage: (text: string) => void;
abortSignal: AbortController;
} }
export const streamFetch = ({ url, data, onMessage }: StreamFetchProps) => export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise(async (resolve, reject) => { new Promise(async (resolve, reject) => {
try { try {
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
Authorization: getToken() || ''
}, },
body: JSON.stringify(data) body: JSON.stringify(data),
signal: abortSignal.signal
}); });
const reader = res.body?.getReader(); const reader = res.body?.getReader();
if (!reader) return; if (!reader) return;

View File

@@ -1,28 +1,63 @@
import { GET, POST, DELETE, PUT } from './request'; import { GET, POST, DELETE, PUT } from './request';
import type { ModelType } from '@/types/model'; import type { ModelSchema, ModelDataSchema, ModelSplitDataSchema } from '@/types/mongoSchema';
import { ModelUpdateParams } from '@/types/model'; import { ModelUpdateParams } from '@/types/model';
import { TrainingItemType } from '../types/training'; import { TrainingItemType } from '../types/training';
import { RequestPaging } from '../types/index';
import { Obj2Query } from '@/utils/tools';
export const getMyModels = () => GET<ModelType[]>('/model/list'); export const getMyModels = () => GET<ModelSchema[]>('/model/list');
export const postCreateModel = (data: { name: string; serviceModelName: string }) => export const postCreateModel = (data: { name: string; serviceModelName: string }) =>
POST<ModelType>('/model/create', data); POST<ModelSchema>('/model/create', data);
export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`); export const delModelById = (id: string) => DELETE(`/model/del?modelId=${id}`);
export const getModelById = (id: string) => GET<ModelType>(`/model/detail?modelId=${id}`); export const getModelById = (id: string) => GET<ModelSchema>(`/model/detail?modelId=${id}`);
export const putModelById = (id: string, data: ModelUpdateParams) => export const putModelById = (id: string, data: ModelUpdateParams) =>
PUT(`/model/update?modelId=${id}`, data); PUT(`/model/update?modelId=${id}`, data);
export const postTrainModel = (id: string, form: FormData) => export const postTrainModel = (id: string, form: FormData) =>
POST(`/model/train?modelId=${id}`, form, { POST(`/model/train/train?modelId=${id}`, form, {
headers: { headers: {
'content-type': 'multipart/form-data' 'content-type': 'multipart/form-data'
} }
}); });
export const putModelTrainingStatus = (id: string) => PUT(`/model/putTrainStatus?modelId=${id}`); export const putModelTrainingStatus = (id: string) =>
PUT(`/model/train/putTrainStatus?modelId=${id}`);
export const getModelTrainings = (id: string) => export const getModelTrainings = (id: string) =>
GET<TrainingItemType[]>(`/model/getTrainings?modelId=${id}`); GET<TrainingItemType[]>(`/model/train/getTrainings?modelId=${id}`);
/* 模型 data */
type GetModelDataListProps = RequestPaging & {
modelId: string;
};
export const getModelDataList = (props: GetModelDataListProps) =>
GET(`/model/data/getModelData?${Obj2Query(props)}`);
export const getExportDataList = (modelId: string) =>
GET<string>(`/model/data/exportModelData?modelId=${modelId}`);
export const getModelSplitDataList = (modelId: string) =>
GET<ModelSplitDataSchema[]>(`/model/data/getSplitData?modelId=${modelId}`);
export const postModelDataInput = (data: {
modelId: string;
data: { text: ModelDataSchema['text']; q: ModelDataSchema['q'] }[];
}) => POST<number>(`/model/data/pushModelDataInput`, data);
export const postModelDataFileText = (data: { modelId: string; text: string; prompt: string }) =>
POST(`/model/data/splitData`, data);
export const postModelDataJsonData = (
modelId: string,
jsonData: { prompt: string; completion: string; vector?: number[] }[]
) => POST(`/model/data/pushModelDataJson`, { modelId, data: jsonData });
export const putModelDataById = (data: { dataId: string; text: string; q?: string }) =>
PUT('/model/data/putModelData', data);
export const delOneModelData = (dataId: string) =>
DELETE(`/model/data/delModelDataById?dataId=${dataId}`);

View File

@@ -37,7 +37,7 @@ function checkRes(data: ResponseDataType) {
console.log('error->', data, 'data is empty'); console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常'); return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) { } else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data.message); return Promise.reject(data);
} }
return data.data; return data.data;
} }

13
src/api/response/chat.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import type { ChatPopulate, ModelSchema } from '@/types/mongoSchema';
import type { ChatItemType } from '@/types/chat';
export type InitChatResponse = {
chatId: string;
modelId: string;
name: string;
avatar: string;
intro: string;
chatModel: ModelSchema.service.chatModel; // 对话模型名
modelName: ModelSchema.service.modelName; // 底层模型
history: ChatItemType[];
};

View File

@@ -1,8 +1,11 @@
import { GET, POST, PUT } from './request'; import { GET, POST, PUT } from './request';
import { createHashPassword } from '@/utils/tools'; import { createHashPassword, Obj2Query } from '@/utils/tools';
import { ResLogin } from './response/user'; import { ResLogin } from './response/user';
import { EmailTypeEnum } from '@/constants/common'; import { EmailTypeEnum } from '@/constants/common';
import { UserType, UserUpdateParams } from '@/types/user'; import { UserType, UserUpdateParams } from '@/types/user';
import type { PagingData, RequestPaging } from '@/types';
import { BillSchema, PaySchema } from '@/types/mongoSchema';
import { adaptBill } from '@/utils/adapt';
export const sendCodeToEmail = ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) => export const sendCodeToEmail = ({ email, type }: { email: string; type: `${EmailTypeEnum}` }) =>
GET('/user/sendEmail', { email, type }); GET('/user/sendEmail', { email, type });
@@ -46,3 +49,19 @@ export const postLogin = ({ email, password }: { email: string; password: string
}); });
export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data); export const putUserInfo = (data: UserUpdateParams) => PUT('/user/update', data);
export const getUserBills = (data: RequestPaging) =>
GET<PagingData<BillSchema>>(`/user/getBill?${Obj2Query(data)}`).then((res) => ({
...res,
data: res.data.map((bill) => adaptBill(bill))
}));
export const getPayOrders = () => GET<PaySchema[]>(`/user/getPayOrders`);
export const getPayCode = (amount: number) =>
GET<{
codeUrl: string;
payId: string;
}>(`/user/getPayCode?amount=${amount}`);
export const checkPayResult = (payId: string) => GET<number>(`/user/checkPayResult?payId=${payId}`);

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805359001" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1328" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><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" p-id="1329"></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" p-id="1330"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805221456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><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" p-id="1174"></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" p-id="1175"></path></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679114254212" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2776" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M923.733333 394.666667c-85.333333-70.4-206.933333-174.933333-362.666666-309.333334C533.333333 61.866667 490.666667 61.866667 462.933333 85.333333c-155.733333 134.4-277.333333 238.933333-362.666666 309.333334-14.933333 14.933333-25.6 34.133333-25.6 53.333333 0 38.4 32 70.4 70.4 70.4H192v358.4c0 29.866667 23.466667 53.333333 53.333333 53.333333H405.333333c29.866667 0 53.333333-23.466667 53.333334-53.333333v-206.933333h106.666666v206.933333c0 29.866667 23.466667 53.333333 53.333334 53.333333h160c29.866667 0 53.333333-23.466667 53.333333-53.333333V518.4h46.933333c38.4 0 70.4-32 70.4-70.4 0-21.333333-10.666667-40.533333-25.6-53.333333z m-44.8 59.733333h-57.6c-29.866667 0-53.333333 23.466667-53.333333 53.333333v358.4h-138.666667V661.333333c0-29.866667-23.466667-53.333333-53.333333-53.333333h-128c-29.866667 0-53.333333 23.466667-53.333333 53.333333v206.933334H256V507.733333c0-29.866667-23.466667-53.333333-53.333333-53.333333H145.066667c-4.266667 0-6.4-2.133333-6.4-6.4 0-2.133333 2.133333-4.266667 2.133333-6.4 85.333333-70.4 206.933333-174.933333 362.666667-309.333333 4.266667-4.266667 10.666667-4.266667 14.933333 0 155.733333 134.4 277.333333 238.933333 362.666667 309.333333 2.133333 2.133333 2.133333 2.133333 2.133333 4.266667 2.133333 6.4-2.133333 8.533333-4.266667 8.533333z" p-id="2777"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679316084227" class="icon" viewBox="0 0 1305 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="61.171875" height="48"><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" p-id="1174"></path></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679070302676" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><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" p-id="1174"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679410564438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2824" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M693.095316 281.760857l-131.632817 223.935003 103.718481 0 0 49.478312-120.846571 0 0 68.193688 120.846571 0 0 50.115659-120.846571 0 0 99.276514-62.164435 0L482.169975 673.483519 356.88022 673.483519l0-50.115659 125.289755 0 0-68.193688L356.88022 555.174172l0-49.478312 106.893053 0-130.364204-223.935003 70.099647 0c60.895822 111.230417 97.898433 181.748475 111.012698 211.562689l1.268612 0c4.441967-12.262847 16.596562-37.002611 36.474732-74.219292l74.536749-137.343396L693.095316 281.760857 693.095316 281.760857zM693.095316 281.760857" p-id="2825"></path><path d="M784.470674 621.448522c-15.061578 0-27.247797 12.187435-27.247797 27.247797s12.187435 27.247797 27.247797 27.247797l71.98128 0c-61.204765 128.843816-192.338895 217.986027-344.464118 217.986027-210.6687 0-381.478892-170.782216-381.478892-381.475243 0-210.696675 170.810191-381.465512 381.478892-381.465512 192.121175 0 350.635679 142.189179 377.137878 326.968701l55.08064 0C917.333181 242.953241 734.255278 76.493794 511.987837 76.493794 271.197197 76.493794 76.012135 271.688586 76.012135 512.456117c0 240.762665 195.185062 435.972053 435.975702 435.972053 164.236031 0 307.128238-90.894915 381.475243-225.064956l0 61.57574c0 15.061578 12.187435 27.247797 27.276989 27.247797 15.004412 0 27.247797-12.187435 27.247797-27.247797L947.987865 648.697535c0-3.297419 0-27.247797-27.247797-27.247797L784.470674 621.449738 784.470674 621.448522zM784.470674 621.448522" p-id="2826"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679070718083" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5975" id="mx_n_1679070718084" width="48" height="48" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M1023.82 694.91v146.26c0 102.38-80.44 182.82-182.82 182.82H182.83C80.45 1024 0 943.56 0 841.18V183.01C0 80.63 80.45 0.19 182.83 0.19h146.26c21.94 0 36.57 14.62 36.57 36.57 0 21.94-14.62 36.56-36.57 36.56H182.83c-58.5 0-109.7 51.19-109.7 109.7v658.17c0 58.5 51.19 109.7 109.7 109.7h658.17c58.5 0 109.7-51.19 109.7-109.7V694.91c0-21.94 14.62-36.56 36.56-36.56 21.93 0 36.56 14.63 36.56 36.56z" p-id="5976"></path><path d="M1012.6 292.61L684.73 5.86c-6.56-5.7-15.02-6.32-21.96-1.49-6.94 4.83-11.31 14.24-11.31 24.65v132.66h-80.9c-84.89 0-164.74 41.49-224.82 116.92-29.27 36.79-52.28 79.65-68.44 127.34C260.57 455.5 252.11 508.02 252.11 562.27c0 40.13 4.65 85.72 12.17 118.79 2.47 11.02 9.89 18.95 18.72 19.94h1.81c8.08 0 15.59-6.07 19.21-15.61 50.29-134.27 154.86-220.98 266.46-220.98h80.9v138.12c0 10.28 4.37 19.69 11.31 24.65 6.94 4.83 15.4 4.33 21.96-1.49l327.96-286.75c5.89-5.21 9.51-13.87 9.51-23.16-0.01-9.3-3.53-17.97-9.52-23.17z m-88.21 16.04L717.4 477.58v-81.92c0-11.07-7.41-20.08-16.52-20.08h-78.98c-49.84 0-95.79 2.5-146.79 32.25-30.31 17.68-130.92 89.06-150 121.09-0.51-7.94 20.14-85.47 23-92.41C390.11 334.54 504.6 237.8 621.9 237.8h78.98c9.1 0 16.52-9.02 16.52-20.08v-78l206.99 168.93z" p-id="5977"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,23 +1,24 @@
type TIconfont = { import React from 'react';
name: string; import type { IconProps } from '@chakra-ui/react';
color?: string; import { Icon } from '@chakra-ui/react';
width?: number | string; import dynamic from 'next/dynamic';
height?: number | string;
className?: string; const map = {
model: require('./icons/model.svg').default,
share: require('./icons/share.svg').default,
home: require('./icons/home.svg').default,
menu: require('./icons/menu.svg').default,
pay: require('./icons/pay.svg').default,
copy: require('./icons/copy.svg').default,
chatSend: require('./icons/chatSend.svg').default
}; };
function Icon({ name, color = 'inherit', width = 16, height = 16, className = '' }: TIconfont) { export type IconName = keyof typeof map;
const style = {
fill: color, const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
width, return map[name] ? (
height <Icon as={map[name]} w={w} h={h} boxSizing={'content-box'} verticalAlign={'top'} {...props} />
) : null;
}; };
return ( export default MyIcon;
<svg className={`icon ${className}`} aria-hidden="true" style={style}>
<use xlinkHref={`#${name}`}></use>
</svg>
);
}
export default Icon;

View File

@@ -0,0 +1,23 @@
type TIconfont = {
name: string;
color?: string;
width?: number | string;
height?: number | string;
className?: string;
};
function Iconfont({ 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 Iconfont;

View File

@@ -1,15 +1,13 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useToast } from '@chakra-ui/react'; import { useToast } from '@chakra-ui/react';
import { getTokenLogin } from '@/api/user';
import { useUserStore } from '@/store/user'; import { useUserStore } from '@/store/user';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
const unAuthPage: { [key: string]: boolean } = { const unAuthPage: { [key: string]: boolean } = {
'/': true, '/': true,
'/login': true, '/login': true
'/chat': true
}; };
const Auth = ({ children }: { children: JSX.Element }) => { const Auth = ({ children }: { children: JSX.Element }) => {
@@ -19,7 +17,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
position: 'top', position: 'top',
status: 'warning' status: 'warning'
}); });
const { userInfo, setUserInfo } = useUserStore(); const { userInfo, initUserInfo } = useUserStore();
const { setLoading } = useGlobalStore(); const { setLoading } = useGlobalStore();
useQuery( useQuery(
@@ -29,15 +27,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
return setLoading(false); return setLoading(false);
} else { } else {
setLoading(true); setLoading(true);
return getTokenLogin(); return initUserInfo();
} }
}, },
{ {
onSuccess(user) {
if (user) {
setUserInfo(user);
}
},
onError(error) { onError(error) {
console.log('error->', error); console.log('error->', error);
router.push('/login'); router.push('/login');

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Box } from '@chakra-ui/react'; import { Box, useColorMode, Flex } from '@chakra-ui/react';
import Link from 'next/link';
import Navbar from './navbar'; import Navbar from './navbar';
import NavbarPhone from './navbarPhone'; import NavbarPhone from './navbarPhone';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -30,8 +29,8 @@ const navbarList = [
// { // {
// label: '数据', // label: '数据',
// icon: 'icon-datafull', // icon: 'icon-datafull',
// link: '/training/dataList', // link: '/data/list',
// activeLink: ['/training/dataList'] // activeLink: ['/data/list', '/data/detail']
// }, // },
{ {
label: '账号', label: '账号',
@@ -44,9 +43,16 @@ const navbarList = [
const Layout = ({ children }: { children: JSX.Element }) => { const Layout = ({ children }: { children: JSX.Element }) => {
const { isPc } = useScreen(); const { isPc } = useScreen();
const router = useRouter(); const router = useRouter();
const { colorMode, setColorMode } = useColorMode();
const { Loading } = useLoading({ defaultLoading: true }); const { Loading } = useLoading({ defaultLoading: true });
const { loading } = useGlobalStore(); const { loading } = useGlobalStore();
useEffect(() => {
if (colorMode === 'dark' && router.pathname !== '/chat') {
setColorMode('light');
}
}, [colorMode, router.pathname, setColorMode]);
return ( return (
<> <>
{!unShowLayoutRoute[router.pathname] ? ( {!unShowLayoutRoute[router.pathname] ? (
@@ -56,29 +62,21 @@ const Layout = ({ children }: { children: JSX.Element }) => {
<Box h={'100%'} position={'fixed'} left={0} top={0} w={'80px'}> <Box h={'100%'} position={'fixed'} left={0} top={0} w={'80px'}>
<Navbar navbarList={navbarList} /> <Navbar navbarList={navbarList} />
</Box> </Box>
<Box ml={'80px'} p={7}> <Box h={'100%'} ml={'80px'}>
<Box maxW={'1100px'} m={'auto'}> <Box h={'100%'} py={7} px={'5vw'} m={'auto'} overflowY={'auto'}>
<Auth>{children}</Auth> <Auth>{children}</Auth>
</Box> </Box>
</Box> </Box>
</> </>
) : ( ) : (
<Box pt={'60px'}> <Flex h={'100%'} flexDirection={'column'}>
<Box <Box h={'60px'} borderBottom={'1px solid rgba(0,0,0,0.1)'}>
h={'60px'}
position={'fixed'}
top={0}
left={0}
right={0}
zIndex={100}
borderBottom={'1px solid rgba(0,0,0,0.1)'}
>
<NavbarPhone navbarList={navbarList} /> <NavbarPhone navbarList={navbarList} />
</Box> </Box>
<Box py={3} px={4}> <Box flex={'1 0 0'} h={0} py={3} px={4} overflowY={'auto'}>
<Auth>{children}</Auth> <Auth>{children}</Auth>
</Box> </Box>
</Box> </Flex>
)} )}
</Box> </Box>
) : ( ) : (

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Icon from '../Icon'; import Icon from '../Iconfont';
export enum NavbarTypeEnum { export enum NavbarTypeEnum {
normal = 'normal', normal = 'normal',
@@ -45,12 +45,12 @@ const Navbar = ({
flexDirection={'column'} flexDirection={'column'}
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
onClick={() => onClick={() => {
!item.activeLink.includes(router.pathname) && if (item.link === router.pathname) return;
router.push(item.link, undefined, { router.push(item.link, undefined, {
shallow: true shallow: true
}) });
} }}
cursor={'pointer'} cursor={'pointer'}
fontSize={'sm'} fontSize={'sm'}
w={'60px'} w={'60px'}

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Icon from '../Icon'; import Icon from '../Iconfont';
import { import {
Flex, Flex,
Drawer, Drawer,
@@ -57,6 +57,7 @@ const NavbarPhone = ({
alignItems={'center'} alignItems={'center'}
justifyContent={'center'} justifyContent={'center'}
onClick={() => { onClick={() => {
if (item.link === router.pathname) return;
router.push(item.link); router.push(item.link);
onClose(); onClose();
}} }}

View File

@@ -4,7 +4,7 @@
width: 4px; width: 4px;
height: 14px; height: 14px;
transform: translate(4px, 2px) scaleY(1.3); transform: translate(4px, 2px) scaleY(1.3);
background-color: rgba(0, 0, 0, 0.7); background-color: var(--chakra-colors-chakra-body-text);
animation: blink 0.6s infinite; animation: blink 0.6s infinite;
} }
.animation { .animation {
@@ -14,7 +14,7 @@
width: 4px; width: 4px;
height: 14px; height: 14px;
transform: translate(4px, 2px) scaleY(1.3); transform: translate(4px, 2px) scaleY(1.3);
background-color: rgba(0, 0, 0, 0.7); background-color: var(--chakra-colors-chakra-body-text);
animation: blink 0.6s infinite; animation: blink 0.6s infinite;
} }
} }
@@ -55,7 +55,7 @@
.markdown h6 { .markdown h6 {
cursor: text; cursor: text;
font-weight: bold; font-weight: bold;
margin: 20px 0 10px; margin: 10px 0;
padding: 0; padding: 0;
position: relative; position: relative;
} }
@@ -65,7 +65,6 @@
.markdown h4 .mini-icon-link, .markdown h4 .mini-icon-link,
.markdown h5 .mini-icon-link, .markdown h5 .mini-icon-link,
.markdown h6 .mini-icon-link { .markdown h6 .mini-icon-link {
color: #000000;
display: none; display: none;
} }
.markdown h1:hover a.anchor, .markdown h1:hover a.anchor,
@@ -103,11 +102,9 @@
font-size: inherit; font-size: inherit;
} }
.markdown h1 { .markdown h1 {
color: #000000;
font-size: 28px; font-size: 28px;
} }
.markdown h2 { .markdown h2 {
color: #000000;
font-size: 24px; font-size: 24px;
} }
.markdown h3 { .markdown h3 {
@@ -120,8 +117,7 @@
font-size: 14px; font-size: 14px;
} }
.markdown h6 { .markdown h6 {
color: #777777; font-size: 12px;
font-size: 14px;
} }
.markdown p, .markdown p,
.markdown blockquote, .markdown blockquote,
@@ -130,15 +126,7 @@
.markdown dl, .markdown dl,
.markdown table, .markdown table,
.markdown pre { .markdown pre {
margin: 15px 0; margin: 10px 0;
}
.markdown hr {
background: url('https://a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-350cca8f57223ebd53603021b2e670f4f319f1b7.png')
repeat-x scroll 0 0 transparent;
border: 0 none;
color: #cccccc;
height: 4px;
padding: 0;
} }
.markdown > h2:first-child, .markdown > h2:first-child,
.markdown > h1:first-child, .markdown > h1:first-child,
@@ -172,7 +160,7 @@
} }
.markdown ul, .markdown ul,
.markdown ol { .markdown ol {
padding-left: 30px; padding-left: 1em;
} }
.markdown ul.no-list, .markdown ul.no-list,
.markdown ol.no-list { .markdown ol.no-list {
@@ -321,7 +309,6 @@
} }
.markdown code, .markdown code,
.markdown tt { .markdown tt {
background-color: #f0f0f0;
border: 1px solid #eaeaea; border: 1px solid #eaeaea;
border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px;
margin: 0 2px; margin: 0 2px;
@@ -336,7 +323,6 @@
} }
.markdown .highlight pre, .markdown .highlight pre,
.markdown pre { .markdown pre {
background-color: #f0f0f0;
border: 1px solid #cccccc; border: 1px solid #cccccc;
border-radius: 3px 3px 3px 3px; border-radius: 3px 3px 3px 3px;
font-size: max(0.9em, 14px); font-size: max(0.9em, 14px);
@@ -351,10 +337,14 @@
} }
.markdown { .markdown {
text-align: justify; text-align: justify;
word-break: break-all;
overflow-y: hidden; overflow-y: hidden;
tab-size: 4; tab-size: 4;
word-spacing: normal; word-spacing: normal;
word-break: break-all;
p {
white-space: pre-line;
}
pre { pre {
display: block; display: block;
@@ -365,11 +355,11 @@
border-radius: 0; border-radius: 0;
background-color: #222 !important; background-color: #222 !important;
overflow-x: auto; overflow-x: auto;
color: #fff;
} }
pre code { pre code {
background-color: #222 !important; background-color: #222 !important;
color: #fff;
width: 100%; width: 100%;
} }
@@ -381,6 +371,7 @@
table { table {
border-collapse: separate; border-collapse: separate;
border-spacing: 0px; border-spacing: 0px;
color: var(--chakra-colors-gray-700);
thead tr:first-child th { thead tr:first-child th {
border-bottom-width: 1px; border-bottom-width: 1px;

View File

@@ -1,7 +1,7 @@
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import { useCopyData } from '@/utils/tools'; import { useCopyData } from '@/utils/tools';
import Icon from '@/components/Icon'; import Icon from '@/components/Icon';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
@@ -12,7 +12,7 @@ import 'katex/dist/katex.min.css';
import styles from './index.module.scss'; import styles from './index.module.scss';
import { codeLight } from './codeLight'; import { codeLight } from './codeLight';
const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean }) => { const Markdown = ({ source, isChatting = false }: { source: string; isChatting?: boolean }) => {
const formatSource = useMemo(() => source, [source]); const formatSource = useMemo(() => source, [source]);
const { copyData } = useCopyData(); const { copyData } = useCopyData();
@@ -34,14 +34,14 @@ const Markdown = ({ source, isChatting }: { source: string; isChatting: boolean
<Flex <Flex
py={2} py={2}
px={5} px={5}
backgroundColor={'#323641'} backgroundColor={useColorModeValue('#323641', 'gray.600')}
color={'#fff'} color={'#fff'}
fontSize={'sm'} fontSize={'sm'}
userSelect={'none'} userSelect={'none'}
> >
<Box flex={1}>{match?.[1]}</Box> <Box flex={1}>{match?.[1]}</Box>
<Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}> <Flex cursor={'pointer'} onClick={() => copyData(code)} alignItems={'center'}>
<Icon name={'icon-fuzhi'} width={15} height={15} color={'#fff'}></Icon> <Icon name={'copy'} width={15} height={15} fill={'#fff'}></Icon>
<Box ml={1}></Box> <Box ml={1}></Box>
</Flex> </Flex>
</Flex> </Flex>

View File

@@ -0,0 +1,76 @@
import React, { useRef, useEffect, useMemo } from 'react';
import type { BoxProps } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import { throttle } from 'lodash';
import { useLoading } from '@/hooks/useLoading';
interface Props extends BoxProps {
nextPage: () => void;
isLoadAll: boolean;
requesting: boolean;
children: React.ReactNode;
initRequesting?: boolean;
}
const ScrollData = ({
children,
nextPage,
isLoadAll,
requesting,
initRequesting,
...props
}: Props) => {
const { Loading } = useLoading({ defaultLoading: true });
const elementRef = useRef<HTMLDivElement>(null);
const loadText = useMemo(() => {
if (requesting) return '请求中……';
if (isLoadAll) return '已加载全部';
return '点击加载更多';
}, [isLoadAll, requesting]);
useEffect(() => {
if (!elementRef.current) return;
const scrolling = throttle((e: Event) => {
const element = e.target as HTMLDivElement;
if (!element) return;
// 当前滚动位置
const scrollTop = element.scrollTop;
// 可视高度
const clientHeight = element.clientHeight;
// 内容总高度
const scrollHeight = element.scrollHeight;
// 判断是否滚动到底部
if (scrollTop + clientHeight + 100 >= scrollHeight) {
nextPage();
}
}, 100);
elementRef.current.addEventListener('scroll', scrolling);
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
elementRef.current?.removeEventListener('scroll', scrolling);
};
}, [elementRef, nextPage]);
return (
<Box {...props} ref={elementRef} overflowY={'auto'} position={'relative'}>
{children}
<Box
mt={2}
fontSize={'xs'}
color={'blackAlpha.500'}
textAlign={'center'}
cursor={loadText === '点击加载更多' ? 'pointer' : 'default'}
onClick={() => {
if (loadText !== '点击加载更多') return;
nextPage();
}}
>
{loadText}
</Box>
{initRequesting && <Loading fixed={false} />}
</Box>
);
};
export default ScrollData;

View File

@@ -0,0 +1,82 @@
import React, { useMemo } from 'react';
import {
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
SliderMark,
Box
} from '@chakra-ui/react';
const MySlider = ({
markList,
setVal,
activeVal,
max = 100,
min = 0,
step = 1
}: {
markList: {
label: string | number;
value: number;
}[];
activeVal?: number;
setVal: (index: number) => void;
max?: number;
min?: number;
step?: number;
}) => {
const startEndPointStyle = {
content: '""',
borderRadius: '10px',
width: '10px',
height: '10px',
backgroundColor: '#ffffff',
border: '2px solid #D7DBE2',
position: 'absolute',
zIndex: 1,
top: 0,
transform: 'translateY(-3px)'
};
const value = useMemo(() => {
const index = markList.findIndex((item) => item.value === activeVal);
return index > -1 ? index : 0;
}, [activeVal, markList]);
return (
<Slider max={max} min={min} step={step} size={'lg'} value={value} onChange={setVal}>
{markList.map((item, i) => (
<SliderMark
key={item.value}
value={i}
mt={3}
fontSize={'sm'}
transform={'translateX(-50%)'}
{...(activeVal === item.value ? { color: 'blue.500', fontWeight: 'bold' } : {})}
>
<Box px={3} cursor={'pointer'}>
{item.label}
</Box>
</SliderMark>
))}
<SliderTrack
bg={'#EAEDF3'}
overflow={'visible'}
h={'4px'}
_before={{
...startEndPointStyle,
left: '-5px'
}}
_after={{
...startEndPointStyle,
right: '-5px'
}}
>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb border={'2.5px solid'} borderColor={'blue.500'}></SliderThumb>
</Slider>
);
};
export default MySlider;

View File

@@ -0,0 +1,49 @@
import React from 'react';
import {
Box,
Button,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useColorModeValue
} from '@chakra-ui/react';
import Image from 'next/image';
const WxConcat = ({ onClose }: { onClose: () => void }) => {
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
<ModalHeader>wx交流群</ModalHeader>
<ModalCloseButton />
<ModalBody textAlign={'center'}>
<Image
style={{ margin: 'auto' }}
src={'/imgs/wxcode300.jpg'}
width={200}
height={200}
alt=""
/>
<Box mt={2}>
:{' '}
<Box as={'span'} userSelect={'all'}>
YNyiqi
</Box>
</Box>
</ModalBody>
<ModalFooter>
<Button variant={'outline'} onClick={onClose}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default WxConcat;

View File

@@ -3,42 +3,62 @@ export enum EmailTypeEnum {
findPassword = 'findPassword' findPassword = 'findPassword'
} }
export const PRICE_SCALE = 100000;
export const introPage = ` export const introPage = `
## 欢迎使用 Fast GPT ## 欢迎使用 Fast GPT
[Git 仓库](https://github.com/c121914yu/FastGPT) [Git 仓库](https://github.com/c121914yu/FastGPT)
时间比较赶,介绍没来得及完善,先直接上怎么使用: ### 交流群/问题反馈
wx: YNyiqi
![](/imgs/wxcode300.jpg)
### 快速开始
1. 使用邮箱注册账号。 1. 使用邮箱注册账号。
2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。 2. 进入账号页面,添加关联账号,目前只有 openai 的账号可以添加,直接去 openai 官网,把 API Key 粘贴过来。
3. 进入模型页,创建一个模型,建议直接用 ChatGPT。 3. 如果填写了自己的 openai 账号,使用时会直接用你的账号。如果没有填写,需要付费使用平台的账号。
4. 在模型列表点击【对话】,即可使用 API 进行聊天。 4. 进入模型页,创建一个模型,建议直接用 ChatGPT。
5. 在模型列表点击【对话】,即可使用 API 进行聊天。
### 模型配置 ### 定制 prompt
1. **提示语**:会在每个对话框的第一句自动加入,用于限定该模型的对话内容。 1. 进入模型编辑页
2. 调整温度和提示词
3. 使用该模型对话。每次对话时,提示词和温度都会自动注入,方便管理个人的模型。建议把自己日常经常需要使用的 5~10 个方向预设好。
### 知识库
2. **单句最大长度**:每个聊天,单次输入内容的最大长度。 1. 创建模型时选择【知识库】
2. 进入模型编辑页
3. 导入数据,可以选择手动导入,或者选择文件导入。文件导入会自动调用 chatGPT 理解文件内容,并生成知识库。
4. 使用该模型对话。
注意使用知识库模型对话时tokens 消耗会加快。
3. **上下文最大长度**每个聊天最多的轮数除以2建议设置为偶数。可以持续聊天但是旧的聊天内容会被截断AI 就不会知道被截取的内容。 `;
例如上下文最大长度为6。在第 4 轮对话时,第一轮对话的内容不会被计入。
export const chatProblem = `
4. **过期时间**:生成对话框后,这个对话框多久过期。 ## 常见问题
**内容长度**
5. **聊天最大加载次数**:单个对话框最多被加载几次,设置为-1代表不限制正数代表只能加载 n 次,防止被盗刷。 单次最长 4000 tokens, 上下文最长 8000 tokens, 上下文超长时会被截断。
### 对话框介绍 **删除和复制**
点击对话头像,可以选择复制或删除该条内容。
1. 每个对话框以 windowId 作为标识。
2. 每次点击【对话】,都会生成新的对话框,无法回到旧的对话框。对话框内刷新,会恢复对话内容。 **代理出错**
3. 直接分享对话框(网页)的链接给朋友,会共享同一个对话内容。但是!!!千万不要两个人同时用一个链接,会串味,还没解决这个问题 服务器代理不稳定,可以过一会儿再尝试
4. 如果想分享一个纯的对话框,可以把链接里 windowId 参数去掉。例如: `;
* 当前网页链接http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764&windowId=6402c94cb5d6283f76fb49 export const versionIntro = `
* 分享链接应为http://docgpt.ahapocket.cn/chat?chatId=6402c9f64cb5d6283f764 ## Fast GPT V2.3
* 数据集导出功能,可用于知识库分享。
### 其他问题 * 优化文件拆分功能,可自定义提示词。
还有其他问题,可以加我 wx: YNyiqi拉个交流群大家一起聊聊 * 定制知识库:创建模型时可以选择【知识库】模型, 可以手动导入知识点或者直接导入一个文件自动学习
`;
export const shareHint = `
你正准备分享对话,请确保分享链接不会滥用,因为它是使用的是你的 API key。
* 分享空白对话:为该模型创建一个空白的聊天分享出去。
* 分享当前对话:会把当前聊天的内容也分享出去,但是要注意不要多个人同时用一个聊天内容。
`; `;

6
src/constants/data.ts Normal file
View File

@@ -0,0 +1,6 @@
import type { DataType } from '@/types/data';
export const DataTypeTextMap: Record<DataType, string> = {
QA: '问答拆分',
abstract: '摘要总结'
};

View File

@@ -1,22 +1,66 @@
export enum OpenAiModelEnum { import type { ServiceName, ModelDataType, ModelSchema } from '@/types/mongoSchema';
import type { RedisModelDataItemType } from '@/types/redis';
export enum ChatModelNameEnum {
GPT35 = 'gpt-3.5-turbo', GPT35 = 'gpt-3.5-turbo',
GPT3 = 'text-davinci-003' VECTOR_GPT = 'VECTOR_GPT',
GPT3 = 'text-davinci-003',
VECTOR = 'text-embedding-ada-002'
} }
export const OpenAiList = [
export const ChatModelNameMap = {
[ChatModelNameEnum.GPT35]: 'gpt-3.5-turbo',
[ChatModelNameEnum.VECTOR_GPT]: 'gpt-3.5-turbo',
[ChatModelNameEnum.GPT3]: 'text-davinci-003',
[ChatModelNameEnum.VECTOR]: 'text-embedding-ada-002'
};
export type ModelConstantsData = {
serviceCompany: `${ServiceName}`;
name: string;
model: `${ChatModelNameEnum}`;
trainName: string; // 空字符串代表不能训练
maxToken: number;
contextMaxToken: number;
maxTemperature: number;
trainedMaxToken: number; // 训练后最大多少tokens
price: number; // 多少钱 / 1token单位: 0.00001元
};
export const modelList: ModelConstantsData[] = [
{ {
serviceCompany: 'openai',
name: 'chatGPT', name: 'chatGPT',
model: OpenAiModelEnum.GPT35, model: ChatModelNameEnum.GPT35,
trainName: 'turbo', trainName: '',
canTraining: false, maxToken: 4000,
maxToken: 4060 contextMaxToken: 7500,
trainedMaxToken: 2000,
maxTemperature: 2,
price: 3
}, },
{ {
name: 'GPT3', serviceCompany: 'openai',
model: OpenAiModelEnum.GPT3, name: '知识库',
trainName: 'davinci', model: ChatModelNameEnum.VECTOR_GPT,
canTraining: true, trainName: 'vector',
maxToken: 4060 maxToken: 4000,
contextMaxToken: 7500,
trainedMaxToken: 2000,
maxTemperature: 1,
price: 3
} }
// {
// serviceCompany: 'openai',
// name: 'GPT3',
// model: ChatModelNameEnum.GPT3,
// trainName: 'davinci',
// maxToken: 4000,
// contextMaxToken: 7500,
// trainedMaxToken: 2000,
// maxTemperature: 2,
// price: 30
// }
]; ];
export enum TrainingStatusEnum { export enum TrainingStatusEnum {
@@ -51,3 +95,34 @@ export const formatModelStatus = {
text: '已关闭' text: '已关闭'
} }
}; };
export const ModelDataStatusMap: Record<RedisModelDataItemType['status'], string> = {
ready: '训练完成',
waiting: '训练中'
};
export const defaultModel: ModelSchema = {
_id: '',
userId: '',
name: '',
avatar: '',
status: ModelStatusEnum.pending,
updateTime: Date.now(),
trainingTimes: 0,
systemPrompt: '',
intro: '',
temperature: 5,
service: {
company: 'openai',
trainId: '',
chatModel: ChatModelNameEnum.GPT35,
modelName: ChatModelNameEnum.GPT35
},
security: {
domain: ['*'],
contextMaxLen: 1,
contentMaxLen: 1,
expiredTime: 9999,
maxLoadAmount: 1
}
};

6
src/constants/redis.ts Normal file
View File

@@ -0,0 +1,6 @@
export const VecModelDataPrefix = 'model:data';
export const VecModelDataIdx = `idx:${VecModelDataPrefix}:hash`;
export enum ModelDataStatusEnum {
ready = 'ready',
waiting = 'waiting'
}

View File

@@ -45,8 +45,13 @@ const Button = defineStyleConfig({
} }
}, },
variants: { variants: {
outline: { white: {
borderWidth: '1.5px' color: '#fff',
backgroundColor: 'transparent',
border: '1px solid #ffffff',
_hover: {
backgroundColor: 'rgba(255,255,255,0.1)'
}
} }
}, },
defaultProps: { defaultProps: {
@@ -85,6 +90,13 @@ export const theme = extendTheme({
fonts: { fonts: {
body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"' body: '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"'
}, },
breakpoints: {
sm: '900px',
md: '1200px',
lg: '1500px',
xl: '1800',
'2xl': '2100'
},
components: { components: {
Modal: ModalTheme, Modal: ModalTheme,
Button Button

View File

@@ -1,5 +1,22 @@
export enum BillTypeEnum {
chat = 'chat',
splitData = 'splitData',
QA = 'QA',
abstract = 'abstract',
vector = 'vector',
return = 'return'
}
export enum PageTypeEnum { export enum PageTypeEnum {
login = 'login', login = 'login',
register = 'register', register = 'register',
forgetPassword = 'forgetPassword' forgetPassword = 'forgetPassword'
} }
export const BillTypeMap: Record<`${BillTypeEnum}`, string> = {
[BillTypeEnum.chat]: '对话',
[BillTypeEnum.splitData]: 'QA拆分',
[BillTypeEnum.QA]: 'QA拆分',
[BillTypeEnum.abstract]: '摘要总结',
[BillTypeEnum.vector]: '索引生成',
[BillTypeEnum.return]: '退款'
};

View File

@@ -19,9 +19,10 @@ export const useConfirm = ({ title = '提示', content }: { title?: string; cont
return { return {
openConfirm: useCallback( openConfirm: useCallback(
(confirm?: any, cancel?: any) => { (confirm?: any, cancel?: any) => {
onOpen();
confirmCb.current = confirm; confirmCb.current = confirm;
cancelCb.current = cancel; cancelCb.current = cancel;
return onOpen;
}, },
[onOpen] [onOpen]
), ),

View File

@@ -0,0 +1,81 @@
import { useState, useCallback, useMemo } from 'react';
import type { PagingData } from '../types/index';
import { IconButton, Flex, Box } from '@chakra-ui/react';
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useToast } from './useToast';
export const usePagination = <T = any,>({
api,
pageSize = 10,
params = {}
}: {
api: (data: any) => any;
pageSize?: number;
params?: Record<string, any>;
}) => {
const { toast } = useToast();
const [pageNum, setPageNum] = useState(1);
const [total, setTotal] = useState(0);
const [data, setData] = useState<T[]>([]);
const maxPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]);
const { mutate, isLoading } = useMutation({
mutationFn: async (num: number = pageNum) => {
try {
const res: PagingData<T> = await api({
pageNum: num,
pageSize,
...params
});
setPageNum(num);
setTotal(res.total);
setData(res.data);
} catch (error: any) {
toast({
title: error?.message || '获取数据异常',
status: 'error'
});
console.log(error);
}
}
});
useQuery(['init'], () => {
mutate(1);
return null;
});
const Pagination = useCallback(() => {
return (
<Flex alignItems={'center'} justifyContent={'end'}>
<IconButton
isDisabled={pageNum === 1}
icon={<ArrowBackIcon />}
aria-label={'left'}
size={'sm'}
onClick={() => mutate(pageNum - 1)}
/>
<Box mx={2}>
{pageNum}/{maxPage}
</Box>
<IconButton
isDisabled={pageNum === maxPage}
icon={<ArrowForwardIcon />}
aria-label={'left'}
size={'sm'}
onClick={() => mutate(pageNum + 1)}
/>
</Flex>
);
}, [maxPage, mutate, pageNum]);
return {
pageNum,
pageSize,
total,
data,
isLoading,
Pagination,
getData: mutate
};
};

81
src/hooks/usePaging.ts Normal file
View File

@@ -0,0 +1,81 @@
import { useState, useCallback } from 'react';
import type { PagingData } from '../types/index';
import { useQuery } from '@tanstack/react-query';
import { useToast } from './useToast';
export const usePaging = <T = any>({
api,
pageSize = 10,
params = {}
}: {
api: (data: any) => any;
pageSize?: number;
params?: Record<string, any>;
}) => {
const { toast } = useToast();
const [data, setData] = useState<T[]>([]);
const [pageNum, setPageNum] = useState(1);
const [total, setTotal] = useState(0);
const [isLoadAll, setIsLoadAll] = useState(false);
const [requesting, setRequesting] = useState(false);
const [initRequesting, setInitRequesting] = useState(false);
const getData = useCallback(
async (num: number, init = false) => {
if (requesting) return;
if (!init && isLoadAll) return;
if (init) {
setInitRequesting(true);
}
setRequesting(true);
try {
const res: PagingData<T> = await api({
pageNum: num,
pageSize,
...params
});
setData((state) => {
const data = init ? res.data : state.concat(res.data);
if (data.length >= res.total) {
setIsLoadAll(true);
}
setTotal(res.total);
setPageNum(num);
return data;
});
} catch (error: any) {
toast({
title: error?.message || '获取数据异常',
status: 'error'
});
console.log(error);
}
setRequesting(false);
setInitRequesting(false);
return null;
},
[api, isLoadAll, pageSize, params, requesting, toast]
);
const nextPage = useCallback(() => {
if (requesting || isLoadAll) return;
getData(pageNum + 1);
}, [getData, isLoadAll, pageNum, requesting]);
useQuery(['init'], () => getData(1, true));
return {
pageNum,
pageSize,
total,
data,
getData,
requesting,
isLoadAll,
nextPage,
initRequesting,
setData
};
};

33
src/hooks/useRequest.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { useToast } from '@/hooks/useToast';
import { useMutation } from '@tanstack/react-query';
import type { UseMutationOptions } from '@tanstack/react-query';
interface Props extends UseMutationOptions<any, any, any, any> {
successToast?: string;
errorToast?: string;
}
export const useRequest = ({ successToast, errorToast, onSuccess, onError, ...props }: Props) => {
const { toast } = useToast();
const mutation = useMutation<unknown, unknown, any, unknown>({
...props,
onSuccess(res, variables: void, context: unknown) {
onSuccess?.(res, variables, context);
successToast &&
toast({
title: successToast,
status: 'success'
});
},
onError(err: any, variables: void, context: unknown) {
onError?.(err, variables, context);
errorToast &&
toast({
title: typeof err === 'string' ? err : err?.message || errorToast,
status: 'error'
});
}
});
return mutation;
};

View File

@@ -0,0 +1,34 @@
import React, { useRef, useCallback } from 'react';
import { Box } from '@chakra-ui/react';
export const useSelectFile = (props?: { fileType?: string; multiple?: boolean }) => {
const { fileType = '*', multiple = false } = props || {};
const SelectFileDom = useRef<HTMLInputElement>(null);
const File = useCallback(
({ onSelect }: { onSelect: (e: File[]) => void }) => (
<Box position={'absolute'} w={0} h={0} overflow={'hidden'}>
<input
ref={SelectFileDom}
type="file"
accept={fileType}
multiple={multiple}
onChange={(e) => {
if (!e.target.files || e.target.files?.length === 0) return;
onSelect(Array.from(e.target.files));
}}
/>
</Box>
),
[fileType, multiple]
);
const onOpen = useCallback(() => {
SelectFileDom.current && SelectFileDom.current.click();
}, []);
return {
File,
onOpen
};
};

18
src/hooks/useTabs.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React, { useState, useCallback, useRef } from 'react';
export const useTabs = ({
tabs = []
}: {
tabs: {
id: string;
label: string;
}[];
}) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
return {
tabs,
activeTab,
setActiveTab
};
};

View File

@@ -1,7 +1,8 @@
import type { AppProps, NextWebVitalsMetric } from 'next/app'; import { useEffect } from 'react';
import type { AppProps } from 'next/app';
import Script from 'next/script'; import Script from 'next/script';
import Head from 'next/head'; import Head from 'next/head';
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
import Layout from '@/components/Layout'; import Layout from '@/components/Layout';
import { theme } from '@/constants/theme'; import { theme } from '@/constants/theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -9,6 +10,7 @@ import NProgress from 'nprogress'; //nprogress module
import Router from 'next/router'; import Router from 'next/router';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import '../styles/reset.scss'; import '../styles/reset.scss';
import { useToast } from '@/hooks/useToast';
//Binding events. //Binding events.
Router.events.on('routeChangeStart', () => NProgress.start()); Router.events.on('routeChangeStart', () => NProgress.start());
@@ -27,6 +29,17 @@ const queryClient = new QueryClient({
}); });
export default function App({ Component, pageProps }: AppProps) { export default function App({ Component, pageProps }: AppProps) {
const { toast } = useToast();
// 校验是否支持 click 事件
useEffect(() => {
if (typeof document.createElement('div').click !== 'function') {
toast({
title: '你的浏览器版本过低',
status: 'warning'
});
}
}, [toast]);
return ( return (
<> <>
<Head> <Head>
@@ -38,9 +51,12 @@ export default function App({ Component, pageProps }: AppProps) {
/> />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<Script src="/iconfont.js" strategy="afterInteractive"></Script> <Script src="/js/iconfont.js" strategy="afterInteractive"></Script>
<Script src="/js/qrcode.min.js" strategy="afterInteractive"></Script>
<Script src="/js/pdf.js" strategy="afterInteractive"></Script>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<Layout> <Layout>
<Component {...pageProps} /> <Component {...pageProps} />
</Layout> </Layout>

View File

@@ -1,45 +1,69 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser'; import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import type { ModelType } from '@/types/model';
import { getOpenAIApi, authChat } from '@/service/utils/chat'; import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent, openaiChatFilter } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai'; import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream'; import { PassThrough } from 'stream';
import { modelList } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { chatId, windowId, prompt } = req.body as { let step = 0; // step=1时表示开始了流响应
const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
try {
const { chatId, prompt } = req.body as {
prompt: ChatItemType; prompt: ChatItemType;
windowId: string;
chatId: string; chatId: string;
}; };
try { const { authorization } = req.headers;
if (!windowId || !chatId || !prompt) { if (!chatId || !prompt) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
await connectToDatabase(); await connectToDatabase();
let startTime = Date.now();
const { chat, userApiKey } = await authChat(chatId); const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
const model: ModelType = chat.modelId; const model: ModelSchema = chat.modelId;
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 读取对话内容 // 读取对话内容
const prompts: ChatItemType[] = (await ChatWindow.findById(windowId)).content; const prompts = [...chat.content, prompt];
prompts.push(prompt);
// 上下文长度过滤 // 如果有系统提示词,自动插入
const maxContext = model.security.contextMaxLen; if (model.systemPrompt) {
const filterPrompts = prompts.unshift({
prompts.length > maxContext + 2 obj: 'SYSTEM',
? [prompts[0], ...prompts.slice(prompts.length - maxContext)] value: model.systemPrompt
: prompts.slice(0, prompts.length); });
}
// 格式化文本内容 // 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
// 格式化文本内容成 chatgpt 格式
const map = { const map = {
Human: ChatCompletionRequestMessageRoleEnum.User, Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant, AI: ChatCompletionRequestMessageRoleEnum.Assistant,
@@ -51,42 +75,42 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
content: item.value content: item.value
}) })
); );
// 第一句话,强调代码类型 // console.log(formatPrompts);
formatPrompts.unshift({ // 计算温度
role: ChatCompletionRequestMessageRoleEnum.System, const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
content: '如果你想返回代码,请务必声明代码的类型!并且在代码块前加一个换行符。'
});
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey); const chatAPI = getOpenAIApi(userApiKey || systemKey);
// 发出请求 // 发出请求
const chatResponse = await chatAPI.createChatCompletion( const chatResponse = await chatAPI.createChatCompletion(
{ {
model: model.service.chatModel, model: model.service.chatModel,
temperature: 1, temperature: temperature,
// max_tokens: model.security.contentMaxLen, // max_tokens: modelConstantsData.maxToken,
messages: formatPrompts, messages: formatPrompts,
stream: true frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: true,
stop: ['.!?。']
}, },
{ {
timeout: 20000, timeout: 40000,
responseType: 'stream', responseType: 'stream',
httpsAgent httpsAgent
} }
); );
console.log(
formatPrompts.reduce((sum, item) => sum + item.content.length, 0), console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
'response success'
);
// 创建响应流 // 创建响应流
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8'); res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no'); res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform'); res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
const pass = new PassThrough(); let responseContent = '';
pass.pipe(res); stream.pipe(res);
const onParse = async (event: ParsedEvent | ReconnectInterval) => { const onParse = async (event: ParsedEvent | ReconnectInterval) => {
if (event.type !== 'event') return; if (event.type !== 'event') return;
@@ -95,20 +119,48 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try { try {
const json = JSON.parse(data); const json = JSON.parse(data);
const content: string = json?.choices?.[0].delta.content || ''; const content: string = json?.choices?.[0].delta.content || '';
if (!content) return; if (!content || (responseContent === '' && content === '\n')) return;
responseContent += content;
// console.log('content:', content) // console.log('content:', content)
pass.push(content.replace(/\n/g, '<br/>')); !stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
} catch (error) { } catch (error) {
error; error;
} }
}; };
const decoder = new TextDecoder();
try {
for await (const chunk of chatResponse.data as any) { for await (const chunk of chatResponse.data as any) {
const parser = createParser(onParse); if (stream.destroyed) {
parser.feed(decodeURIComponent(chunk)); // 流被中断了,直接忽略后面的内容
break;
} }
pass.push(null); const parser = createParser(onParse);
parser.feed(decoder.decode(chunk));
}
} catch (error) {
console.log('pipe error', error);
}
// close stream
!stream.destroyed && stream.push(null);
stream.destroy();
const promptsContent = formatPrompts.map((item) => item.content).join('');
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
userId,
chatId,
text: promptsContent + responseContent
});
} catch (err: any) { } catch (err: any) {
if (step === 1) {
// 直接结束流
console.log('error结束');
stream.destroy();
} else {
res.status(500); res.status(500);
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,
@@ -116,3 +168,4 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
} }
} }
}

View File

@@ -0,0 +1,62 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, index } = req.query as { chatId: string; index: string };
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!chatId || !index) {
throw new Error('缺少参数');
}
await connectToDatabase();
// 凭证校验
const userId = await authToken(authorization);
const chatRecord = await Chat.findById(chatId);
if (!chatRecord) {
throw new Error('找不到对话');
}
// 重新计算 index跳过已经被删除的内容
let unDeleteIndex = +index;
let deletedIndex = 0;
for (deletedIndex = 0; deletedIndex < chatRecord.content.length; deletedIndex++) {
if (!chatRecord.content[deletedIndex].deleted) {
unDeleteIndex--;
if (unDeleteIndex < 0) {
break;
}
}
}
// 删除最一条数据库记录, 也就是预发送的那一条
await Chat.updateOne(
{
_id: chatId,
userId
},
{
$set: {
[`content.${deletedIndex}.deleted`]: true,
updateTime: Date.now()
}
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,28 +0,0 @@
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
});
}
}

View File

@@ -2,16 +2,19 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Chat } from '@/service/mongo'; import { connectToDatabase, Model, Chat } from '@/service/mongo';
import { authToken } from '@/service/utils/tools'; import { authToken } from '@/service/utils/tools';
import { ModelType } from '@/types/model'; import type { ModelSchema } from '@/types/mongoSchema';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { modelId } = req.query; const { modelId, isShare = 'false' } = req.query as {
modelId: string;
isShare?: 'true' | 'false';
};
const { authorization } = req.headers; const { authorization } = req.headers;
if (!authorization) { if (!authorization) {
throw new Error('无权操作'); throw new Error('无权生成对话');
} }
if (!modelId) { if (!modelId) {
@@ -24,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase(); await connectToDatabase();
// 获取模型配置 // 获取模型配置
const model: ModelType | null = await Model.findOne({ const model = await Model.findOne<ModelSchema>({
_id: modelId, _id: modelId,
userId userId
}); });
@@ -38,11 +41,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
userId, userId,
modelId, modelId,
expiredTime: Date.now() + model.security.expiredTime, expiredTime: Date.now() + model.security.expiredTime,
loadAmount: model.security.maxLoadAmount loadAmount: model.security.maxLoadAmount,
isShare: isShare === 'true',
content: []
}); });
jsonRes(res, { jsonRes(res, {
data: response._id data: response._id // 即聊天框的 ID
}); });
} catch (err) { } catch (err) {
jsonRes(res, { jsonRes(res, {

View File

@@ -1,85 +1,173 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { connectToDatabase, Chat } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import type { ModelType } from '@/types/model'; import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { getOpenAIApi } from '@/service/utils/chat';
import { ChatItemType } from '@/types/chat';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent } from '@/service/utils/tools';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream';
import { modelList } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
/* 发送提示词 */ /* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { let step = 0; // step=1时表示开始了流响应
const { prompt, chatId } = req.body as { prompt: ChatItemType[]; chatId: string }; const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
if (!prompt || !chatId) { try {
const { chatId, prompt } = req.body as {
prompt: ChatItemType;
chatId: string;
};
const { authorization } = req.headers;
if (!chatId || !prompt) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
await connectToDatabase(); await connectToDatabase();
// 获取 chat 数据 const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
const chat = await Chat.findById(chatId)
.populate({
path: 'modelId',
options: {
strictPopulate: false
}
})
.populate({
path: 'userId',
options: {
strictPopulate: false
}
});
if (!chat || !chat.modelId || !chat.userId) { const model: ModelSchema = chat.modelId;
throw new Error('聊天已过期');
// 读取对话内容
const prompts = [...chat.content, prompt];
// 上下文长度过滤
const maxContext = model.security.contextMaxLen;
const filterPrompts =
prompts.length > maxContext ? prompts.slice(prompts.length - maxContext) : prompts;
// 格式化文本内容
const formatPrompts: string[] = filterPrompts.map((item: ChatItemType) => item.value);
// 如果有系统提示词,自动插入
if (model.systemPrompt) {
formatPrompts.unshift(`${model.systemPrompt}`);
} }
const model: ModelType = chat.modelId; const promptText = formatPrompts.join('</s>');
// 获取 user 的 apiKey // 计算温度
const user = chat.userId; const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
const userApiKey = user.accounts?.find((item: any) => item.type === 'openai')?.value; throw new Error('模型异常,请用 chatgpt 模型');
if (!userApiKey) {
throw new Error('缺少ApiKey, 无法请求');
} }
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// 获取 chatAPI // 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey); const chatAPI = getOpenAIApi(userApiKey || systemKey);
let startTime = Date.now();
// prompt处理 // console.log({
const formatPrompt = prompt.map((item) => `${item.value}\n\n###\n\n`).join(''); // model: model.service.chatModel,
// temperature: temperature,
// 发送请求 // prompt: promptText,
const response = await chatAPI.createCompletion( // stream: true,
// max_tokens:
// model.trainingTimes > 0 ? modelConstantsData.trainedMaxToken : modelConstantsData.maxToken,
// presence_penalty: -0.5, // 越大,越容易出现新内容
// frequency_penalty: 0.5, // 越大,重复内容越少
// stop: [`###`]
// });
// 发出请求
const chatResponse = await chatAPI.createCompletion(
{ {
model: model.service.modelName, model: model.service.chatModel,
prompt: formatPrompt, temperature: temperature,
temperature: 0.5, prompt: promptText,
max_tokens: model.security.contentMaxLen, stream: true,
top_p: 1, max_tokens:
frequency_penalty: 0, model.trainingTimes > 0
presence_penalty: 0.6, ? modelConstantsData.trainedMaxToken
stop: ['###'] : modelConstantsData.maxToken,
presence_penalty: -0.5, // 越大,越容易出现新内容
frequency_penalty: 0.5, // 越大,重复内容越少
stop: [`###`, '。!?.!.']
}, },
{ {
timeout: 40000,
responseType: 'stream',
httpsAgent httpsAgent
} }
); );
const responseMessage = response.data.choices[0]?.text; console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
jsonRes(res, { // 创建响应流
data: responseMessage res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
let responseContent = '';
stream.pipe(res);
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
if (event.type !== 'event') return;
const data = event.data;
if (data === '[DONE]') return;
try {
const json = JSON.parse(data);
const content: string = json?.choices?.[0].text || '';
// console.log('content:', content);
if (!content || (responseContent === '' && content === '\n')) return;
responseContent += content;
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
} catch (error) {
error;
}
};
const decoder = new TextDecoder();
try {
for await (const chunk of chatResponse.data as any) {
if (stream.destroyed) {
// 流被中断了,直接忽略后面的内容
break;
}
const parser = createParser(onParse);
parser.feed(decoder.decode(chunk));
}
} catch (error) {
console.log('pipe error', error);
}
// close stream
!stream.destroyed && stream.push(null);
stream.destroy();
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
userId,
chatId,
text: promptText + responseContent
}); });
} catch (err: any) { } catch (err: any) {
// console.log(err?.response);
if (step === 1) {
// 直接结束流
console.log('error结束');
stream.destroy();
} else {
res.status(500);
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,
error: err error: err
}); });
} }
} }
}

View File

@@ -1,12 +1,17 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
import type { ModelType } from '@/types/model'; import type { ChatPopulate } from '@/types/mongoSchema';
import type { InitChatResponse } from '@/api/response/chat';
import { authToken } from '@/service/utils/tools';
/* 获取我的模型 */ /* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { chatId, windowId } = req.query as { chatId: string; windowId?: string }; const { authorization } = req.headers;
const userId = await authToken(authorization);
const { chatId } = req.query as { chatId: string };
if (!chatId) { if (!chatId) {
throw new Error('缺少参数'); throw new Error('缺少参数');
@@ -15,70 +20,34 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase(); await connectToDatabase();
// 获取 chat 数据 // 获取 chat 数据
const chat = await Chat.findById(chatId).populate({ const chat = await Chat.findOne<ChatPopulate>({
_id: chatId,
userId
}).populate({
path: 'modelId', path: 'modelId',
options: { options: {
strictPopulate: false strictPopulate: false
} }
}); });
// 安全校验 if (!chat) {
if (!chat || chat.loadAmount === 0 || chat.expiredTime < Date.now()) { throw new Error('聊天框不存在');
throw new Error('聊天框已过期');
} }
if (chat.loadAmount > 0) { // filter 掉被 deleted 的内容
await Chat.updateOne( chat.content = chat.content.filter((item) => item.deleted !== true);
{
_id: chat._id
},
{
$inc: { loadAmount: -1 }
}
);
}
const model: ModelType = chat.modelId; const model = chat.modelId;
jsonRes<InitChatResponse>(res, {
/* 查找是否有记录 */
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: { data: {
windowId: responseId, chatId: chat._id,
chatSite: {
modelId: model._id, modelId: model._id,
name: model.name, name: model.name,
avatar: model.avatar, avatar: model.avatar,
secret: model.security, intro: model.intro,
chatModel: model.service.chatModel modelName: model.service.modelName,
}, chatModel: model.service.chatModel,
history: history ? history.content : defaultContent history: chat.content
} }
}); });
} catch (err) { } catch (err) {

View File

@@ -0,0 +1,277 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { connectToDatabase } from '@/service/mongo';
import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream';
import { modelList } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { connectRedis } from '@/service/redis';
import { VecModelDataPrefix } from '@/constants/redis';
import { vectorToBuffer } from '@/utils/tools';
import { openaiCreateEmbedding } from '@/service/utils/openai';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let step = 0; // step=1时表示开始了流响应
const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
try {
const { chatId, prompt } = req.body as {
prompt: ChatItemType;
chatId: string;
};
const { authorization } = req.headers;
if (!chatId || !prompt) {
throw new Error('缺少参数');
}
await connectToDatabase();
const redis = await connectRedis();
let startTime = Date.now();
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
const model: ModelSchema = chat.modelId;
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 获取 chatAPI
const chatAPI = getOpenAIApi(userApiKey || systemKey);
// 请求一次 chatgpt 拆解需求
const promptResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: 0,
// max_tokens: modelConstantsData.maxToken,
messages: [
{
role: 'system',
content: `服务端逻辑生成器。根据用户输入的需求,拆解成代码实现的步骤,并按格式返回: 1.\n2.\n3.\n ......
下面是一些例子:
实现一个手机号注册账号的方法,包含两个函数
* 发送手机验证码函数:
1. 从 query 中获取 phone
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}
3. 给 phone 发送一个短信验证码验证码长度为6位字符串内容为你正在注册laf, 验证码为code
4. 数据库添加数据,表为"codes",内容为 {phone, code}
* 注册函数
1. 从 body 中获取 phone 和 code
2. 校验手机号格式是否正确,不正确返回{error: "手机号格式错误"}
2. 获取数据库数据,表为"codes",查找是否有符合 phone, code 等于body参数的记录没有的话返回 {error:"验证码不正确"}
4. 添加数据库数据,表为"users" ,内容为{phone, code, createTime}
5. 删除数据库数据,删除 code 记录
---------------
更新博客记录。传入blogIdblogTexttags还需要记录更新的时间
1. 从 body 中获取 blogIdblogText 和 tags
2. 校验 blogId 是否为空,为空则返回 {error: "博客ID不能为空"}
3. 校验 blogText 是否为空,为空则返回 {error: "博客内容不能为空"}
4. 校验 tags 是否为数组,不是则返回 {error: "标签必须为数组"}
5. 获取当前时间,记录为 updateTime
6. 更新数据库数据,表为"blogs",更新符合 blogId 的记录的内容为{blogText, tags, updateTime}
7. 返回结果 {message: "更新博客记录成功"}`
},
{
role: 'user',
content: prompt.value
}
]
},
{
timeout: 40000,
httpsAgent
}
);
const promptResolve = promptResponse.data.choices?.[0]?.message?.content || '';
if (!promptResolve) {
throw new Error('gpt 异常');
}
prompt.value += `\n${promptResolve}`;
console.log('prompt resolve success, time:', `${(Date.now() - startTime) / 1000}s`);
// 获取提示词的向量
const { vector: promptVector } = await openaiCreateEmbedding({
isPay: !userApiKey,
apiKey: userApiKey || systemKey,
userId,
text: prompt.value
});
// 读取对话内容
const prompts = [...chat.content, prompt];
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
const redisData: any[] = await redis.sendCommand([
'FT.SEARCH',
`idx:${VecModelDataPrefix}:hash`,
`@modelId:{${String(
chat.modelId._id
)}} @vector:[VECTOR_RANGE 0.25 $blob]=>{$YIELD_DISTANCE_AS: score}`,
// `@modelId:{${String(chat.modelId._id)}}=>[KNN 10 @vector $blob AS score]`,
'RETURN',
'1',
'text',
'SORTBY',
'score',
'PARAMS',
'2',
'blob',
vectorToBuffer(promptVector),
'LIMIT',
'0',
'20',
'DIALECT',
'2'
]);
// 格式化响应值,获取 qa
const formatRedisPrompt = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
.map((i) => {
if (!redisData[i]) return '';
const text = (redisData[i][1] as string) || '';
if (!text) return '';
return text;
})
.filter((item) => item);
if (formatRedisPrompt.length === 0) {
throw new Error('对不起,我没有找到你的问题');
}
// textArr 筛选,最多 3000 tokens
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3400);
prompts.unshift({
obj: 'SYSTEM',
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
});
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
(item: ChatItemType) => ({
role: map[item.obj],
content: item.value
})
);
console.log(formatPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: temperature,
// max_tokens: modelConstantsData.maxToken,
messages: formatPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: true
},
{
timeout: 40000,
responseType: 'stream',
httpsAgent
}
);
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
// 创建响应流
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
let responseContent = '';
stream.pipe(res);
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
if (event.type !== 'event') return;
const data = event.data;
if (data === '[DONE]') return;
try {
const json = JSON.parse(data);
const content: string = json?.choices?.[0].delta.content || '';
if (!content || (responseContent === '' && content === '\n')) return;
responseContent += content;
// console.log('content:', content)
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
} catch (error) {
error;
}
};
const decoder = new TextDecoder();
try {
for await (const chunk of chatResponse.data as any) {
if (stream.destroyed) {
// 流被中断了,直接忽略后面的内容
break;
}
const parser = createParser(onParse);
parser.feed(decoder.decode(chunk));
}
} catch (error) {
console.log('pipe error', error);
}
// close stream
!stream.destroyed && stream.push(null);
stream.destroy();
const promptsContent = formatPrompts.map((item) => item.content).join('');
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
userId,
chatId,
text: promptsContent + responseContent
});
} catch (err: any) {
if (step === 1) {
// 直接结束流
console.log('error结束');
stream.destroy();
} else {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}
}

View File

@@ -1,24 +1,24 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { ChatItemType } from '@/types/chat'; import { ChatItemType } from '@/types/chat';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
/* 聊天内容存存储 */ /* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { windowId, prompts } = req.body as { const { chatId, prompts } = req.body as {
windowId: string; chatId: string;
prompts: ChatItemType[]; prompts: ChatItemType[];
}; };
if (!windowId || !prompts) { if (!chatId || !prompts) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
await connectToDatabase(); await connectToDatabase();
// 存入库 // 存入库
await ChatWindow.findByIdAndUpdate(windowId, { await Chat.findByIdAndUpdate(chatId, {
$push: { $push: {
content: { content: {
$each: prompts.map((item) => ({ $each: prompts.map((item) => ({
@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
})) }))
} }
}, },
updateTime: Date.now() updateTime: new Date()
}); });
jsonRes(res); jsonRes(res);

View File

@@ -0,0 +1,222 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser';
import { connectToDatabase } from '@/service/mongo';
import { getOpenAIApi, authChat } from '@/service/utils/chat';
import { httpsAgent, openaiChatFilter, systemPromptFilter } from '@/service/utils/tools';
import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from 'openai';
import { ChatItemType } from '@/types/chat';
import { jsonRes } from '@/service/response';
import type { ModelSchema } from '@/types/mongoSchema';
import { PassThrough } from 'stream';
import { modelList } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { connectRedis } from '@/service/redis';
import { VecModelDataPrefix } from '@/constants/redis';
import { vectorToBuffer } from '@/utils/tools';
import { openaiCreateEmbedding } from '@/service/utils/openai';
/* 发送提示词 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
let step = 0; // step=1时表示开始了流响应
const stream = new PassThrough();
stream.on('error', () => {
console.log('error: ', 'stream error');
stream.destroy();
});
res.on('close', () => {
stream.destroy();
});
res.on('error', () => {
console.log('error: ', 'request error');
stream.destroy();
});
try {
const { chatId, prompt } = req.body as {
prompt: ChatItemType;
chatId: string;
};
const { authorization } = req.headers;
if (!chatId || !prompt) {
throw new Error('缺少参数');
}
await connectToDatabase();
const redis = await connectRedis();
let startTime = Date.now();
const { chat, userApiKey, systemKey, userId } = await authChat(chatId, authorization);
const model: ModelSchema = chat.modelId;
const modelConstantsData = modelList.find((item) => item.model === model.service.modelName);
if (!modelConstantsData) {
throw new Error('模型加载异常');
}
// 读取对话内容
const prompts = [...chat.content, prompt];
// 获取提示词的向量
const { vector: promptVector, chatAPI } = await openaiCreateEmbedding({
isPay: !userApiKey,
apiKey: userApiKey || systemKey,
userId,
text: prompt.value
});
// 搜索系统提示词, 按相似度从 redis 中搜出相关的 q 和 text
const redisData: any[] = await redis.sendCommand([
'FT.SEARCH',
`idx:${VecModelDataPrefix}:hash`,
`@modelId:{${String(
chat.modelId._id
)}} @vector:[VECTOR_RANGE 0.25 $blob]=>{$YIELD_DISTANCE_AS: score}`,
// `@modelId:{${String(chat.modelId._id)}}=>[KNN 10 @vector $blob AS score]`,
'RETURN',
'1',
'text',
'SORTBY',
'score',
'PARAMS',
'2',
'blob',
vectorToBuffer(promptVector),
'LIMIT',
'0',
'20',
'DIALECT',
'2'
]);
// 格式化响应值,获取 qa
const formatRedisPrompt = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
.map((i) => {
if (!redisData[i]) return '';
const text = (redisData[i][1] as string) || '';
if (!text) return '';
return text;
})
.filter((item) => item);
if (formatRedisPrompt.length === 0) {
throw new Error('对不起,我没有找到你的问题');
}
// textArr 筛选,最多 3000 tokens
const systemPrompt = systemPromptFilter(formatRedisPrompt, 3400);
prompts.unshift({
obj: 'SYSTEM',
value: `${model.systemPrompt} 知识库内容是最新的,知识库内容为: "${systemPrompt}"`
});
// 控制在 tokens 数量,防止超出
const filterPrompts = openaiChatFilter(prompts, modelConstantsData.contextMaxToken);
// 格式化文本内容成 chatgpt 格式
const map = {
Human: ChatCompletionRequestMessageRoleEnum.User,
AI: ChatCompletionRequestMessageRoleEnum.Assistant,
SYSTEM: ChatCompletionRequestMessageRoleEnum.System
};
const formatPrompts: ChatCompletionRequestMessage[] = filterPrompts.map(
(item: ChatItemType) => ({
role: map[item.obj],
content: item.value
})
);
// console.log(formatPrompts);
// 计算温度
const temperature = modelConstantsData.maxTemperature * (model.temperature / 10);
// 发出请求
const chatResponse = await chatAPI.createChatCompletion(
{
model: model.service.chatModel,
temperature: temperature,
// max_tokens: modelConstantsData.maxToken,
messages: formatPrompts,
frequency_penalty: 0.5, // 越大,重复内容越少
presence_penalty: -0.5, // 越大,越容易出现新内容
stream: true
},
{
timeout: 40000,
responseType: 'stream',
httpsAgent
}
);
console.log('api response time:', `${(Date.now() - startTime) / 1000}s`);
// 创建响应流
res.setHeader('Content-Type', 'text/event-stream;charset-utf-8');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('X-Accel-Buffering', 'no');
res.setHeader('Cache-Control', 'no-cache, no-transform');
step = 1;
let responseContent = '';
stream.pipe(res);
const onParse = async (event: ParsedEvent | ReconnectInterval) => {
if (event.type !== 'event') return;
const data = event.data;
if (data === '[DONE]') return;
try {
const json = JSON.parse(data);
const content: string = json?.choices?.[0].delta.content || '';
if (!content || (responseContent === '' && content === '\n')) return;
responseContent += content;
// console.log('content:', content)
!stream.destroyed && stream.push(content.replace(/\n/g, '<br/>'));
} catch (error) {
error;
}
};
const decoder = new TextDecoder();
try {
for await (const chunk of chatResponse.data as any) {
if (stream.destroyed) {
// 流被中断了,直接忽略后面的内容
break;
}
const parser = createParser(onParse);
parser.feed(decoder.decode(chunk));
}
} catch (error) {
console.log('pipe error', error);
}
// close stream
!stream.destroyed && stream.push(null);
stream.destroy();
const promptsContent = formatPrompts.map((item) => item.content).join('');
// 只有使用平台的 key 才计费
pushChatBill({
isPay: !userApiKey,
modelName: model.service.modelName,
userId,
chatId,
text: promptsContent + responseContent
});
// jsonRes(res);
} catch (err: any) {
if (step === 1) {
// 直接结束流
console.log('error结束');
stream.destroy();
} else {
res.status(500);
jsonRes(res, {
code: 500,
error: err
});
}
}
}

View File

@@ -0,0 +1,47 @@
// 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, Data, DataItem } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataListItem } from '@/types/data';
import type { PagingData } from '@/types';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
await authToken(authorization);
const { dataId } = req.query as { dataId: string };
if (!dataId) {
throw new Error('缺少参数');
}
await connectToDatabase();
await Data.findByIdAndUpdate(dataId, {
isDeleted: true
});
// 改变 dataItem 状态为 0
await DataItem.updateMany(
{
dataId
},
{
status: 0
}
);
jsonRes<PagingData<DataListItem>>(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,48 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, DataItem } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let {
dataId,
pageNum = 1,
pageSize = 10
} = req.query as { dataId: string; pageNum: string; pageSize: string };
pageNum = +pageNum;
pageSize = +pageSize;
if (!dataId) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
await authToken(authorization);
const dataItems = await DataItem.find({
dataId
})
.sort({ _id: -1 }) // 按照创建时间倒序排列
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
jsonRes(res, {
data: {
pageNum,
pageSize,
data: dataItems,
total: await DataItem.countDocuments({
dataId
})
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,71 @@
// 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, Data, DataItem } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataListItem } from '@/types/data';
import mongoose from 'mongoose';
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();
const datalist = await Data.aggregate<DataListItem>([
{
$match: {
userId: new mongoose.Types.ObjectId(userId),
isDeleted: false
}
},
{
$sort: { createTime: -1 } // 按照创建时间倒序排列
},
{
$lookup: {
from: 'dataitems',
localField: '_id',
foreignField: 'dataId',
as: 'items'
}
},
{
$addFields: {
totalData: {
$size: '$items' // 统计dataItem的总数
},
trainingData: {
$size: {
$filter: {
input: '$items',
as: 'item',
cond: { $ne: ['$$item.status', 0] } // 统计 status 不为0的数量
}
}
}
}
},
{
$project: {
items: 0 // 不返回 items 字段
}
}
]);
jsonRes(res, {
data: datalist
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Data } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataType } from '@/types/data';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { name, type } = req.body as { name: string; type: DataType };
if (!name || !type) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
const userId = await authToken(authorization);
// 生成 data 集合
const data = await Data.create({
userId,
name,
type
});
jsonRes(res, {
data: data._id
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,37 @@
// 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, Data } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { DataListItem } from '@/types/data';
import type { PagingData } from '@/types';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
await authToken(authorization);
const { dataId, name } = req.query as { dataId: string; name: string };
if (!dataId || !name) {
throw new Error('缺少参数');
}
await connectToDatabase();
await Data.findByIdAndUpdate(dataId, {
name
});
jsonRes<PagingData<DataListItem>>(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,69 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, DataItem, Data } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateQA } from '@/service/events/generateQA';
import { generateAbstract } from '@/service/events/generateAbstract';
import { encode } from 'gpt-token-utils';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { text, dataId } = req.body as { text: string; dataId: string };
if (!text || !dataId) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
const userId = await authToken(authorization);
const DataRecord = await Data.findById(dataId);
if (!DataRecord) {
throw new Error('找不到数据集');
}
const replaceText = text.replace(/[\\n]+/g, ' ');
// 文本拆分成 chunk
let chunks = replaceText.match(/[^!?.。]+[!?.。]/g) || [];
const dataItems: any[] = [];
let splitText = '';
chunks.forEach((chunk) => {
splitText += chunk;
const tokens = encode(splitText).length;
if (tokens >= 780) {
dataItems.push({
userId,
dataId,
type: DataRecord.type,
text: splitText,
status: 1
});
splitText = '';
}
});
// 批量插入数据
await DataItem.insertMany(dataItems);
try {
generateQA();
generateAbstract();
} catch (error) {
error;
}
jsonRes(res, {
data: { chunks, replaceText }
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -3,48 +3,42 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools'; import { authToken } from '@/service/utils/tools';
import { ModelStatusEnum, OpenAiList } from '@/constants/model'; import { ModelStatusEnum, modelList, ChatModelNameEnum, ChatModelNameMap } from '@/constants/model';
import { Model } from '@/service/models/model'; import { Model } from '@/service/models/model';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { name, serviceModelName, serviceModelCompany = 'openai' } = req.body; const { name, serviceModelName } = req.body as {
name: string;
serviceModelName: `${ChatModelNameEnum}`;
};
const { authorization } = req.headers; const { authorization } = req.headers;
if (!authorization) { if (!authorization) {
throw new Error('无权操作'); throw new Error('无权操作');
} }
if (!name || !serviceModelName || !serviceModelCompany) { if (!name || !serviceModelName) {
throw new Error('缺少参数'); throw new Error('缺少参数');
} }
// 凭证校验 // 凭证校验
const userId = await authToken(authorization); const userId = await authToken(authorization);
const modelItem = OpenAiList.find((item) => item.model === serviceModelName); const modelItem = modelList.find((item) => item.model === serviceModelName);
if (!modelItem) { if (!modelItem) {
throw new Error('模型错误'); throw new Error('模型不存在');
} }
await connectToDatabase(); await connectToDatabase();
// 重名校验
const authRepeatName = await Model.findOne({
name,
userId
});
if (authRepeatName) {
throw new Error('模型名重复');
}
// 上限校验 // 上限校验
const authCount = await Model.countDocuments({ const authCount = await Model.countDocuments({
userId userId
}); });
if (authCount >= 5) { if (authCount >= 20) {
throw new Error('上限5个模型'); throw new Error('上限 20 个模型');
} }
// 创建模型 // 创建模型
@@ -53,10 +47,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
userId, userId,
status: ModelStatusEnum.running, status: ModelStatusEnum.running,
service: { service: {
company: serviceModelCompany, company: modelItem.serviceCompany,
trainId: modelItem.trainName, trainId: '',
chatModel: modelItem.model, chatModel: ChatModelNameMap[modelItem.model], // 聊天时用的模型
modelName: modelItem.model modelName: modelItem.model // 最底层的模型,不会变,用于计费等核心操作
} }
}); });

View File

@@ -0,0 +1,41 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { connectRedis } from '@/service/redis';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { dataId } = req.query as {
dataId: string;
};
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!dataId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
const redis = await connectRedis();
// 校验是否为该用户的数据
const dataItemUserId = await redis.hGet(dataId, 'userId');
if (dataItemUserId !== userId) {
throw new Error('无权操作');
}
// 删除
await redis.del(dataId);
jsonRes(res);
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,69 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { connectRedis } from '@/service/redis';
import { VecModelDataIdx } from '@/constants/redis';
import { BufferToVector } from '@/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { modelId } = req.query as {
modelId: string;
};
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!modelId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
const redis = await connectRedis();
// 从 redis 中获取数据
const searchRes = await redis.ft.search(
VecModelDataIdx,
`@modelId:{${modelId}} @userId:{${userId}}`,
{
RETURN: ['q', 'text', 'rawVector'],
LIMIT: {
from: 0,
size: 10000
}
}
);
const data = searchRes.documents
.filter((item) => {
if (!item?.value?.rawVector) return false;
try {
JSON.parse(item.value.rawVector as string);
return true;
} catch (error) {
return false;
}
})
.map((item: any) => ({
prompt: item.value.q,
completion: item.value.text,
vector: JSON.parse(item.value.rawVector)
}));
jsonRes(res, {
data: JSON.stringify(data)
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,74 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { connectRedis } from '@/service/redis';
import { VecModelDataIdx } from '@/constants/redis';
import { SearchOptions } from 'redis';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let {
modelId,
pageNum = 1,
pageSize = 10
} = req.query as {
modelId: string;
pageNum: string;
pageSize: string;
};
const { authorization } = req.headers;
pageNum = +pageNum;
pageSize = +pageSize;
if (!authorization) {
throw new Error('无权操作');
}
if (!modelId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
const redis = await connectRedis();
// 从 redis 中获取数据
const searchRes = await redis.ft.search(
VecModelDataIdx,
`@modelId:{${modelId}} @userId:{${userId}}`,
{
RETURN: ['q', 'text', 'status'],
LIMIT: {
from: (pageNum - 1) * pageSize,
size: pageSize
},
SORTBY: {
BY: 'modelId',
DIRECTION: 'DESC'
}
}
);
jsonRes(res, {
data: {
pageNum,
pageSize,
data: searchRes.documents.map((item) => ({
id: item.id,
...item.value
})),
total: searchRes.total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { modelId } = req.query as { modelId: string };
if (!modelId) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
const userId = await authToken(authorization);
// 找到长度大于0的数据
const data = await SplitData.find({
userId,
modelId,
textList: { $exists: true, $not: { $size: 0 } }
});
jsonRes(res, {
data
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,72 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { ModelDataSchema } from '@/types/mongoSchema';
import { generateVector } from '@/service/events/generateVector';
import { connectRedis } from '@/service/redis';
import { VecModelDataPrefix, ModelDataStatusEnum } from '@/constants/redis';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId, data } = req.body as {
modelId: string;
data: { text: ModelDataSchema['text']; q: ModelDataSchema['q'] }[];
};
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!modelId || !Array.isArray(data)) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
const redis = await connectRedis();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
const insertRes = await Promise.allSettled(
data.map((item) => {
return redis.sendCommand([
'HMSET',
`${VecModelDataPrefix}:${item.q.id}`,
'userId',
userId,
'modelId',
modelId,
'q',
item.q.text,
'text',
item.text,
'status',
ModelDataStatusEnum.waiting
]);
})
);
generateVector(true);
jsonRes(res, {
data: insertRes.filter((item) => item.status === 'rejected').length
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,80 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateVector } from '@/service/events/generateVector';
import { vectorToBuffer, formatVector } from '@/utils/tools';
import { connectRedis } from '@/service/redis';
import { VecModelDataPrefix, ModelDataStatusEnum } from '@/constants/redis';
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { modelId, data } = req.body as {
modelId: string;
data: { prompt: string; completion: string; vector?: number[] }[];
};
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!modelId || !Array.isArray(data)) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
await connectToDatabase();
const redis = await connectRedis();
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
// 插入 redis
const insertRedisRes = await Promise.allSettled(
data.map((item) => {
const vector = item.vector;
return redis.sendCommand([
'HMSET',
`${VecModelDataPrefix}:${nanoid()}`,
'userId',
userId,
'modelId',
String(modelId),
...(vector
? ['vector', vectorToBuffer(formatVector(vector)), 'rawVector', JSON.stringify(vector)]
: []),
'q',
item.prompt,
'text',
item.completion,
'status',
vector ? ModelDataStatusEnum.ready : ModelDataStatusEnum.waiting
]);
})
);
generateVector(true);
jsonRes(res, {
data: insertRedisRes.filter((item) => item.status === 'rejected').length
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,52 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { connectRedis } from '@/service/redis';
import { ModelDataStatusEnum } from '@/constants/redis';
import { generateVector } from '@/service/events/generateVector';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { dataId, text, q } = req.body as { dataId: string; text: string; q?: string };
const { authorization } = req.headers;
if (!authorization) {
throw new Error('无权操作');
}
if (!dataId) {
throw new Error('缺少参数');
}
// 凭证校验
const userId = await authToken(authorization);
const redis = await connectRedis();
// 校验是否为该用户的数据
const dataItemUserId = await redis.hGet(dataId, 'userId');
if (dataItemUserId !== userId) {
throw new Error('无权操作');
}
// 更新
await redis.sendCommand([
'HMSET',
dataId,
...(q ? ['q', q, 'status', ModelDataStatusEnum.waiting] : []),
'text',
text
]);
if (q) {
generateVector();
}
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,78 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, SplitData, Model } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { generateQA } from '@/service/events/generateQA';
import { encode } from 'gpt-token-utils';
/* 拆分数据成QA */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { text, modelId, prompt } = req.body as { text: string; modelId: string; prompt: string };
if (!text || !modelId || !prompt) {
throw new Error('参数错误');
}
await connectToDatabase();
const { authorization } = req.headers;
const userId = await authToken(authorization);
// 验证是否是该用户的 model
const model = await Model.findOne({
_id: modelId,
userId
});
if (!model) {
throw new Error('无权操作该模型');
}
const replaceText = text.replace(/(\\n|\n)+/g, ' ');
// 文本拆分成 chunk
const chunks = replaceText.match(/[^!?.。]+[!?.。]/g) || [];
const textList: string[] = [];
let splitText = '';
/* 取 3k ~ 4K tokens 内容 */
chunks.forEach((chunk) => {
const tokens = encode(splitText + chunk).length;
if (tokens >= 4000) {
// 超过 4000不要这块内容
textList.push(splitText);
splitText = chunk;
} else if (tokens >= 3000) {
// 超过 3000取内容
textList.push(splitText + chunk);
splitText = '';
} else {
//没超过 3000继续添加
splitText += chunk;
}
});
if (splitText) {
textList.push(splitText);
}
// 批量插入数据
await SplitData.create({
userId,
modelId,
rawText: text,
textList,
prompt
});
generateQA();
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,11 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { Chat, Model, Training, connectToDatabase } from '@/service/mongo'; import { Chat, Model, Training, connectToDatabase } from '@/service/mongo';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools'; import { authToken } from '@/service/utils/tools';
import { getUserApiOpenai } from '@/service/utils/openai';
import { TrainingStatusEnum } from '@/constants/model'; import { TrainingStatusEnum } from '@/constants/model';
import { getOpenAIApi } from '@/service/utils/chat';
import { TrainingItemType } from '@/types/training'; import { TrainingItemType } from '@/types/training';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent } from '@/service/utils/tools';
import { connectRedis } from '@/service/redis';
import { VecModelDataIdx } from '@/constants/redis';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -24,14 +26,33 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 凭证校验 // 凭证校验
const userId = await authToken(authorization); const userId = await authToken(authorization);
await connectToDatabase(); // 验证是否是该用户的 model
const model = await Model.findOne({
// 删除模型
await Model.deleteOne({
_id: modelId, _id: modelId,
userId userId
}); });
if (!model) {
throw new Error('无权操作该模型');
}
await connectToDatabase();
const redis = await connectRedis();
// 获取 redis 中模型关联的所有数据
const searchRes = await redis.ft.search(
VecModelDataIdx,
`@modelId:{${modelId}} @userId:{${userId}}`,
{
LIMIT: {
from: 0,
size: 10000
}
}
);
// 删除 redis 内容
await Promise.all(searchRes.documents.map((item) => redis.del(item.id)));
// 删除对应的聊天 // 删除对应的聊天
await Chat.deleteMany({ await Chat.deleteMany({
modelId modelId
@@ -45,7 +66,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 如果正在训练需要删除openai上的相关信息 // 如果正在训练需要删除openai上的相关信息
if (training) { if (training) {
const openai = getOpenAIApi(await getUserOpenaiKey(userId)); const { openai } = await getUserApiOpenai(userId);
// 获取训练记录 // 获取训练记录
const tuneRecord = await openai.retrieveFineTune(training.tuneId, { httpsAgent }); const tuneRecord = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
@@ -60,6 +81,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
modelId modelId
}); });
// 删除模型
await Model.deleteOne({
_id: modelId,
userId
});
jsonRes(res); jsonRes(res);
} catch (err) { } catch (err) {
jsonRes(res, { jsonRes(res, {

View File

@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
import { authToken } from '@/service/utils/tools'; import { authToken } from '@/service/utils/tools';
import { Model } from '@/service/models/model'; import { Model } from '@/service/models/model';
import { ModelType } from '@/types/model'; import type { ModelSchema } from '@/types/mongoSchema';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
@@ -26,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase(); await connectToDatabase();
// 根据 userId 获取模型信息 // 根据 userId 获取模型信息
const model: ModelType | null = await Model.findOne({ const model = await Model.findOne<ModelSchema>({
userId, userId,
_id: modelId _id: modelId
}); });

View File

@@ -21,6 +21,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 根据 userId 获取模型信息 // 根据 userId 获取模型信息
const models = await Model.find({ const models = await Model.find({
userId userId
}).sort({
_id: -1
}); });
jsonRes(res, { jsonRes(res, {

View File

@@ -1,15 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo'; import { connectToDatabase, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat'; import { authToken } from '@/service/utils/tools';
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 { httpsAgent } from '@/service/utils/tools';
// 关闭next默认的bodyParser处理方式 // 关闭next默认的bodyParser处理方式
export const config = { export const config = {
@@ -18,7 +10,7 @@ export const config = {
} }
}; };
/* 上传文件,开始微调 */ /* 获取模型训练记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try { try {
const { authorization } = req.headers; const { authorization } = req.headers;
@@ -30,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (!modelId) { if (!modelId) {
throw new Error('参数错误'); throw new Error('参数错误');
} }
const userId = await authToken(authorization); await authToken(authorization);
await connectToDatabase(); await connectToDatabase();

View File

@@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo'; import { connectToDatabase, Model, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat'; import { authToken } from '@/service/utils/tools';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools'; import { getUserApiOpenai } from '@/service/utils/openai';
import type { ModelType } from '@/types/model'; import type { ModelSchema } from '@/types/mongoSchema';
import { TrainingItemType } from '@/types/training'; import { TrainingItemType } from '@/types/training';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model'; import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { OpenAiTuneStatusEnum } from '@/service/constants/training'; import { OpenAiTuneStatusEnum } from '@/service/constants/training';
@@ -26,7 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase(); await connectToDatabase();
// 获取模型 // 获取模型
const model: ModelType | null = await Model.findById(modelId); const model = await Model.findById<ModelSchema>(modelId);
if (!model || model.status !== 'training') { if (!model || model.status !== 'training') {
throw new Error('模型不在训练中'); throw new Error('模型不在训练中');
@@ -43,16 +43,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
// 用户的 openai 实例 // 用户的 openai 实例
const openai = getOpenAIApi(await getUserOpenaiKey(userId)); const { openai } = await getUserApiOpenai(userId);
// 获取 openai 的训练情况 // 获取 openai 的训练情况
const { data } = await openai.retrieveFineTune(training.tuneId, { httpsAgent }); const { data } = await openai.retrieveFineTune(training.tuneId, { httpsAgent });
// console.log(data);
if (data.status === OpenAiTuneStatusEnum.succeeded) { if (data.status === OpenAiTuneStatusEnum.succeeded) {
// 删除训练文件 // 删除训练文件
openai.deleteFile(data.training_files[0].id, { httpsAgent }); openai.deleteFile(data.training_files[0].id, { httpsAgent });
// 更新模型 // 更新模型状态和模型内容
await Model.findByIdAndUpdate(modelId, { await Model.findByIdAndUpdate(modelId, {
status: ModelStatusEnum.running, status: ModelStatusEnum.running,
updateTime: new Date(), updateTime: new Date(),
@@ -72,6 +72,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
} }
/* 取消微调 */
if (data.status === OpenAiTuneStatusEnum.cancelled) { if (data.status === OpenAiTuneStatusEnum.cancelled) {
// 删除训练文件 // 删除训练文件
openai.deleteFile(data.training_files[0].id, { httpsAgent }); openai.deleteFile(data.training_files[0].id, { httpsAgent });
@@ -87,11 +88,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
return jsonRes(res, { return jsonRes(res, {
data: '模型微调取消' data: '模型微调取消'
}); });
} }
throw new Error('模型还在训练中'); jsonRes(res, {
data: '模型还在训练中'
});
} catch (err: any) { } catch (err: any) {
jsonRes(res, { jsonRes(res, {
code: 500, code: 500,

View File

@@ -2,12 +2,12 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, Model, Training } from '@/service/mongo'; import { connectToDatabase, Model, Training } from '@/service/mongo';
import { getOpenAIApi } from '@/service/utils/chat';
import formidable from 'formidable'; import formidable from 'formidable';
import { authToken, getUserOpenaiKey } from '@/service/utils/tools'; import { authToken } from '@/service/utils/tools';
import { getUserApiOpenai } from '@/service/utils/openai';
import { join } from 'path'; import { join } from 'path';
import fs from 'fs'; import fs from 'fs';
import type { ModelType } from '@/types/model'; import type { ModelSchema } from '@/types/mongoSchema';
import type { OpenAIApi } from 'openai'; import type { OpenAIApi } from 'openai';
import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model'; import { ModelStatusEnum, TrainingStatusEnum } from '@/constants/model';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent } from '@/service/utils/tools';
@@ -30,6 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('无权操作'); throw new Error('无权操作');
} }
const { modelId } = req.query; const { modelId } = req.query;
if (!modelId) { if (!modelId) {
throw new Error('参数错误'); throw new Error('参数错误');
} }
@@ -38,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase(); await connectToDatabase();
// 获取模型的状态 // 获取模型的状态
const model: ModelType | null = await Model.findById(modelId); const model = await Model.findById<ModelSchema>(modelId);
if (!model || model.status !== 'running') { if (!model || model.status !== 'running') {
throw new Error('模型正忙'); throw new Error('模型正忙');
@@ -48,7 +49,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const trainingType = model.service.trainId; // 目前都默认是 openai text-davinci-03 const trainingType = model.service.trainId; // 目前都默认是 openai text-davinci-03
// 获取用户的 API Key 实例化后的对象 // 获取用户的 API Key 实例化后的对象
openai = getOpenAIApi(await getUserOpenaiKey(userId)); const user = await getUserApiOpenai(userId);
openai = user.openai;
// 接收文件并保存 // 接收文件并保存
const form = formidable({ const form = formidable({
@@ -67,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}); });
const file = files.file; const file = files.file;
// 上传文件 // 上传文件到 openai
// @ts-ignore // @ts-ignore
const uploadRes = await openai.createFile( const uploadRes = await openai.createFile(
// @ts-ignore // @ts-ignore
@@ -82,7 +84,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
{ {
training_file: uploadFileId, training_file: uploadFileId,
model: trainingType, model: trainingType,
suffix: model.name suffix: model.name,
n_epochs: 4
}, },
{ httpsAgent } { httpsAgent }
); );

View File

@@ -8,7 +8,8 @@ import type { ModelUpdateParams } from '@/types/model';
/* 获取我的模型 */ /* 获取我的模型 */
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try { try {
const { name, service, security, systemPrompt } = req.body as ModelUpdateParams; const { name, service, security, systemPrompt, intro, temperature } =
req.body as ModelUpdateParams;
const { modelId } = req.query as { modelId: string }; const { modelId } = req.query as { modelId: string };
const { authorization } = req.headers; const { authorization } = req.headers;
@@ -33,8 +34,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
}, },
{ {
name, name,
service,
systemPrompt, systemPrompt,
intro,
temperature,
// service,
security security
} }
); );

View File

@@ -5,6 +5,9 @@ import { AuthCode } from '@/service/models/authCode';
import { connectToDatabase } from '@/service/mongo'; import { connectToDatabase } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (process.env.NODE_ENV !== 'development') {
throw new Error('不是开发环境');
}
try { try {
await connectToDatabase(); await connectToDatabase();

View File

@@ -1,13 +1,16 @@
import type { NextApiRequest, NextApiResponse } from 'next'; import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response'; import { jsonRes } from '@/service/response';
import { connectToDatabase, ChatWindow } from '@/service/mongo'; import { connectToDatabase, Chat } from '@/service/mongo';
/* 定时删除那些不活跃的内容 */ /* 定时删除那些不活跃的内容 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (process.env.NODE_ENV !== 'development') {
throw new Error('不是开发环境');
}
try { try {
await connectToDatabase(); await connectToDatabase();
const response = await ChatWindow.deleteMany( const response = await Chat.deleteMany(
{ $expr: { $lt: [{ $size: '$content' }, 5] } }, { $expr: { $lt: [{ $size: '$content' }, 5] } },
// 使用 $pull 操作符删除数组中的元素 // 使用 $pull 操作符删除数组中的元素
{ $pull: { content: { $exists: true } } } { $pull: { content: { $exists: true } } }

View File

@@ -0,0 +1,35 @@
// 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, Bill } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { BillSchema } from '@/types/mongoSchema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (process.env.NODE_ENV !== 'development') {
throw new Error('不是开发环境');
}
await connectToDatabase();
const bills = await Bill.find({
tokenLen: { $exists: false }
});
await Promise.all(
bills.map((bill) =>
Bill.findByIdAndUpdate(bill._id, {
tokenLen: bill.textLen
})
)
);
jsonRes(res, {
data: {}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,37 @@
// 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, DataItem, Data } from '@/service/mongo';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
if (process.env.NODE_ENV !== 'development') {
throw new Error('不是开发环境');
}
await connectToDatabase();
// await DataItem.updateMany(
// {},
// {
// type: 'QA'
// // times: 2
// }
// );
await Data.updateMany(
{},
{
type: 'QA'
}
);
jsonRes(res, {
data: {}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -4,13 +4,16 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, Training, Model } from '@/service/mongo'; import { connectToDatabase, Training, Model } from '@/service/mongo';
import type { TrainingItemType } from '@/types/training'; import type { TrainingItemType } from '@/types/training';
import { TrainingStatusEnum, ModelStatusEnum } from '@/constants/model'; import { TrainingStatusEnum, ModelStatusEnum } from '@/constants/model';
import { getOpenAIApi } from '@/service/utils/chat'; import { getUserApiOpenai } from '@/service/utils/openai';
import { getUserOpenaiKey } from '@/service/utils/tools';
import { OpenAiTuneStatusEnum } from '@/service/constants/training'; import { OpenAiTuneStatusEnum } from '@/service/constants/training';
import { sendTrainSucceed } from '@/service/utils/sendEmail'; import { sendTrainSucceed } from '@/service/utils/sendEmail';
import { httpsAgent } from '@/service/utils/tools'; import { httpsAgent } from '@/service/utils/tools';
import { ModelPopulate } from '@/types/mongoSchema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (process.env.NODE_ENV !== 'development') {
throw new Error('不是开发环境');
}
try { try {
await connectToDatabase(); await connectToDatabase();
@@ -19,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
status: TrainingStatusEnum.pending status: TrainingStatusEnum.pending
}); });
const openai = getOpenAIApi(await getUserOpenaiKey('63f9a14228d2a688d8dc9e1b')); const { openai } = await getUserApiOpenai('63f9a14228d2a688d8dc9e1b');
const response = await Promise.all( const response = await Promise.all(
trainingRecords.map(async (item) => { trainingRecords.map(async (item) => {
@@ -28,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 删除训练文件 // 删除训练文件
openai.deleteFile(data.training_files[0].id, { httpsAgent }); openai.deleteFile(data.training_files[0].id, { httpsAgent });
const model = await Model.findById(item.modelId).populate({ const model = await Model.findById<ModelPopulate>(item.modelId).populate({
path: 'userId', path: 'userId',
options: { options: {
strictPopulate: false strictPopulate: false

View File

@@ -0,0 +1,82 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import axios from 'axios';
import { connectToDatabase, User, Pay } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PaySchema } from '@/types/mongoSchema';
import dayjs from 'dayjs';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
let { payId } = req.query as { payId: string };
const userId = await authToken(authorization);
await connectToDatabase();
// 查找订单记录校验
const payOrder = await Pay.findById<PaySchema>(payId);
if (!payOrder) {
throw new Error('订单不存在');
}
if (payOrder.status !== 'NOTPAY') {
throw new Error('订单已结算');
}
const { data } = await axios.get(
`https://sif268.laf.dev/wechat-order-query?order_number=${payOrder.orderId}&api_key=${process.env.WXPAYCODE}`
);
// 校验下是否超过一天
const orderTime = dayjs(payOrder.createTime);
const diffInHours = dayjs().diff(orderTime, 'hours');
if (data.trade_state === 'SUCCESS') {
// 订单已支付
try {
// 更新订单状态
const updateRes = await Pay.updateOne(
{
_id: payId,
status: 'NOTPAY'
},
{
status: 'SUCCESS'
}
);
if (updateRes.modifiedCount === 1) {
// 给用户账号充钱
await User.findByIdAndUpdate(userId, {
$inc: { balance: payOrder.price }
});
jsonRes(res, {
data: '支付成功'
});
}
} catch (error) {
await Pay.findByIdAndUpdate(payId, {
status: 'NOTPAY'
});
console.log(error);
}
} else if (data.trade_state === 'CLOSED' || diffInHours > 24) {
// 订单已关闭
await Pay.findByIdAndUpdate(payId, {
status: 'CLOSED'
});
jsonRes(res, {
data: '订单已过期'
});
} else {
throw new Error(data.trade_state_desc);
}
} catch (err) {
// console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,51 @@
// 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, Bill } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import type { BillSchema } from '@/types/mongoSchema';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
pageNum = +pageNum;
pageSize = +pageSize;
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
await connectToDatabase();
// 根据 id 获取用户账单
const bills = await Bill.find<BillSchema>({
userId
})
.sort({ _id: -1 }) // 按照创建时间倒序排列
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
// 获取total
const total = await Bill.countDocuments({
userId
});
jsonRes(res, {
data: {
pageNum,
pageSize,
data: bills,
total
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,56 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import axios from 'axios';
import { authToken } from '@/service/utils/tools';
import { customAlphabet } from 'nanoid';
import { connectToDatabase, Pay } from '@/service/mongo';
import { PRICE_SCALE } from '@/constants/common';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 20);
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
let { amount = 0 } = req.query as { amount: string };
amount = +amount;
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
const id = nanoid();
await connectToDatabase();
const response = await axios({
url: 'https://sif268.laf.dev/wechat-pay',
method: 'POST',
data: {
trade_order_number: id,
amount: amount * 100,
api_key: process.env.WXPAYCODE
}
});
// 充值记录 + 1
const payOrder = await Pay.create({
userId,
price: amount * PRICE_SCALE,
orderId: id
});
jsonRes(res, {
data: {
payId: payOrder._id,
codeUrl: response.data?.code_url
}
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { authToken } from '@/service/utils/tools';
import { connectToDatabase, Pay } from '@/service/mongo';
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();
const records = await Pay.find({
userId
}).sort({ createTime: -1 });
jsonRes(res, {
data: records
});
} catch (err) {
console.log(err);
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -46,6 +46,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
// 根据 id 获取用户信息 // 根据 id 获取用户信息
const user = await User.findById(response._id); const user = await User.findById(response._id);
if (!user) {
throw new Error('获取用户信息异常');
}
jsonRes(res, { jsonRes(res, {
data: { data: {
token: generateToken(user._id), token: generateToken(user._id),

View File

@@ -44,6 +44,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
email email
}); });
if (!user) {
throw new Error('获取用户信息异常');
}
jsonRes(res, { jsonRes(res, {
data: { data: {
token: generateToken(user._id), token: generateToken(user._id),

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { Card, Box, Mark } from '@chakra-ui/react';
import { versionIntro, chatProblem } from '@/constants/common';
import Markdown from '@/components/Markdown';
const Empty = ({ intro }: { intro: string }) => {
const Header = ({ children }: { children: string }) => (
<Box fontSize={'lg'} fontWeight={'bold'} textAlign={'center'} pb={2}>
{children}
</Box>
);
return (
<Box
minH={'100%'}
w={'85%'}
maxW={'600px'}
m={'auto'}
py={'5vh'}
alignItems={'center'}
justifyContent={'center'}
>
{!!intro && (
<Card p={4} mb={10}>
<Header></Header>
<Box whiteSpace={'pre-line'}>{intro}</Box>
</Card>
)}
{/* version intro */}
<Card p={4} mb={10}>
<Markdown source={versionIntro} />
</Card>
<Card p={4}>
<Markdown source={chatProblem} />
</Card>
</Box>
);
};
export default Empty;

View File

@@ -0,0 +1,313 @@
import React, { useState, useEffect } from 'react';
import { AddIcon, ChatIcon, DeleteIcon, MoonIcon, SunIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Flex,
Divider,
IconButton,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure,
useColorMode,
useColorModeValue
} from '@chakra-ui/react';
import { useUserStore } from '@/store/user';
import { useChatStore } from '@/store/chat';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { getToken } from '@/utils/user';
import MyIcon from '@/components/Icon';
import { useCopyData } from '@/utils/tools';
import Markdown from '@/components/Markdown';
import { shareHint } from '@/constants/common';
import { getChatSiteId } from '@/api/chat';
import WxConcat from '@/components/WxConcat';
const SlideBar = ({
name,
chatId,
modelId,
resetChat,
onClose
}: {
name?: string;
chatId: string;
modelId: string;
resetChat: () => void;
onClose: () => void;
}) => {
const router = useRouter();
const { colorMode, toggleColorMode } = useColorMode();
const { copyData } = useCopyData();
const { myModels, getMyModels } = useUserStore();
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
const [hasReady, setHasReady] = useState(false);
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
const { isSuccess } = useQuery(['init'], getMyModels, {
cacheTime: 5 * 60 * 1000
});
useEffect(() => {
setHasReady(true);
}, []);
const RenderHistory = () => (
<>
{chatHistory.map((item) => (
<Flex
key={item.chatId}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.chatId === chatId
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={() => {
if (item.chatId === chatId) return;
router.replace(`/chat?chatId=${item.chatId}`);
onClose();
}}
>
<ChatIcon mr={2} />
<Box flex={'1 0 0'} w={0} className="textEllipsis">
{item.title}
</Box>
<Box>
<IconButton
icon={<DeleteIcon />}
variant={'unstyled'}
aria-label={'edit'}
size={'xs'}
onClick={(e) => {
removeChatHistoryByWindowId(item.chatId);
if (item.chatId === chatId) {
resetChat();
}
e.stopPropagation();
}}
/>
</Box>
</Flex>
))}
</>
);
const RenderButton = ({
onClick,
children
}: {
onClick: () => void;
children: JSX.Element | string;
}) => (
<Box px={3} mb={3}>
<Flex
alignItems={'center'}
p={2}
cursor={'pointer'}
borderRadius={'md'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={onClick}
>
{children}
</Flex>
</Box>
);
return (
<Flex
flexDirection={'column'}
w={'100%'}
h={'100%'}
py={3}
backgroundColor={useColorModeValue('blackAlpha.800', 'blackAlpha.500')}
color={'white'}
>
{/* 新对话 */}
{getToken() && (
<Button
w={'90%'}
variant={'white'}
h={'40px'}
mb={4}
mx={'auto'}
leftIcon={<AddIcon />}
onClick={resetChat}
>
</Button>
)}
{/* 我的模型 & 历史记录 折叠框*/}
<Box flex={'1 0 0'} px={3} h={0} overflowY={'auto'}>
<Accordion defaultIndex={[0]} allowMultiple>
{isSuccess && (
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4} px={0}>
{myModels.map((item) => (
<Flex
key={item._id}
alignItems={'center'}
p={3}
borderRadius={'md'}
mb={2}
cursor={'pointer'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.1)'
}}
fontSize={'xs'}
border={'1px solid transparent'}
{...(item.name === name
? {
borderColor: 'rgba(255,255,255,0.5)',
backgroundColor: 'rgba(255,255,255,0.1)'
}
: {})}
onClick={async () => {
if (item.name === name) return;
router.replace(`/chat?chatId=${await getChatSiteId(item._id)}`);
onClose();
}}
>
<MyIcon name="model" mr={2} fill={'white'} w={'16px'} h={'16px'} />
<Box className={'textEllipsis'} flex={'1 0 0'} w={0}>
{item.name}
</Box>
</Flex>
))}
</AccordionPanel>
</AccordionItem>
)}
<AccordionItem borderTop={0} borderBottom={0}>
<AccordionButton borderRadius={'md'} pl={1}>
<Box as="span" flex="1" textAlign="left">
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={0} px={0}>
{hasReady && <RenderHistory />}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Box>
<Divider my={4} colorScheme={useColorModeValue('gray', 'white')} />
<RenderButton onClick={() => router.push('/')}>
<>
<MyIcon name="home" fill={'white'} w={'18px'} h={'18px'} mr={4} />
</>
</RenderButton>
{/* <RenderButton onClick={onOpenShare}>
<>
<MyIcon name="share" fill={'white'} w={'16px'} h={'16px'} mr={4} />
分享
</>
</RenderButton> */}
<RenderButton onClick={() => router.push('/number/setting')}>
<>
<MyIcon name="pay" fill={'white'} w={'16px'} h={'16px'} mr={4} />
</>
</RenderButton>
<Flex alignItems={'center'} mr={4}>
<Box flex={1}>
<RenderButton onClick={onOpenWx}></RenderButton>
</Box>
<IconButton
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
aria-label={''}
variant={'outline'}
w={'16px'}
colorScheme={'white'}
_hover={{
backgroundColor: 'rgba(255,255,255,0.2)'
}}
onClick={toggleColorMode}
/>
</Flex>
{/* 分享提示modal */}
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
<ModalOverlay />
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody>
<Markdown source={shareHint} />
</ModalBody>
<ModalFooter>
<Button colorScheme="gray" variant={'outline'} mr={3} onClick={onCloseShare}>
</Button>
{getToken() && (
<Button
variant="outline"
mr={3}
onClick={async () => {
copyData(
`${location.origin}/chat?chatId=${await getChatSiteId(modelId, true)}`,
'已复制分享链接'
);
onCloseShare();
onClose();
}}
>
</Button>
)}
<Button
onClick={() => {
copyData(`${location.origin}/chat?chatId=${chatId}`, '已复制分享链接');
onCloseShare();
onClose();
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* wx 联系 */}
{isOpenWx && <WxConcat onClose={onCloseWx} />}
</Flex>
);
};
export default SlideBar;

View File

@@ -1,37 +1,82 @@
import React, { useCallback, useState, useRef, useMemo } from 'react'; import React, { useCallback, useState, useRef, useMemo, useEffect } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import Image from 'next/image'; import Image from 'next/image';
import { getInitChatSiteInfo, postGPT3SendPrompt, delLastMessage, postSaveChat } from '@/api/chat'; import {
import { ChatSiteItemType, ChatSiteType } from '@/types/chat'; getInitChatSiteInfo,
import { Textarea, Box, Flex, Button } from '@chakra-ui/react'; getChatSiteId,
postGPT3SendPrompt,
delChatRecordByIndex,
postSaveChat
} from '@/api/chat';
import type { InitChatResponse } from '@/api/response/chat';
import { ChatSiteItemType } from '@/types/chat';
import {
Textarea,
Box,
Flex,
useDisclosure,
Drawer,
DrawerOverlay,
DrawerContent,
useColorModeValue,
Menu,
MenuButton,
MenuList,
MenuItem
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import Icon from '@/components/Icon';
import { useScreen } from '@/hooks/useScreen'; import { useScreen } from '@/hooks/useScreen';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { OpenAiModelEnum } from '@/constants/model'; import { ChatModelNameEnum } from '@/constants/model';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { useChatStore } from '@/store/chat';
import { useCopyData } from '@/utils/tools';
import { streamFetch } from '@/api/fetch'; import { streamFetch } from '@/api/fetch';
import SlideBar from './components/SlideBar';
import Empty from './components/Empty';
import Icon from '@/components/Icon';
import { encode } from 'gpt-token-utils';
import { modelList } from '@/constants/model';
const Markdown = dynamic(() => import('@/components/Markdown')); const Markdown = dynamic(() => import('@/components/Markdown'));
const textareaMinH = '22px'; const textareaMinH = '22px';
const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => { interface ChatType extends InitChatResponse {
history: ChatSiteItemType[];
}
const Chat = ({ chatId }: { chatId: string }) => {
const { toast } = useToast(); const { toast } = useToast();
const router = useRouter(); const router = useRouter();
const { isPc, media } = useScreen();
const ChatBox = useRef<HTMLDivElement>(null); const ChatBox = useRef<HTMLDivElement>(null);
const TextareaDom = useRef<HTMLTextAreaElement>(null); const TextareaDom = useRef<HTMLTextAreaElement>(null);
// 中断请求
const [chatSiteData, setChatSiteData] = useState<ChatSiteType>(); // 聊天框整体数据 const controller = useRef(new AbortController());
const [chatList, setChatList] = useState<ChatSiteItemType[]>([]); // 对话内容 const [chatData, setChatData] = useState<ChatType>({
chatId: '',
modelId: '',
name: '',
avatar: '',
intro: '',
chatModel: '',
modelName: '',
history: []
}); // 聊天框整体数据
const [inputVal, setInputVal] = useState(''); // 输入的内容 const [inputVal, setInputVal] = useState(''); // 输入的内容
const isChatting = useMemo(() => chatList[chatList.length - 1]?.status === 'loading', [chatList]); const isChatting = useMemo(
const lastWordHuman = useMemo(() => chatList[chatList.length - 1]?.obj === 'Human', [chatList]); () => chatData.history[chatData.history.length - 1]?.status === 'loading',
[chatData.history]
);
const { copyData } = useCopyData();
const { isPc, media } = useScreen();
const { setLoading } = useGlobalStore(); const { setLoading } = useGlobalStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const { pushChatHistory } = useChatStore();
// 滚动到底部 // 滚动到底部
const scrollToBottom = useCallback(() => { const scrollToBottom = useCallback(() => {
setTimeout(() => { setTimeout(() => {
@@ -43,26 +88,262 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
}, 100); }, 100);
}, []); }, []);
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
setInputVal(val);
setTimeout(() => {
/* 回到最小高度 */
if (TextareaDom.current) {
TextareaDom.current.style.height =
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
}
}, 100);
}, []);
// 重载对话
const resetChat = useCallback(async () => {
if (!chatData) return;
try {
router.replace(`/chat?chatId=${await getChatSiteId(chatData.modelId)}`);
} catch (error: any) {
toast({
title: error?.message || '生成新对话失败',
status: 'warning'
});
}
onCloseSlider();
}, [chatData, onCloseSlider, router, toast]);
// gpt 对话
const gptChatPrompt = useCallback(
async (prompts: ChatSiteItemType) => {
const urlMap: Record<string, string> = {
[ChatModelNameEnum.GPT35]: '/api/chat/chatGpt',
[ChatModelNameEnum.VECTOR_GPT]: '/api/chat/vectorGpt',
// [ChatModelNameEnum.VECTOR_GPT]: '/api/chat/lafGpt',
[ChatModelNameEnum.GPT3]: '/api/chat/gpt3'
};
if (!urlMap[chatData.modelName]) return Promise.reject('找不到模型');
const prompt = {
obj: prompts.obj,
value: prompts.value
};
// 流请求,获取数据
const res = await streamFetch({
url: urlMap[chatData.modelName],
data: {
prompt,
chatId
},
onMessage: (text: string) => {
setChatData((state) => ({
...state,
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
...item,
value: item.value + text
};
})
}));
},
abortSignal: controller.current
});
// 保存对话信息
try {
await postSaveChat({
chatId,
prompts: [
prompt,
{
obj: 'AI',
value: res as string
}
]
});
} catch (err) {
toast({
title: '对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
status: 'warning',
duration: 3000,
isClosable: true
});
}
// 设置完成状态
setChatData((state) => ({
...state,
history: state.history.map((item, index) => {
if (index !== state.history.length - 1) return item;
return {
...item,
status: 'finish'
};
})
}));
},
[chatData.modelName, chatId, toast]
);
/**
* 发送一个内容
*/
const sendPrompt = useCallback(async () => {
if (isChatting) {
toast({
title: '正在聊天中...请等待结束',
status: 'warning'
});
return;
}
const storeInput = inputVal;
// 去除空行
const val = inputVal.trim().replace(/\n\s*/g, '\n');
if (!chatData?.modelId || !val) {
toast({
title: '内容为空',
status: 'warning'
});
return;
}
// 长度校验
const tokens = encode(val).length;
const model = modelList.find((item) => item.model === chatData.modelName);
if (model && tokens >= model.maxToken) {
toast({
title: '单次输入超出 4000 tokens',
status: 'warning'
});
return;
}
const newChatList: ChatSiteItemType[] = [
...chatData.history,
{
obj: 'Human',
value: val,
status: 'finish'
},
{
obj: 'AI',
value: '',
status: 'loading'
}
];
// 插入内容
setChatData((state) => ({
...state,
history: newChatList
}));
// 清空输入内容
resetInputVal('');
scrollToBottom();
try {
await gptChatPrompt(newChatList[newChatList.length - 2]);
// 如果是 Human 第一次发送,插入历史记录
const humanChat = newChatList.filter((item) => item.obj === 'Human');
if (humanChat.length === 1) {
pushChatHistory({
chatId,
title: humanChat[0].value
});
}
} catch (err: any) {
toast({
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
status: 'warning',
duration: 5000,
isClosable: true
});
resetInputVal(storeInput);
setChatData((state) => ({
...state,
history: newChatList.slice(0, newChatList.length - 2)
}));
}
}, [
inputVal,
chatData,
isChatting,
resetInputVal,
scrollToBottom,
toast,
gptChatPrompt,
pushChatHistory,
chatId
]);
// 删除一句话
const delChatRecord = useCallback(
async (index: number) => {
setLoading(true);
try {
// 删除数据库最后一句
await delChatRecordByIndex(chatId, index);
setChatData((state) => ({
...state,
history: state.history.filter((_, i) => i !== index)
}));
} catch (err) {
console.log(err);
}
setLoading(false);
},
[chatId, setLoading]
);
// 复制内容
const onclickCopy = useCallback(
(chatId: string) => {
const dom = document.getElementById(chatId);
const innerText = dom?.innerText;
innerText && copyData(innerText);
},
[copyData]
);
useEffect(() => {
controller.current = new AbortController();
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
controller.current?.abort();
};
}, [chatId]);
// 初始化聊天框 // 初始化聊天框
useQuery( useQuery(
['initData'], ['init', chatId],
() => { () => {
setLoading(true); setLoading(true);
return getInitChatSiteInfo(chatId, windowId); return getInitChatSiteInfo(chatId);
}, },
{ {
onSuccess(res) { onSuccess(res) {
// 可能没有 windowId给它设置一下 setChatData({
router.replace(`/chat?chatId=${chatId}&windowId=${res.windowId}`); ...res,
history: res.history.map((item) => ({
setChatSiteData(res.chatSite);
setChatList(
res.history.map((item) => ({
...item, ...item,
status: 'finish' status: 'finish'
})) }))
); });
if (res.history.length > 0) {
setTimeout(() => {
scrollToBottom(); scrollToBottom();
}, 500);
}
}, },
onError(e: any) { onError(e: any) {
toast({ toast({
@@ -78,249 +359,96 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
} }
); );
// 重置输入内容
const resetInputVal = useCallback((val: string) => {
setInputVal(val);
setTimeout(() => {
/* 回到最小高度 */
if (TextareaDom.current) {
TextareaDom.current.style.height =
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
}
}, 100);
}, []);
// 重载对话
const resetChat = useCallback(() => {
window.open(`/chat?chatId=${chatId}`, '_self');
}, [chatId]);
// 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;
const prompt = {
obj: newChatList[newChatList.length - 1].obj,
value: newChatList[newChatList.length - 1].value
};
// 流请求,获取数据
const res = await streamFetch({
url: '/api/chat/chatGpt',
data: {
windowId,
prompt,
chatId
},
onMessage: (text: string) => {
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
value: item.value + text
};
})
);
}
});
// 保存对话信息
try {
await postSaveChat({
windowId,
prompts: [
prompt,
{
obj: 'AI',
value: res as string
}
]
});
} catch (err) {
toast({
title: '存储对话出现异常, 继续对话会导致上下文丢失,请刷新页面',
status: 'warning',
duration: 3000,
isClosable: true
});
}
// 设置完成状态
setChatList((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
},
[chatId, toast, 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);
resetInputVal('');
scrollToBottom();
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: any) {
toast({
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
status: 'warning',
duration: 5000,
isClosable: true
});
resetInputVal(storeInput);
setChatList(newChatList.slice(0, newChatList.length - 2));
}
}, [
chatGPTPrompt,
chatList,
chatSiteData,
gpt3ChatPrompt,
inputVal,
isChatting,
resetInputVal,
scrollToBottom,
toast
]);
// 重新编辑
const reEdit = useCallback(async () => {
if (chatList[chatList.length - 1]?.obj !== 'Human') return;
// 删除数据库最后一句
await delLastMessage(windowId);
const val = chatList[chatList.length - 1].value;
resetInputVal(val);
setChatList(chatList.slice(0, -1));
}, [chatList, resetInputVal, windowId]);
return ( return (
<Flex height={'100%'} flexDirection={'column'}>
{/* 头部 */}
<Flex <Flex
px={4} h={'100%'}
h={'50px'} flexDirection={media('row', 'column')}
alignItems={'center'} backgroundColor={useColorModeValue('white', '')}
backgroundColor={'white'}
boxShadow={'0 5px 10px rgba(0,0,0,0.1)'}
zIndex={1}
> >
<Box flex={1}>{chatSiteData?.name}</Box> {isPc ? (
{/* 滚动到底部按键 */} <Box flex={'0 0 250px'} w={0} h={'100%'}>
{ChatBox.current && ChatBox.current.scrollHeight > 2 * ChatBox.current.clientHeight && ( <SlideBar
<Box mr={10} cursor={'pointer'} onClick={scrollToBottom}> resetChat={resetChat}
name={chatData?.name}
chatId={chatId}
modelId={chatData.modelId}
onClose={onCloseSlider}
/>
</Box>
) : (
<Box h={'60px'} borderBottom={'1px solid rgba(0,0,0,0.1)'}>
<Flex
alignItems={'center'}
h={'100%'}
justifyContent={'space-between'}
backgroundColor={useColorModeValue('white', 'gray.700')}
color={useColorModeValue('blackAlpha.700', 'white')}
position={'relative'}
px={7}
>
<Box onClick={onOpenSlider}>
<Icon <Icon
name={'icon-xiangxiazhankai-xianxingyuankuang'} name={'menu'}
width={25} w={'20px'}
height={25} h={'20px'}
color={'#718096'} fill={useColorModeValue('blackAlpha.700', 'white')}
></Icon> />
</Box>
<Box>{chatData?.name}</Box>
</Flex>
<Drawer isOpen={isOpenSlider} placement="left" size={'xs'} onClose={onCloseSlider}>
<DrawerOverlay backgroundColor={'rgba(255,255,255,0.5)'} />
<DrawerContent maxWidth={'250px'}>
<SlideBar
resetChat={resetChat}
name={chatData?.name}
chatId={chatId}
modelId={chatData.modelId}
onClose={onCloseSlider}
/>
</DrawerContent>
</Drawer>
</Box> </Box>
)} )}
{/* 重置按键 */}
<Button size={'sm'} colorScheme={'gray'} onClick={resetChat}> <Flex
{...media({ h: '100%', w: 0 }, { h: 0, w: '100%' })}
</Button> flex={'1 0 0'}
</Flex> flexDirection={'column'}
>
{/* 聊天内容 */} {/* 聊天内容 */}
<Box ref={ChatBox} flex={'1 0 0'} h={0} w={'100%'} px={0} pb={10} overflowY={'auto'}> <Box ref={ChatBox} pb={[4, 0]} flex={'1 0 0'} h={0} w={'100%'} overflowY={'auto'}>
{chatList.map((item, index) => ( {chatData.history.map((item, index) => (
<Box <Box
key={index} key={index}
py={media(9, 6)} py={media(9, 6)}
px={media(4, 2)} px={media(4, 2)}
backgroundColor={index % 2 === 0 ? 'rgba(247,247,248,1)' : '#fff'} backgroundColor={
index % 2 !== 0 ? useColorModeValue('blackAlpha.50', 'gray.700') : ''
}
color={useColorModeValue('blackAlpha.700', 'white')}
borderBottom={'1px solid rgba(0,0,0,0.1)'} borderBottom={'1px solid rgba(0,0,0,0.1)'}
> >
<Flex maxW={'800px'} m={'auto'} alignItems={'flex-start'}> <Flex maxW={'750px'} m={'auto'} alignItems={'flex-start'}>
<Box mr={media(4, 1)}> <Menu>
<MenuButton as={Box} mr={media(4, 1)} cursor={'pointer'}>
<Image <Image
src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'} src={item.obj === 'Human' ? '/icon/human.png' : '/icon/logo.png'}
alt="/icon/logo.png" alt="/icon/logo.png"
width={media(30, 20)} width={media(30, 20)}
height={media(30, 20)} height={media(30, 20)}
/> />
</Box> </MenuButton>
<Box flex={'1 0 0'} w={0} overflow={'hidden'}> <MenuList fontSize={'sm'}>
<MenuItem onClick={() => onclickCopy(`chat${index}`)}></MenuItem>
<MenuItem onClick={() => delChatRecord(index)}></MenuItem>
</MenuList>
</Menu>
<Box flex={'1 0 0'} w={0} overflow={'hidden'} id={`chat${index}`}>
{item.obj === 'AI' ? ( {item.obj === 'AI' ? (
<Markdown <Markdown
source={item.value} source={item.value}
isChatting={isChatting && index === chatList.length - 1} isChatting={isChatting && index === chatData.history.length - 1}
/> />
) : ( ) : (
<Box whiteSpace={'pre-wrap'}>{item.value}</Box> <Box whiteSpace={'pre-wrap'}>{item.value}</Box>
@@ -329,48 +457,24 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
</Flex> </Flex>
</Box> </Box>
))} ))}
{chatData.history.length === 0 && <Empty intro={chatData.intro} />}
</Box> </Box>
{/* 空内容提示 */} {/* 发送区 */}
{/* { <Box m={media('20px auto', '0 auto')} w={'100%'} maxW={media('min(750px, 100%)', 'auto')}>
chatList.length === 0 && (
<>
<Card>
内容太长
</Card>
</>
)
} */}
<Box <Box
m={media('20px auto', '0 auto')} py={'18px'}
w={media('100vw', '100%')}
maxW={media('800px', 'auto')}
boxShadow={'0 -14px 30px rgba(255,255,255,0.6)'}
borderTop={media('none', '1px solid rgba(0,0,0,0.1)')}
>
{lastWordHuman ? (
<Box textAlign={'center'}>
<Box color={'red'}></Box>
<Flex py={5} justifyContent={'center'}>
<Button mr={20} onClick={resetChat} colorScheme={'green'}>
</Button>
<Button onClick={reEdit}></Button>
</Flex>
</Box>
) : (
<Box
py={5}
position={'relative'} position={'relative'}
boxShadow={'base'} boxShadow={`0 0 15px rgba(0,0,0,0.1)`}
overflow={'hidden'} border={media('1px solid', '0')}
borderRadius={media('md', 'none')} borderColor={useColorModeValue('gray.200', 'gray.700')}
borderRadius={['none', 'md']}
backgroundColor={useColorModeValue('white', 'gray.700')}
> >
{/* 输入框 */} {/* 输入框 */}
<Textarea <Textarea
ref={TextareaDom} ref={TextareaDom}
w={'100%'}
pr={'45px'}
py={0} py={0}
pr={['45px', '55px']}
border={'none'} border={'none'}
_focusVisible={{ _focusVisible={{
border: 'none' border: 'none'
@@ -382,8 +486,11 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
height={'22px'} height={'22px'}
lineHeight={'22px'} lineHeight={'22px'}
maxHeight={'150px'} maxHeight={'150px'}
maxLength={chatSiteData?.secret.contentMaxLen || -1} maxLength={-1}
overflowY={'auto'} overflowY={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
color={useColorModeValue('blackAlpha.700', 'white')}
onChange={(e) => { onChange={(e) => {
const textarea = e.target; const textarea = e.target;
setInputVal(textarea.value); setInputVal(textarea.value);
@@ -402,24 +509,36 @@ const Chat = ({ chatId, windowId }: { chatId: string; windowId?: string }) => {
}} }}
/> />
{/* 发送和等待按键 */} {/* 发送和等待按键 */}
<Box position={'absolute'} bottom={5} right={media('20px', '10px')}> <Flex
alignItems={'center'}
justifyContent={'center'}
h={'30px'}
w={'30px'}
position={'absolute'}
right={['12px', '20px']}
bottom={'15px'}
onClick={sendPrompt}
>
{isChatting ? ( {isChatting ? (
<Image <Image
style={{ transform: 'translateY(4px)' }} style={{ transform: 'translateY(4px)' }}
src={'/icon/chatting.svg'} src={'/icon/chatting.svg'}
width={30} fill
height={30}
alt={''} alt={''}
/> />
) : ( ) : (
<Box cursor={'pointer'} onClick={sendPrompt}> <Icon
<Icon name={'icon-fasong'} width={20} height={20} color={'#718096'}></Icon> name={'chatSend'}
</Box> width={['18px', '20px']}
height={['18px', '20px']}
cursor={'pointer'}
fill={useColorModeValue('#718096', 'white')}
></Icon>
)} )}
</Flex>
</Box> </Box>
</Box> </Box>
)} </Flex>
</Box>
</Flex> </Flex>
); );
}; };
@@ -428,9 +547,8 @@ export default Chat;
export async function getServerSideProps(context: any) { export async function getServerSideProps(context: any) {
const chatId = context.query?.chatId || ''; const chatId = context.query?.chatId || '';
const windowId = context.query?.windowId || '';
return { return {
props: { chatId, windowId } props: { chatId }
}; };
} }

View File

@@ -0,0 +1,97 @@
import React, { useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
Input,
Select,
FormControl,
FormErrorMessage
} from '@chakra-ui/react';
import { postData } from '@/api/data';
import { useMutation } from '@tanstack/react-query';
import { useForm, SubmitHandler } from 'react-hook-form';
import { DataType } from '@/types/data';
import { DataTypeTextMap } from '@/constants/data';
export interface CreateDataProps {
name: string;
type: DataType;
}
const CreateDataModal = ({
onClose,
onSuccess
}: {
onClose: () => void;
onSuccess: () => void;
}) => {
const [inputVal, setInputVal] = useState('');
const {
getValues,
register,
handleSubmit,
formState: { errors }
} = useForm<CreateDataProps>({
defaultValues: {
name: '',
type: 'abstract'
}
});
const { isLoading, mutate } = useMutation({
mutationFn: (e: CreateDataProps) => postData(e),
onSuccess() {
onSuccess();
onClose();
}
});
return (
<Modal isOpen={true} onClose={onClose}>
<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>
<Select placeholder="数据集类型" {...register('type', {})}>
{Object.entries(DataTypeTextMap).map(([key, value]) => (
<option key={key} value={key}>
{value}
</option>
))}
</Select>
</FormControl>
</ModalBody>
<ModalFooter>
<Button colorScheme={'gray'} onClick={onClose}>
</Button>
<Button ml={3} isLoading={isLoading} onClick={handleSubmit(mutate as any)}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default CreateDataModal;

View File

@@ -0,0 +1,229 @@
import React, { useState, useCallback } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Button,
Box,
Flex,
Textarea
} from '@chakra-ui/react';
import { useTabs } from '@/hooks/useTabs';
import { useConfirm } from '@/hooks/useConfirm';
import { useSelectFile } from '@/hooks/useSelectFile';
import { readTxtContent, readPdfContent, readDocContent } from '@/utils/tools';
import { postSplitData } from '@/api/data';
import { useMutation } from '@tanstack/react-query';
import { useToast } from '@/hooks/useToast';
import { useLoading } from '@/hooks/useLoading';
import { formatPrice } from '@/utils/user';
import { modelList, ChatModelNameEnum } from '@/constants/model';
import { encode } from 'gpt-token-utils';
const fileExtension = '.txt,.doc,.docx,.pdf,.md';
const ImportDataModal = ({
dataId,
onClose,
onSuccess
}: {
dataId: string;
onClose: () => void;
onSuccess: () => void;
}) => {
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认提交生成任务?该任务无法终止!'
});
const { toast } = useToast();
const { setIsLoading, Loading } = useLoading();
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
const { tabs, activeTab, setActiveTab } = useTabs({
tabs: [
{ id: 'text', label: '文本' },
{ id: 'doc', label: '文件' }
// { id: 'url', label: '链接' }
]
});
const [textInput, setTextInput] = useState('');
const [fileText, setFileText] = useState('');
const { mutate: handleClickSubmit, isLoading } = useMutation({
mutationFn: async () => {
let text = '';
if (activeTab === 'text') {
text = textInput;
} else if (activeTab === 'doc') {
text = fileText;
} else if (activeTab === 'url') {
}
if (!text) return;
return postSplitData(dataId, text);
},
onSuccess() {
toast({
title: '任务提交成功',
status: 'success'
});
onClose();
onSuccess();
},
onError(err: any) {
toast({
title: err?.message || '提交任务异常',
status: 'error'
});
}
});
const onSelectFile = useCallback(
async (e: File[]) => {
setIsLoading(true);
try {
const fileTexts = (
await Promise.all(
e.map((file) => {
// @ts-ignore
const extension = file?.name?.split('.').pop().toLowerCase();
switch (extension) {
case 'txt':
case 'md':
return readTxtContent(file);
case 'pdf':
return readPdfContent(file);
case 'doc':
case 'docx':
return readDocContent(file);
default:
return '';
}
})
)
)
.join('\n')
.replace(/\n+/g, '\n');
setFileText(fileTexts);
console.log(encode(fileTexts));
} catch (error: any) {
console.log(error);
toast({
title: typeof error === 'string' ? error : '解析文件失败',
status: 'error'
});
}
setIsLoading(false);
},
[setIsLoading, toast]
);
return (
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent position={'relative'} maxW={['90vw', '800px']}>
<ModalHeader>
QA
<Box ml={2} as={'span'} fontSize={'sm'} color={'blackAlpha.600'}>
{formatPrice(
modelList.find((item) => item.model === ChatModelNameEnum.GPT35)?.price || 0,
1000
)}
/1K tokens
</Box>
</ModalHeader>
<ModalCloseButton />
<ModalBody display={'flex'}>
<Box>
{tabs.map((item) => (
<Button
key={item.id}
display={'block'}
variant={activeTab === item.id ? 'solid' : 'outline'}
_notLast={{
mb: 3
}}
onClick={() => setActiveTab(item.id)}
>
{item.label}
</Button>
))}
</Box>
<Box flex={'1 0 0'} w={0} ml={3} minH={'200px'}>
{activeTab === 'text' && (
<>
<Textarea
h={'100%'}
maxLength={-1}
value={textInput}
placeholder={'请粘贴或输入需要处理的文本'}
onChange={(e) => setTextInput(e.target.value)}
/>
<Box mt={2}>
{textInput.length} {encode(textInput).length} tokens
</Box>
</>
)}
{activeTab === 'doc' && (
<Flex
flexDirection={'column'}
p={2}
h={'100%'}
alignItems={'center'}
justifyContent={'center'}
border={'1px solid '}
borderColor={'blackAlpha.200'}
borderRadius={'md'}
fontSize={'sm'}
>
<Button onClick={onOpen}></Button>
<Box mt={2}> {fileExtension} </Box>
{fileText && (
<>
<Box mt={2}>
{fileText.length} {encode(fileText).length} tokens
</Box>
<Box
maxH={'300px'}
w={'100%'}
overflow={'auto'}
p={2}
backgroundColor={'blackAlpha.50'}
whiteSpace={'pre'}
fontSize={'xs'}
>
{fileText}
</Box>
</>
)}
</Flex>
)}
</Box>
</ModalBody>
<ModalFooter>
<Button colorScheme={'gray'} onClick={onClose}>
</Button>
<Button
ml={3}
isLoading={isLoading}
isDisabled={!textInput && !fileText}
onClick={openConfirm(handleClickSubmit)}
>
</Button>
</ModalFooter>
<Loading />
</ModalContent>
<ConfirmChild />
<File onSelect={onSelectFile} />
</Modal>
);
};
export default ImportDataModal;

Some files were not shown because too many files have changed in this diff Show More