├── .assets ├── chat.png ├── file.jpg ├── param.png ├── qq.jpg ├── role.jpg ├── translate.png ├── web.jpg └── wx.jpg ├── .eslintrc.js ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── README.md ├── apps ├── server │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── nest-cli.json │ ├── package.json │ ├── src │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── app.service.ts │ │ ├── chat │ │ │ ├── chat.controller.ts │ │ │ ├── chat.dto.ts │ │ │ └── chat.service.ts │ │ ├── global-exception.filter.ts │ │ ├── main.ts │ │ ├── translate │ │ │ ├── translate.controller.ts │ │ │ ├── translate.dto.ts │ │ │ └── translate.service.ts │ │ └── upload │ │ │ ├── upload.controller.ts │ │ │ └── upload.service.ts │ ├── test │ │ ├── app.e2e-spec.ts │ │ └── jest-e2e.json │ ├── tsconfig.build.json │ └── tsconfig.json └── web │ ├── .eslintrc.js │ ├── README.md │ ├── global-store.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.page.js │ ├── chat │ │ ├── chat-panel.tsx │ │ ├── config-modal.tsx │ │ ├── index.page.tsx │ │ ├── sidebar.tsx │ │ └── store │ │ │ ├── chat.ts │ │ │ ├── index.ts │ │ │ └── session-list.ts │ ├── index.page.tsx │ └── translate.page.tsx │ ├── postcss.config.js │ ├── public │ ├── bg.png │ ├── subset-Kingsoft_Cloud_Font.woff │ └── subset-Kingsoft_Cloud_Font.woff2 │ ├── styles.css │ ├── tailwind.config.js │ ├── tsconfig.json │ └── utils.ts ├── ecosystem.config.js ├── package.json ├── packages ├── eslint-config-custom │ ├── index.js │ └── package.json └── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/chat.png -------------------------------------------------------------------------------- /.assets/file.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/file.jpg -------------------------------------------------------------------------------- /.assets/param.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/param.png -------------------------------------------------------------------------------- /.assets/qq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/qq.jpg -------------------------------------------------------------------------------- /.assets/role.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/role.jpg -------------------------------------------------------------------------------- /.assets/translate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/translate.png -------------------------------------------------------------------------------- /.assets/web.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/web.jpg -------------------------------------------------------------------------------- /.assets/wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/.assets/wx.jpg -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | settings: { 6 | next: { 7 | rootDir: ["apps/*/"], 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | 4 | .store 5 | .uploads 6 | 7 | # dependencies 8 | node_modules 9 | .pnp 10 | .pnp.js 11 | 12 | # testing 13 | coverage 14 | 15 | # next.js 16 | .next/ 17 | out/ 18 | build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # turbo 37 | .turbo 38 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach NestJS WS", 11 | "port": 9229, 12 | "restart": true 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.associations": { 4 | "*.css": "tailwindcss" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI 实验室 2 | 3 | AI 实验室(https://www.ailab.fit/) 4 | 是一个开源的 AI 探索广场,为 AI 从业者、研究人员或者 AI 爱好者们提供了一个可以针对 Open API 进行实验的能力,目前探索的场景有: 5 | 6 | - 实时翻译 7 | - 个人助理 8 | - AI 调参 9 | - 智能客服 10 | - 更多能力场景持续建设中... 11 | 12 | ## 功能详情 13 | 14 | ### 实时翻译 15 | 16 | 这里提供了一个利用 Open AI 的 API 进行翻译的平台,支持 `prompt` 的自定义,您可以让 AI 返回更适合自己场景的翻译方案。 17 | 与直接使用`ChatGPT`不同的是,这里对文本内容进行了自动分割,可以突破 AI 的上下文设置,您可以将大段长文本输入进来: 18 | 19 | ![translate](.assets/translate.png) 20 | 21 | ### 个人助理 22 | 23 | 与`ChatGPT`类似,AI 实现了一个可供聊天的会话交互界面,可以保持会话上下文,与 AI 进行对话,只需要有`API KEY`,对网络环境无特殊要求: 24 | 25 | ![chat](.assets/chat.png) 26 | 27 | #### 系统角色扮演 28 | 29 | 在助手里支持预设角色扮演,与`ChatGPT`不同的是,在整个会话过程中,AI 会保持一致的系统角色行为,而不会因上下文过长而丢失。 30 | 31 | ![role](.assets/role.jpg) 32 | 33 | ### AI 调参 34 | 35 | 在会话配置界面,可以配置不同的 AI 参数,支持 OpenAI 所有模型,用于调试不同参数组合下的 AI 回复的质量是否满足要求,这在我们做应用开发的场景十分有用。 36 | 37 | ![param](.assets/param.png) 38 | 39 | ### 智能客服 40 | 41 | 在智能助手里,还支持了基于提供语料环境进行客服问答的能力,目前支持在线网页、PDF 上传、Github 等。 42 | 43 | #### 基于网页的内容问答 44 | 45 | 输入网页可以在线解析网页内容,基于内容做问答: 46 | 47 | ![web](.assets/web.jpg) 48 | 49 | #### 基于 PDF 的内容问答 50 | 51 | 上传 PDF 文件,可以基于 PDF 文件本身来进行问答: 52 | 53 | ![file](.assets/file.jpg) 54 | 55 | ## 动机 56 | 57 | 2022 年 11 月 30 日,ChatGPT 发布,人工智能迎来了 Netscape Navigator 时刻。它不但可以实现多轮文本对话,也可以写代码、写营销文案、写诗歌、写商业计划书、写电影剧本。虽然并不完美、也会出错,但看起来无所不能。随着后面 GPT-4 的发布,拥有更强的推理能力,一个崭新的时代即将到来。连埃隆·马斯克都评价道:“ChatGPT 好得吓人,我们离危险的强人工智能不远了。” 58 | 59 | 2023 年 2 月 10 日,比尔盖茨在接受采访时表示,像 ChatGPT 这样的人工智能的崛起,与互联网的诞生和个人计算机的发展同样重要。 60 | 61 | 作为一名前端开发人员,我感到了深深的危机和不确定性,随着微软、Google、百度、阿里、腾讯等大公司先后入局,目前 AI 俨然已经对各行各业产生了不同的冲击,从客观来看,人工智能时代来临是必然的趋势。 62 | 63 | 相比于其他行业来说,程序员有一个天然的优势,可以用自己的编程能力更快速地连接 AI,对于 AI 的研发目前主要分为两类: 64 | 65 | - 基础研发人员,主要负责大模型开发 66 | - 应用开发人员,将 AI 落地到实际应用场景当中 67 | 68 | 第一类人员目前还是非常稀少的,需要有很强的理论研究基础,目前主要分布在一些顶尖的高校人才和大厂的少部分研究院当中。 69 | 70 | 我们绝大多数人都处于第二类人群中,不论是前端、后端、数据、算法、测试,掌握不同的编程语言和技能,可以将一些关于 AI 的想法快速落地到生产项目中。这也是十分重要的一环,从目前来看,微软和阿里先后都将它们都产品全家桶加入 AI 的能力,后续产品 AI 化也将成为必然的趋势,Maas 的时代已经到来。 71 | 72 | 作为应用开发的我们,应当坦然接受这一轮 AI 的洗礼,其实现在大模型对开发人员已经十分友好了,不需要太多地知识储备,我们可以直接用现有的技能连接 AI,占取先机。 73 | 74 | 那么为什么选择本项目呢? 75 | 76 | 对于开发人员,这个项目有如下优势: 77 | 78 | - 结合我自身 7 年的大厂研发经验,目前也是部门相关项目的落地负责人,相信能够给你带来一定的帮助。 79 | - 目前 AI 的研发生态主要集中在 Python,而想落地到产品应用,前端技术是必不可少的,目前整个 JS 生态对 AI 的探索还是偏少,本项目可以提供这方面的思路。 80 | - 后续我也会持续参与到 langchain 这类工具的建设中,在项目落地过程中也能触发更多的灵感和思路。 81 | 82 | 对于其他人员,你可以直接使用产品: 83 | 84 | - 增强版 chatGPT,基于 GPT 的能力,提供了调参、智能客服、论文/pdf 阅读、网页理解、智能翻译等多种工具,更多的能力还在支持中。 85 | - 无需复杂的网络环境,只需要 Open AI 的 Token 即可访问。 86 | 87 | 本项目不以盈利为目标,有一个真诚的愿景:建立起 AI 应用探索的生态,帮助大家快速将 AI 想法落地,结合社区地力量让开发更美好。欢迎有志之士加入共建! 88 | 89 | ## 后续规划 90 | 91 | - 高级版的一些尝试探索(可能包括插件机制、IFTTT 等,以开放的形式接入) 92 | - 分发平台,与企微、QQ、微信机器人打通,用户可借助平台能力快速开发管理机器人,形成生态 93 | - 探索更多借助 AI 能做的事情 94 | 95 | ## 如何贡献 96 | 97 | 本项目采用前端最新的技术栈搭建而成,建议先了解: 98 | 99 | - pnpm(https://pnpm.io/) 100 | - turborepo(https://turbo.build/) 101 | - nextjs(https://nextjs.org/) 102 | - nestjs(https://nestjs.com/) 103 | 104 | 在项目工程运行开发环境: 105 | 106 | ``` 107 | pnpm i 108 | ``` 109 | 110 | ``` 111 | pnpm dev 112 | ``` 113 | 114 | 配置 whistle 代理规则: 115 | 116 | ``` 117 | ailib.fit/api 127.0.0.1:3100 118 | ailib.fit 127.0.0.1:3000 119 | ``` 120 | 121 | 浏览器访问 ailib.fit 即可。 122 | 123 | ## 更多交流 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /apps/server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /apps/server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /apps/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /apps/server/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ pnpm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ pnpm run start 40 | 41 | # watch mode 42 | $ pnpm run start:dev 43 | 44 | # production mode 45 | $ pnpm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ pnpm run test 53 | 54 | # e2e tests 55 | $ pnpm run test:e2e 56 | 57 | # test coverage 58 | $ pnpm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /apps/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "dev": "nest start --debug --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@dqbd/tiktoken": "^1.0.4", 24 | "@nestjs/common": "^9.0.0", 25 | "@nestjs/core": "^9.0.0", 26 | "@nestjs/platform-express": "^9.0.0", 27 | "@nestjs/serve-static": "^3.0.1", 28 | "cheerio": "1.0.0-rc.12", 29 | "class-transformer": "^0.5.1", 30 | "class-validator": "^0.14.0", 31 | "hnswlib-node": "^1.4.2", 32 | "langchain": "^0.0.49", 33 | "multer": "1.4.5-lts.1", 34 | "pdfjs-dist": "^3.5.141", 35 | "reflect-metadata": "^0.1.13", 36 | "rxjs": "^7.2.0" 37 | }, 38 | "devDependencies": { 39 | "@nestjs/cli": "^9.0.0", 40 | "@nestjs/schematics": "^9.0.0", 41 | "@nestjs/testing": "^9.0.0", 42 | "@types/express": "^4.17.13", 43 | "@types/jest": "29.5.0", 44 | "@types/node": "18.15.11", 45 | "@types/supertest": "^2.0.11", 46 | "@typescript-eslint/eslint-plugin": "^5.0.0", 47 | "@typescript-eslint/parser": "^5.0.0", 48 | "eslint": "^8.0.1", 49 | "eslint-config-prettier": "^8.3.0", 50 | "eslint-plugin-prettier": "^4.0.0", 51 | "jest": "29.5.0", 52 | "prettier": "^2.3.2", 53 | "source-map-support": "^0.5.20", 54 | "supertest": "^6.1.3", 55 | "ts-jest": "29.0.5", 56 | "ts-loader": "^9.2.3", 57 | "ts-node": "^10.0.0", 58 | "tsconfig-paths": "4.2.0", 59 | "typescript": "^4.7.4" 60 | }, 61 | "jest": { 62 | "moduleFileExtensions": [ 63 | "js", 64 | "json", 65 | "ts" 66 | ], 67 | "rootDir": "src", 68 | "testRegex": ".*\\.spec\\.ts$", 69 | "transform": { 70 | "^.+\\.(t|j)s$": "ts-jest" 71 | }, 72 | "collectCoverageFrom": [ 73 | "**/*.(t|j)s" 74 | ], 75 | "coverageDirectory": "../coverage", 76 | "testEnvironment": "node" 77 | } 78 | } -------------------------------------------------------------------------------- /apps/server/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, ValidationPipe } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { TranslateController } from './translate/translate.controller'; 5 | import { TranslateService } from './translate/translate.service'; 6 | import { APP_PIPE } from '@nestjs/core'; 7 | import { ServeStaticModule } from '@nestjs/serve-static'; 8 | import { join } from 'path'; 9 | import { ChatController } from './chat/chat.controller'; 10 | import { ChatService } from './chat/chat.service'; 11 | import { UploadController } from './upload/upload.controller'; 12 | import { MulterModule } from '@nestjs/platform-express'; 13 | 14 | @Module({ 15 | imports: [ 16 | ServeStaticModule.forRoot({ 17 | rootPath: join(__dirname, '../../web', 'out'), 18 | }), 19 | MulterModule.register({ 20 | dest: './.uploads', 21 | }), 22 | ], 23 | controllers: [ 24 | AppController, 25 | TranslateController, 26 | ChatController, 27 | UploadController, 28 | ], 29 | providers: [ 30 | { 31 | provide: APP_PIPE, 32 | useClass: ValidationPipe, 33 | }, 34 | AppService, 35 | TranslateService, 36 | ChatService, 37 | ], 38 | }) 39 | export class AppModule {} 40 | -------------------------------------------------------------------------------- /apps/server/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/src/chat/chat.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, Res } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | import { ChatMessageDto } from './chat.dto'; 4 | import { ChatService } from './chat.service'; 5 | 6 | @Controller('chat') 7 | export class ChatController { 8 | constructor(private readonly chatService: ChatService) {} 9 | 10 | @Post() 11 | chat(@Body() data: ChatMessageDto, @Res() res: Response) { 12 | res.setHeader('Content-Type', 'text/event-stream'); 13 | res.setHeader('Cache-Control', 'no-cache'); 14 | res.setHeader('Connection', 'keep-alive'); 15 | res.flushHeaders(); // 发送这些头信息到客户端 16 | 17 | const observable = this.chatService.sendMessage(data); 18 | observable.subscribe((token) => { 19 | res.write(`${token}`); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/server/src/chat/chat.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsArray, 3 | IsBoolean, 4 | IsNotEmpty, 5 | IsNumber, 6 | IsOptional, 7 | IsString, 8 | } from 'class-validator'; 9 | 10 | export class ChatMessageDto { 11 | @IsNotEmpty() 12 | @IsString() 13 | id: string; 14 | 15 | @IsString() 16 | @IsOptional() 17 | text?: string; 18 | 19 | @IsNotEmpty() 20 | @IsString() 21 | openAIToken: string; 22 | 23 | @IsNumber() 24 | temperature: number; 25 | 26 | @IsNumber() 27 | topP: number; 28 | 29 | @IsNumber() 30 | frequencyPenalty: number; 31 | 32 | @IsNumber() 33 | presencePenalty: number; 34 | 35 | @IsNumber() 36 | n: number; 37 | 38 | @IsString() 39 | modelName: string; 40 | 41 | @IsString() 42 | @IsOptional() 43 | systemPrompt?: string; 44 | 45 | @IsArray() 46 | @IsOptional() 47 | stop?: string[]; 48 | 49 | @IsNumber() 50 | @IsOptional() 51 | timeout?: number; 52 | 53 | @IsNumber() 54 | @IsOptional() 55 | maxTokens?: number; 56 | 57 | @IsBoolean() 58 | @IsOptional() 59 | isAbstract?: boolean; 60 | 61 | @IsString() 62 | @IsOptional() 63 | extraDataUrl?: string; 64 | 65 | @IsString() 66 | @IsOptional() 67 | extraDataGithub?: string; 68 | 69 | @IsString() 70 | @IsOptional() 71 | githubToken?: string; 72 | 73 | @IsArray() 74 | messageList: Array; 75 | 76 | @IsString() 77 | @IsOptional() 78 | filename?: string; 79 | 80 | @IsString() 81 | @IsOptional() 82 | fileMimeType?: string; 83 | } 84 | -------------------------------------------------------------------------------- /apps/server/src/chat/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ChatOpenAI } from 'langchain/chat_models'; 3 | import { CallbackManager } from 'langchain/callbacks'; 4 | import { Observable, Subscriber } from 'rxjs'; 5 | import { ChatMessageDto } from './chat.dto'; 6 | import { ConversationalRetrievalQAChain } from 'langchain/chains'; 7 | import { 8 | CheerioWebBaseLoader, 9 | GithubRepoLoader, 10 | PDFLoader, 11 | TextLoader, 12 | } from 'langchain/document_loaders'; 13 | import * as fs from 'fs'; 14 | import { HNSWLib } from 'langchain/vectorstores'; 15 | import { OpenAIEmbeddings } from 'langchain/embeddings'; 16 | 17 | import * as crypto from 'crypto'; 18 | import { OpenAIChat } from 'langchain/llms'; 19 | import { 20 | AIChatMessage, 21 | HumanChatMessage, 22 | SystemChatMessage, 23 | } from 'langchain/schema'; 24 | 25 | // 提问模板 26 | const questionGeneratorTemplate = `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question(use chinese). 27 | 28 | Chat History: 29 | {chat_history} 30 | Follow Up Input: {question} 31 | Standalone question:`; 32 | 33 | // 回答模板 34 | const qaTemplate = `Use the following pieces of context to answer the question at the end(use chinese) 35 | 36 | {context} 37 | 38 | Question: {question} 39 | Helpful Answer:`; 40 | 41 | @Injectable() 42 | export class ChatService { 43 | sendMessage(data: ChatMessageDto) { 44 | let observer: Subscriber = null; 45 | const observable = new Observable((ob) => { 46 | observer = ob; 47 | 48 | const callbackManager = CallbackManager.fromHandlers({ 49 | async handleLLMStart(llm, prompts) { 50 | console.log('handleLLMStart', prompts); 51 | }, 52 | async handleLLMNewToken(token: string) { 53 | observer.next(token); 54 | }, 55 | }); 56 | 57 | if (data.extraDataUrl || data.filename || data.extraDataGithub) { 58 | this.qaWithContent(data, callbackManager); 59 | } else { 60 | this.chat(data, callbackManager); 61 | } 62 | }); 63 | 64 | return observable; 65 | } 66 | 67 | private async chat(data: ChatMessageDto, callbackManager: CallbackManager) { 68 | const model = new ChatOpenAI({ 69 | streaming: true, 70 | openAIApiKey: data.openAIToken, 71 | callbackManager, 72 | ...data, 73 | }); 74 | 75 | model.call([ 76 | new SystemChatMessage( 77 | data.systemPrompt + 78 | 'The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.', 79 | ), 80 | ...data.messageList.map((m) => { 81 | if (m.type === 0) { 82 | return new HumanChatMessage(m.text); 83 | } 84 | return new AIChatMessage(m.text); 85 | }), 86 | new HumanChatMessage(data.text), 87 | ]); 88 | } 89 | 90 | private async qaWithContent( 91 | data: ChatMessageDto, 92 | callbackManager: CallbackManager, 93 | ) { 94 | let loader; 95 | if (data.filename) { 96 | loader = data.fileMimeType.includes('pdf') 97 | ? new PDFLoader('./.uploads/' + data.filename, { 98 | pdfjs: () => import('pdfjs-dist/legacy/build/pdf.js'), 99 | }) 100 | : new TextLoader('./.uploads/' + data.filename); 101 | } else if (data.extraDataGithub) { 102 | loader = new GithubRepoLoader(data.extraDataGithub, { 103 | accessToken: data.githubToken, 104 | }); 105 | } else { 106 | loader = new CheerioWebBaseLoader(data.extraDataUrl); 107 | } 108 | const docs = await loader.loadAndSplit(); 109 | 110 | const path = 111 | './.store/' + 112 | crypto 113 | .createHash('md5') 114 | .update(data.filename || data.extraDataGithub || data.extraDataUrl) 115 | .digest('hex'); 116 | 117 | let vectorStore; 118 | if (fs.existsSync(path)) { 119 | vectorStore = await HNSWLib.load( 120 | path, 121 | new OpenAIEmbeddings({ openAIApiKey: data.openAIToken }), 122 | ); 123 | } else { 124 | vectorStore = await HNSWLib.fromDocuments( 125 | docs, 126 | new OpenAIEmbeddings({ openAIApiKey: data.openAIToken }), 127 | ); 128 | await vectorStore.save(path); 129 | } 130 | 131 | const model = new OpenAIChat({ 132 | streaming: true, 133 | openAIApiKey: data.openAIToken, 134 | callbackManager, 135 | ...data, 136 | }); 137 | 138 | const chain = ConversationalRetrievalQAChain.fromLLM( 139 | model, 140 | vectorStore.asRetriever(), 141 | { 142 | qaTemplate, 143 | questionGeneratorTemplate, 144 | }, 145 | ); 146 | 147 | chain.call({ 148 | question: data.text, 149 | chat_history: data.messageList.map((m) => m.text), 150 | }); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /apps/server/src/global-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common'; 2 | import { HttpException } from '@nestjs/common'; 3 | @Catch() 4 | export class GlobalExceptionFilter implements ExceptionFilter { 5 | catch(exception: any, host: ArgumentsHost) { 6 | const ctx = host.switchToHttp(); 7 | const response = ctx.getResponse(); 8 | const request = ctx.getRequest(); 9 | const status = 10 | exception instanceof HttpException ? exception.getStatus() : 500; 11 | const message = 12 | exception.getResponse()?.message || 13 | exception.message || 14 | 'Internal Server Error'; 15 | console.error( 16 | `error, status=${status}, message=${message}, stack=${ 17 | (exception as any).stack 18 | }`, 19 | ); 20 | response.status(status).json({ 21 | statusCode: status, 22 | timestamp: new Date().toISOString(), 23 | path: request.url, 24 | message: message, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import { Logger } from '@nestjs/common'; 5 | import { GlobalExceptionFilter } from './global-exception.filter'; 6 | 7 | async function bootstrap() { 8 | const httpsOptions = { 9 | key: fs.readFileSync('/etc/ssl/certs/ailab.fit.key'), 10 | cert: fs.readFileSync('/etc/ssl/certs/ailab.fit_bundle.crt'), 11 | }; 12 | const app = await NestFactory.create(AppModule, { 13 | httpsOptions, 14 | }); 15 | global.console = new Logger('console') as any; 16 | app.setGlobalPrefix('api'); 17 | app.useGlobalFilters(new GlobalExceptionFilter()); 18 | await app.listen(process.env.PORT || 3100); 19 | } 20 | bootstrap(); 21 | -------------------------------------------------------------------------------- /apps/server/src/translate/translate.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, MessageEvent, Post, Res } from '@nestjs/common'; 2 | import { TranslateService } from './translate.service'; 3 | import { TranslateDto } from './translate.dto'; 4 | import { Response } from 'express'; 5 | 6 | @Controller('translate') 7 | export class TranslateController { 8 | constructor(private readonly translateService: TranslateService) {} 9 | 10 | @Post() 11 | translate(@Body() data: TranslateDto, @Res() res: Response) { 12 | res.setHeader('Content-Type', 'text/event-stream'); 13 | res.setHeader('Cache-Control', 'no-cache'); 14 | res.setHeader('Connection', 'keep-alive'); 15 | res.flushHeaders(); // 发送这些头信息到客户端 16 | 17 | const observable = this.translateService.translate(data); 18 | observable.subscribe((token) => { 19 | res.write(`${token}`); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/server/src/translate/translate.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class TranslateDto { 4 | @IsNotEmpty() 5 | @IsString() 6 | text: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | token: string; 11 | 12 | @IsString() 13 | @IsNotEmpty() 14 | prompt?: string; 15 | } 16 | -------------------------------------------------------------------------------- /apps/server/src/translate/translate.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, MessageEvent } from '@nestjs/common'; 2 | import { ChatOpenAI } from 'langchain/chat_models'; 3 | import { HumanChatMessage, SystemChatMessage } from 'langchain/schema'; 4 | import { TranslateDto } from './translate.dto'; 5 | import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'; 6 | import { CallbackManager } from 'langchain/callbacks'; 7 | import { Observable, Subscriber } from 'rxjs'; 8 | 9 | @Injectable() 10 | export class TranslateService { 11 | translate(data: TranslateDto) { 12 | let observer: Subscriber = null; 13 | const observable = new Observable((ob) => { 14 | observer = ob; 15 | }); 16 | const chatStreaming = new ChatOpenAI({ 17 | openAIApiKey: data.token, 18 | streaming: true, 19 | callbackManager: CallbackManager.fromHandlers({ 20 | async handleLLMNewToken(token: string) { 21 | observer.next(token); 22 | }, 23 | }), 24 | }); 25 | const splitter = new RecursiveCharacterTextSplitter({ 26 | chunkSize: 2000, 27 | chunkOverlap: 100, 28 | }); 29 | splitter.createDocuments([data.text]).then(async (docs) => { 30 | for (const doc of docs) { 31 | await chatStreaming.call([ 32 | new SystemChatMessage('You are a helpful assistant for translating.'), 33 | new HumanChatMessage(data.prompt + doc.pageContent), 34 | ]); 35 | } 36 | }); 37 | 38 | return observable; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/server/src/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | UploadedFile, 5 | UseInterceptors, 6 | } from '@nestjs/common'; 7 | import { FileInterceptor } from '@nestjs/platform-express'; 8 | 9 | @Controller('upload') 10 | export class UploadController { 11 | @Post() 12 | @UseInterceptors(FileInterceptor('file')) 13 | async uploadFile(@UploadedFile() file) { 14 | return file; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/upload/upload.service.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/apps/server/src/upload/upload.service.ts -------------------------------------------------------------------------------- /apps/server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /apps/server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["custom"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 12 | 13 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 14 | 15 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /apps/web/global-store.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorageLRU } from "@cocalc/local-storage-lru"; 2 | 3 | const OPEN_AI_TOKEN_KEY = "OPEN_AI_TOKEN_KEY"; 4 | 5 | // 获取openai token 6 | export function getOpenAIToken() { 7 | return localStorage.getItem(OPEN_AI_TOKEN_KEY); 8 | } 9 | 10 | // 设置openai token 11 | export function setOpenAIToken(token: string) { 12 | localStorage.setItem(OPEN_AI_TOKEN_KEY, token); 13 | } 14 | 15 | export const storage = new LocalStorageLRU(); 16 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | transpilePackages: ["ui"], 4 | pageExtensions: ["page.tsx", "page.ts", "page.jsx", "page.js"], 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@ant-design/icons": "^5.0.1", 13 | "@cocalc/local-storage-lru": "^2.3.0", 14 | "@emotion/react": "^11.10.6", 15 | "@emotion/styled": "^11.10.6", 16 | "@mui/material": "^5.11.16", 17 | "@reduxjs/toolkit": "^1.9.4", 18 | "antd": "^5.4.0", 19 | "classnames": "^2.3.2", 20 | "github-markdown-css": "^5.2.0", 21 | "immer": "^10.0.0", 22 | "marked": "^4.3.0", 23 | "next": "^13.1.1", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "react-redux": "^8.0.5", 27 | "uuidv4": "^6.2.13" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.0.0", 31 | "@types/marked": "^4.0.8", 32 | "@types/node": "^17.0.12", 33 | "@types/react": "^18.0.22", 34 | "@types/react-dom": "^18.0.7", 35 | "autoprefixer": "^10.4.14", 36 | "eslint": "7.32.0", 37 | "eslint-config-custom": "workspace:*", 38 | "postcss": "^8.4.21", 39 | "tailwindcss": "^3.3.1", 40 | "tsconfig": "workspace:*", 41 | "typescript": "^4.5.3" 42 | } 43 | } -------------------------------------------------------------------------------- /apps/web/pages/_app.page.js: -------------------------------------------------------------------------------- 1 | import "github-markdown-css/github-markdown.css"; 2 | import "../styles.css"; 3 | 4 | // This default export is required in a new `pages/_app.js` file. 5 | export default function MyApp({ Component, pageProps }) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/pages/chat/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Avatar, Button, Card, Empty, Input, List } from "antd"; 2 | import cn from "classnames"; 3 | import { marked } from "marked"; 4 | import { SessionConfig } from "./config-modal"; 5 | import { useEffect, useState } from "react"; 6 | import { addMessage as addMessageToStore, getMessageList } from "./store/chat"; 7 | import { uuid } from "uuidv4"; 8 | import { isAdvanced } from "../../utils"; 9 | 10 | // 会话面板属性 11 | interface ChatPanelProps { 12 | sessionConfig?: SessionConfig; 13 | } 14 | 15 | // 消息类型 16 | enum MessageType { 17 | Send, 18 | Reply, 19 | } 20 | 21 | // 消息 22 | export interface Message { 23 | id: string; 24 | type: MessageType; 25 | text: string; 26 | } 27 | 28 | // 消息配置 29 | const messageConf = { 30 | [MessageType.Send]: { 31 | classNames: { 32 | list: ["flex-row-reverse"], 33 | avatar: ["bg-teal-600", "ml-4"], 34 | card: ["border-teal-600"], 35 | }, 36 | avatar: "You", 37 | justify: "end", 38 | }, 39 | [MessageType.Reply]: { 40 | classNames: { 41 | list: ["flex-row"], 42 | avatar: ["bg-indigo-600", "mr-4"], 43 | card: ["border-indigo-600"], 44 | }, 45 | avatar: "AI", 46 | justify: "start", 47 | }, 48 | }; 49 | 50 | /** 51 | * 会话面板 52 | * @param props 53 | * @returns 54 | */ 55 | export function ChatPanel(props: ChatPanelProps) { 56 | const { sessionConfig } = props; 57 | const [messageList, setMessageList] = useState([]); 58 | const [inputValue, setInputValue] = useState(""); 59 | 60 | // 获取消息列表 61 | useEffect(() => { 62 | if (!sessionConfig?.id) { 63 | return; 64 | } 65 | getMessageList(id).then((messageList) => { 66 | if (messageList.length > 0) { 67 | setMessageList(messageList); 68 | // 滚动到底部 69 | scrollToEnd(); 70 | } else { 71 | // 如果没有消息,则发送欢迎语 72 | setMessageList([]); 73 | addMessage({ 74 | id: uuid(), 75 | type: MessageType.Reply, 76 | text: "您好,欢迎来到AI实验室,请问有什么可以帮您?", 77 | }); 78 | } 79 | }); 80 | }, [sessionConfig?.id]); 81 | 82 | // 如果没有会话配置,则显示空 83 | if (!sessionConfig) { 84 | return ( 85 |
86 | 87 |
88 | ); 89 | } 90 | 91 | const { systemPrompt, id, extraDataUrl, filename, extraDataGithub } = 92 | sessionConfig; 93 | 94 | // 滚动到底部 95 | const scrollToEnd = () => { 96 | const chatList = document.getElementById("chat-list"); 97 | if (chatList) { 98 | chatList.scrollTop = chatList.scrollHeight; 99 | } 100 | }; 101 | 102 | const addMessage = (message: Message) => { 103 | setMessageList((prev) => { 104 | const index = prev.findIndex((item) => item.id === message.id); 105 | // 如果已经存在,则替换 106 | if (prev[index]) { 107 | prev[index] = message; 108 | return [...prev]; 109 | } 110 | return [...prev, message]; 111 | }); 112 | addMessageToStore(id, message); 113 | }; 114 | 115 | const sendMessage = async (isAbstract = false) => { 116 | // 首先,当前消息上屏 117 | !isAbstract && 118 | addMessage({ 119 | type: MessageType.Send, 120 | text: inputValue, 121 | id: uuid(), 122 | }); 123 | 124 | // 然后,清空输入框 125 | setInputValue(""); 126 | 127 | // 最后,发送消息 128 | try { 129 | const response = await fetch("/api/chat", { 130 | method: "POST", 131 | headers: { 132 | "Content-Type": "application/json", 133 | }, 134 | body: JSON.stringify({ 135 | text: inputValue, 136 | ...sessionConfig, 137 | isAbstract, 138 | messageList: messageList.slice(-11).slice(1), // 只取最近10条消息(并去掉欢迎语) 139 | }), 140 | }); 141 | 142 | // 处理SSE的返回 143 | const streamReader = response.body!.getReader(); 144 | const utf8Decoder = new TextDecoder("utf-8"); 145 | let dataBuffer = ""; 146 | // 先生成一个消息id 147 | const messageId = uuid(); 148 | const readStream = async () => { 149 | const { done, value } = await streamReader.read(); 150 | 151 | if (done) { 152 | console.error("Stream closed"); 153 | return; 154 | } 155 | 156 | dataBuffer += utf8Decoder.decode(value); 157 | // 这里,我们需要将消息上屏 158 | addMessage({ 159 | id: messageId, 160 | type: MessageType.Reply, 161 | text: marked(dataBuffer), 162 | }); 163 | 164 | scrollToEnd(); 165 | readStream(); 166 | }; 167 | 168 | readStream(); 169 | } catch (e) { 170 | console.error(e); 171 | alert("发送消息失败"); 172 | } 173 | }; 174 | 175 | const managePlugin = () => {}; 176 | 177 | return ( 178 |
179 |
180 | { 185 | const conf = messageConf[item.type]; 186 | return ( 187 | 191 | 195 | {conf.avatar} 196 | 197 | 198 |
202 |
203 |
204 | ); 205 | }} 206 | >
207 |
208 | 209 |
210 | {isAdvanced() && ( 211 | 215 |

216 | 您当前访问的是高级版,可以使用插件的能力。 217 |

218 | 225 |
226 | } 227 | type="info" 228 | showIcon 229 | closable 230 | /> 231 | )} 232 | {extraDataGithub && ( 233 | 237 |

238 | 您当前基于Github({extraDataGithub})内容: 239 | 可对仓库内容进行提问 240 |

241 | 248 |
249 | } 250 | type="info" 251 | showIcon 252 | closable 253 | /> 254 | )} 255 | {filename && ( 256 | 260 |

261 | 您当前基于文件({filename})内容: 可对文件内容进行提问 262 |

263 | 270 | 271 | } 272 | type="info" 273 | showIcon 274 | closable 275 | /> 276 | )} 277 | {extraDataUrl && ( 278 | 282 |

283 | 您当前基于Web URL内容: 284 | {extraDataUrl} 285 | ,可对网页内容进行提问 286 |

287 | 294 | 295 | } 296 | type="info" 297 | showIcon 298 | closable 299 | /> 300 | )} 301 | {systemPrompt && ( 302 | 309 | )} 310 |
311 | { 316 | setInputValue(event.target.value); 317 | }} 318 | onPressEnter={(event) => { 319 | if ( 320 | event.altKey || 321 | event.ctrlKey || 322 | event.shiftKey || 323 | event.metaKey 324 | ) { 325 | return; 326 | } 327 | event.preventDefault(); 328 | if (!inputValue) { 329 | return alert("请输入内容"); 330 | } 331 | sendMessage(); 332 | }} 333 | /> 334 | 345 |
346 | 347 | 348 | ); 349 | } 350 | -------------------------------------------------------------------------------- /apps/web/pages/chat/config-modal.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input, Modal, Radio } from "antd"; 2 | import { useEffect, useState } from "react"; 3 | import { uuid } from "uuidv4"; 4 | import { getOpenAIToken } from "../../global-store"; 5 | import { InboxOutlined } from "@ant-design/icons"; 6 | import { Upload } from "antd"; 7 | import { UploadChangeParam, UploadFile } from "antd/es/upload"; 8 | 9 | const { Dragger } = Upload; 10 | 11 | /** 12 | * 会话配置弹窗模式 13 | */ 14 | export enum ModalMode { 15 | Create, // 创建 16 | Edit, // 修改 17 | } 18 | 19 | /** 20 | * 会话配置弹窗属性 21 | */ 22 | interface ConfigModalProps { 23 | mode: ModalMode; 24 | open: boolean; 25 | onOk: (values: SessionConfig) => void; 26 | onCancel: () => void; 27 | initialSessionConfig?: SessionConfig; 28 | } 29 | 30 | /** 31 | * 会话配置 32 | */ 33 | export interface SessionConfig { 34 | // 会话id 35 | id: string; 36 | // openAI token 37 | openAIToken: string; 38 | // 会话名称 39 | sessionName: string; 40 | // 采样温度 41 | temperature: number; 42 | // 模型参数 43 | topP: number; 44 | // 模型参数 45 | frequencyPenalty: number; 46 | // 模型参数 47 | presencePenalty: number; 48 | // 模型参数 49 | n: number; 50 | // 模型名称 51 | modelName: string; 52 | // 系统设定的提示语 53 | systemPrompt?: string; 54 | // 模型参数 55 | stop?: string[]; 56 | // 模型参数 57 | timeout?: number; 58 | // 模型参数 59 | maxTokens?: number; 60 | extraDataType: ExtraDataType; 61 | extraDataUrl?: string; 62 | extraDataGithub?: string; 63 | githubToken?: string; 64 | filename: string; 65 | fileMimeType?: string; 66 | } 67 | 68 | // 附加数据类型 69 | enum ExtraDataType { 70 | None, 71 | Url, 72 | File, 73 | Github, 74 | } 75 | 76 | /** 77 | * 会话配置弹窗 78 | * @param props 79 | * @returns 80 | */ 81 | export function ConfigModal(props: ConfigModalProps) { 82 | const { mode, open, onOk, onCancel, initialSessionConfig } = props; 83 | const [form] = Form.useForm(); 84 | const [extraDataType, setExtraDataType] = useState(ExtraDataType.None); 85 | const [file, setFile] = useState | null>(null); 86 | 87 | /** 88 | * 默认会话配置 89 | */ 90 | const DEFAULT_SESSION_CONFIG: SessionConfig = { 91 | id: uuid(), 92 | sessionName: "", 93 | systemPrompt: "", 94 | openAIToken: "", 95 | temperature: 1, 96 | topP: 1, 97 | frequencyPenalty: 0, 98 | presencePenalty: 0, 99 | n: 1, 100 | modelName: "gpt-3.5-turbo", 101 | extraDataType: ExtraDataType.None, 102 | extraDataUrl: "", 103 | filename: "", 104 | }; 105 | 106 | // 如果是修改模式, 则设置为当前会话配置(为了回显), 否则设置为默认会话配置 107 | const initialValues = 108 | mode === ModalMode.Edit ? initialSessionConfig : DEFAULT_SESSION_CONFIG; 109 | 110 | useEffect(() => { 111 | // 重置表单, 并设置初始值 112 | if (initialValues) { 113 | const values = { ...initialValues }; 114 | 115 | if (!values.openAIToken) { 116 | // 如果没有设置openAI token, 则设置为全局的openAI token 117 | values.openAIToken = getOpenAIToken() || ""; 118 | } 119 | 120 | form.setFieldsValue(values); 121 | } 122 | }, [initialSessionConfig, mode]); 123 | 124 | const onFileChange = (info: UploadChangeParam>) => { 125 | const { status } = info.file; 126 | if (status === "done") { 127 | setFile(info.file); 128 | } 129 | }; 130 | 131 | return ( 132 | { 136 | form 137 | .validateFields() 138 | .then((values) => { 139 | onOk({ 140 | ...values, 141 | temperature: Number(values.temperature), 142 | topP: Number(values.topP), 143 | frequencyPenalty: Number(values.frequencyPenalty), 144 | presencePenalty: Number(values.presencePenalty), 145 | n: Number(values.n), 146 | filename: file?.response?.filename || "", 147 | fileMimeType: file?.response?.mimetype || "", 148 | }); 149 | }) 150 | .catch((info) => { 151 | console.log("Validate Failed:", info); 152 | }); 153 | }} 154 | onCancel={onCancel} 155 | > 156 |
162 | 163 | 164 | 165 | 170 | 171 | 172 | 177 | 178 | 179 | 184 | 185 | 186 | 191 | 192 | 193 | 198 | 199 | 200 | 205 | 206 | 207 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 228 | 229 | 230 | 235 | 236 | 237 | 242 | { 244 | setExtraDataType(e.target.value); 245 | }} 246 | > 247 | 248 | 网页 249 | 文件 250 | Github 251 | 252 | 253 | {extraDataType === ExtraDataType.Url && ( 254 | 259 | 260 | 261 | )} 262 | {extraDataType === ExtraDataType.File && ( 263 | 268 | 273 |

274 | 275 |

276 |

277 | 请点击或拖拽文件到此区域以上传。 278 |

279 |

支持pdf和txt文件。

280 |
281 |
282 | )} 283 | {extraDataType === ExtraDataType.Github && ( 284 | 289 | 290 | 291 | )} 292 | {extraDataType === ExtraDataType.Github && ( 293 | 298 | 299 | 300 | )} 301 |
302 |
303 | ); 304 | } 305 | -------------------------------------------------------------------------------- /apps/web/pages/chat/index.page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { ConfigModal, ModalMode, SessionConfig } from "./config-modal"; 3 | import { SideBar } from "./sidebar"; 4 | import { ChatPanel } from "./chat-panel"; 5 | import { setOpenAIToken } from "../../global-store"; 6 | import Head from "next/head"; 7 | import { RootState, store } from "./store"; 8 | import { Provider, useDispatch, useSelector } from "react-redux"; 9 | import { addSession, getSessionList, removeSessionById, setSessionList, updateSession } from "./store/session-list"; 10 | 11 | /** 12 | * 会话页面 13 | * @returns 14 | */ 15 | export function Chat() { 16 | const { sessionList } = useSelector((state: RootState) => state.sessionList) 17 | const dispatch = useDispatch() 18 | const [configModalOpen, setConfigModalOpen] = useState(false); 19 | const [configModalMode, setConfigModalMode] = useState(ModalMode.Create); 20 | const [activeSessionId, setActiveSessionId] = useState(null); 21 | 22 | // 获取会话列表, 并设置当前会话为第一个会话 23 | useEffect(() => { 24 | const list = getSessionList(); 25 | dispatch(setSessionList(list)); 26 | if (list.length > 0) { 27 | setActiveSessionId(list[0].id); 28 | } 29 | }, []); 30 | 31 | // 创建新会话 32 | const createNewSession = (values: SessionConfig) => { 33 | dispatch(addSession(values)) 34 | // 设置当前会话为新创建的会话 35 | setActiveSessionId(values.id); 36 | }; 37 | 38 | // 编辑会话 39 | const editSession = (values: SessionConfig) => { 40 | dispatch(updateSession(values)) 41 | }; 42 | 43 | // 创建新会话按钮点击 44 | const onCreateBtnClick = () => { 45 | setConfigModalOpen(true); 46 | setConfigModalMode(ModalMode.Create); 47 | }; 48 | 49 | // 会话菜单被选中 50 | const onMenuSelect = (id: string) => { 51 | setActiveSessionId(id); 52 | }; 53 | 54 | // 会话菜单编辑 55 | const onMenuEdit = (id: string) => { 56 | setConfigModalOpen(true); 57 | setConfigModalMode(ModalMode.Edit); 58 | }; 59 | 60 | // 会话菜单删除 61 | const onMenuDelete = (id: string) => { 62 | dispatch(removeSessionById(id)) 63 | // 如果删除的是当前会话, 则设置当前会话为第一个会话 64 | if (id === activeSessionId) { 65 | setActiveSessionId(sessionList[0]?.id ?? null); 66 | } 67 | }; 68 | 69 | // 会话配置弹窗确定 70 | const onConfigModalOk = (values: SessionConfig) => { 71 | // 如果设置了openAI token, 则更新全局openAI token 72 | if (values.openAIToken) { 73 | setOpenAIToken(values.openAIToken); 74 | } 75 | 76 | // 关闭会话配置弹窗 77 | setConfigModalOpen(false); 78 | 79 | // 根据会话配置弹窗模式, 创建新会话或编辑会话 80 | if (configModalMode === ModalMode.Edit) { 81 | editSession(values); 82 | } else { 83 | createNewSession(values); 84 | } 85 | }; 86 | 87 | return ( 88 | <> 89 | 90 | 聊天助手(普通版) 91 | 92 |
93 | 101 | item.id === activeSessionId 104 | )} 105 | /> 106 |
107 | item.id === activeSessionId 111 | )} 112 | open={configModalOpen} 113 | onOk={onConfigModalOk} 114 | onCancel={() => setConfigModalOpen(false)} 115 | /> 116 | 117 | ); 118 | } 119 | 120 | export default function ChatWrapper() { 121 | return 122 | 123 | 124 | } 125 | -------------------------------------------------------------------------------- /apps/web/pages/chat/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Menu, MenuProps, Popconfirm } from "antd"; 2 | import { DeleteOutlined, EditOutlined } from "@ant-design/icons"; 3 | import { SessionConfig } from "./config-modal"; 4 | import { useState } from "react"; 5 | 6 | // 会话侧边栏属性 7 | interface SideBarProps { 8 | onCreateBtnClick: () => void; 9 | onMenuSelect: (id: string) => void; 10 | onMenuEdit: (id: string) => void; 11 | onMenuDelete: (id: string) => void; 12 | sessionList: SessionConfig[]; 13 | selectedKeys: string[]; 14 | } 15 | 16 | // 会话侧边栏 17 | type MenuItem = Required["items"][number]; 18 | 19 | /** 20 | * 获取菜单项 21 | */ 22 | function getItem( 23 | label: React.ReactNode, 24 | key?: React.Key | null, 25 | icon?: React.ReactNode, 26 | children?: MenuItem[], 27 | type?: "group" 28 | ): MenuItem { 29 | return { 30 | key, 31 | icon, 32 | children, 33 | label, 34 | type, 35 | } as MenuItem; 36 | } 37 | 38 | /** 39 | * 会话侧边栏 40 | * @param props 41 | * @returns 42 | */ 43 | export function SideBar(props: SideBarProps) { 44 | const { sessionList, onMenuEdit, onMenuDelete, selectedKeys } = props; 45 | return ( 46 |
47 | 50 | 55 | getItem( 56 |
57 |

58 | {session.sessionName} 59 |

60 |
61 | onMenuEdit(session.id)} 63 | className="text-white" 64 | /> 65 | onMenuDelete(session.id)} 69 | okText="确认" 70 | cancelText="取消" 71 | > 72 | 73 | 74 |
75 |
, 76 | session.id 77 | ) 78 | )} 79 | onSelect={(menuInfo) => { 80 | props.onMenuSelect(menuInfo.key); 81 | }} 82 | /> 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /apps/web/pages/chat/store/chat.ts: -------------------------------------------------------------------------------- 1 | import { storage } from "../../../global-store"; 2 | import { Message } from "../chat-panel"; 3 | 4 | const MESSAGE_LIST_KEY = "MESSAGE_LIST_KEY"; 5 | 6 | /** 7 | * 获取消息列表 8 | * @param sessionId 9 | * @returns 10 | */ 11 | export async function getMessageList(sessionId: string): Promise { 12 | return (storage.get(`${MESSAGE_LIST_KEY}_${sessionId}`) as Message[]) || []; 13 | } 14 | 15 | /** 16 | * 设置消息列表 17 | * @param sessionId 18 | * @param messageList 19 | */ 20 | export async function setMessageList( 21 | sessionId: string, 22 | messageList: Message[] 23 | ) { 24 | storage.set(`${MESSAGE_LIST_KEY}_${sessionId}`, messageList); 25 | } 26 | 27 | /** 28 | * 添加消息 29 | * @param sessionId 30 | * @param message 31 | */ 32 | export async function addMessage(sessionId: string, message: Message) { 33 | const messageList = await getMessageList(sessionId); 34 | const index = messageList.findIndex((item) => item.id === message.id); 35 | 36 | // 如果已经存在,则更新消息 37 | if (index !== -1) { 38 | messageList[index] = message; 39 | setMessageList(sessionId, messageList); 40 | return; 41 | } 42 | 43 | messageList.push(message); 44 | setMessageList(sessionId, messageList); 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/pages/chat/store/index.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import sessionListReducer from "./session-list"; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | sessionList: sessionListReducer, 7 | }, 8 | }); 9 | 10 | // Infer the `RootState` and `AppDispatch` types from the store itself 11 | export type RootState = ReturnType; 12 | // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} 13 | export type AppDispatch = typeof store.dispatch; 14 | -------------------------------------------------------------------------------- /apps/web/pages/chat/store/session-list.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import { SessionConfig } from "../config-modal"; 3 | 4 | const SESSION_LIST_KEY = "SESSION_LIST_KEY"; 5 | 6 | export interface SessionListState { 7 | sessionList: SessionConfig[]; 8 | } 9 | 10 | const initialState: SessionListState = { 11 | sessionList: [], 12 | }; 13 | 14 | export const sessionListSlice = createSlice({ 15 | name: "sessionList", 16 | initialState, 17 | reducers: { 18 | setSessionList: (state, action) => { 19 | state.sessionList = action.payload; 20 | localStorage.setItem(SESSION_LIST_KEY, JSON.stringify(state.sessionList)); 21 | }, 22 | 23 | addSession: (state, action) => { 24 | state.sessionList.push(action.payload); 25 | localStorage.setItem(SESSION_LIST_KEY, JSON.stringify(state.sessionList)); 26 | }, 27 | updateSession: (state, action) => { 28 | const index = state.sessionList.findIndex( 29 | (item) => item.id === action.payload.id 30 | ); 31 | if (index !== -1) { 32 | state.sessionList[index] = action.payload; 33 | } 34 | localStorage.setItem(SESSION_LIST_KEY, JSON.stringify(state.sessionList)); 35 | }, 36 | removeSessionById: (state, action) => { 37 | const index = state.sessionList.findIndex( 38 | (item) => item.id === action.payload 39 | ); 40 | if (index !== -1) { 41 | state.sessionList.splice(index, 1); 42 | } 43 | localStorage.setItem(SESSION_LIST_KEY, JSON.stringify(state.sessionList)); 44 | }, 45 | }, 46 | }); 47 | 48 | export const { setSessionList, addSession, updateSession, removeSessionById } = 49 | sessionListSlice.actions; 50 | 51 | export default sessionListSlice.reducer; 52 | 53 | /** 54 | * 获取会话列表 55 | * @returns 56 | */ 57 | export function getSessionList(): SessionConfig[] { 58 | const sessionList = localStorage.getItem(SESSION_LIST_KEY); 59 | try { 60 | return JSON.parse(sessionList || "[]"); 61 | } catch (e) { 62 | return []; 63 | } 64 | } 65 | 66 | /** 67 | * 根据id获取会话 68 | * @param id 69 | * @returns 70 | */ 71 | export function getSessionById(id: string): SessionConfig | undefined { 72 | const sessionList = getSessionList(); 73 | return sessionList.find((item) => item.id === id); 74 | } 75 | -------------------------------------------------------------------------------- /apps/web/pages/index.page.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | export default function Web() { 4 | return ( 5 | // https://transfonter.org/ 在线字体压缩 6 |
7 | 8 | AI实验室 9 | 10 |
17 |
18 |

AI 实验室

19 |

为AI赋能,让未来更美好

20 | 21 | 44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /apps/web/pages/translate.page.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from "antd"; 2 | import Head from "next/head"; 3 | import { useEffect, useState } from "react"; 4 | import { getOpenAIToken, setOpenAIToken } from "../global-store"; 5 | 6 | export default function Translate() { 7 | const [token, setToken] = useState(""); 8 | const [prompt, setPrompt] = useState( 9 | "Translate the following English text to Chinese, ensuring that the translation is accurate, preserves the original meaning, and uses natural, fluent language:" 10 | ); 11 | const [sourceText, setSourceText] = useState(""); 12 | const [targetText, setTargetText] = useState(""); 13 | 14 | useEffect(() => { 15 | setToken(getOpenAIToken() || ""); 16 | }, []); 17 | 18 | const translate = async () => { 19 | if (!token) { 20 | return alert("请输入Open AI的token!"); 21 | } 22 | 23 | if (!sourceText) { 24 | return alert("请输入要翻译的文本!"); 25 | } 26 | 27 | const response = await fetch("/api/translate", { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | }, 32 | body: JSON.stringify({ text: sourceText, token, prompt }), 33 | }); 34 | 35 | // 处理SSE的返回 36 | const streamReader = response.body!.getReader(); 37 | const utf8Decoder = new TextDecoder("utf-8"); 38 | let dataBuffer = ""; 39 | const readStream = async () => { 40 | const { done, value } = await streamReader.read(); 41 | 42 | if (done) { 43 | console.error("Stream closed"); 44 | return; 45 | } 46 | 47 | dataBuffer += utf8Decoder.decode(value); 48 | setTargetText(dataBuffer); 49 | const targetElement = document.getElementById("target") as HTMLElement; 50 | targetElement.scrollTop = targetElement?.scrollHeight; 51 | readStream(); 52 | }; 53 | 54 | readStream(); 55 | setOpenAIToken(token); 56 | }; 57 | 58 | return ( 59 |
60 | 61 | AI翻译 62 | 63 |

AI翻译

64 |
65 | { 68 | setToken(event.target.value); 69 | }} 70 | value={token} 71 | /> 72 | { 77 | setPrompt(event.target.value); 78 | }} 79 | /> 80 |
81 | 84 |
85 |
86 |
87 |
88 | { 91 | setSourceText(e.target.value); 92 | }} 93 | /> 94 |
95 |
96 | 97 |
98 |
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/apps/web/public/bg.png -------------------------------------------------------------------------------- /apps/web/public/subset-Kingsoft_Cloud_Font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/apps/web/public/subset-Kingsoft_Cloud_Font.woff -------------------------------------------------------------------------------- /apps/web/public/subset-Kingsoft_Cloud_Font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengjian-github/ailab/af4c51408900f2bfc65c21a5c95145eb437ed98a/apps/web/public/subset-Kingsoft_Cloud_Font.woff2 -------------------------------------------------------------------------------- /apps/web/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | padding: 0; 7 | margin: 0; 8 | } 9 | 10 | @font-face { 11 | font-family: "js"; 12 | src: url("/subset-Kingsoft_Cloud_Font.woff2") format("woff2"), 13 | url("/subset-Kingsoft_Cloud_Font.woff") format("woff"); 14 | font-weight: normal; 15 | font-style: normal; 16 | font-display: swap; 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", 5 | "./pages/**/*.{js,ts,jsx,tsx}", 6 | "./components/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | corePlugins: { 13 | preflight: false, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/nextjs.json", 3 | "include": [ 4 | "next-env.d.ts", 5 | "**/*.ts", 6 | "**/*.tsx", 7 | "pages/_app.page.jsge.js" 8 | ], 9 | "exclude": ["node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取URL中的参数 3 | * @param name 4 | * @returns 5 | */ 6 | export function getQueryParam(name?: string) { 7 | // 将查询字符串(包含 "?")截取并解析成对象 8 | const queryParamsString = window.location.search.substring(1); 9 | const queryParamsArr = queryParamsString.split("&"); 10 | 11 | // 遍历对象,将它们转换成 key-value 键值对形式 12 | const queryParamsObj: any = {}; 13 | for (let i = 0, len = queryParamsArr.length; i < len; i++) { 14 | const queryParam = queryParamsArr[i].split("="); 15 | const key = decodeURIComponent(queryParam[0]); 16 | const value = decodeURIComponent(queryParam[1] || ""); 17 | queryParamsObj[key] = value; 18 | } 19 | 20 | // 返回指定参数名的值 21 | if (name) { 22 | return queryParamsObj[name] || null; 23 | } 24 | return queryParamsObj; 25 | } 26 | 27 | /** 28 | * 判断是否是高级模式 29 | * @returns 30 | */ 31 | export function isAdvanced() { 32 | return getQueryParam("advanced") === "1"; 33 | } 34 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "ailab", 5 | script: "./apps/server/dist/main.js", 6 | env: { PORT: 443, NODE_ENV: "production" }, 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ailib", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build", 11 | "dev": "turbo run dev", 12 | "lint": "turbo run lint", 13 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 14 | }, 15 | "devDependencies": { 16 | "eslint-config-custom": "workspace:*", 17 | "prettier": "latest", 18 | "turbo": "latest" 19 | }, 20 | "engines": { 21 | "node": ">=14.0.0" 22 | }, 23 | "dependencies": {}, 24 | "packageManager": "pnpm@8.1.1" 25 | } -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "turbo", "prettier"], 3 | rules: { 4 | "@next/next/no-html-link-for-pages": "off", 5 | }, 6 | parserOptions: { 7 | babelOptions: { 8 | presets: [require.resolve("next/babel")], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eslint": "^7.23.0", 8 | "eslint-config-next": "13.0.0", 9 | "eslint-config-prettier": "^8.3.0", 10 | "eslint-plugin-react": "7.31.8", 11 | "eslint-config-turbo": "latest" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^4.7.4" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"] 8 | }, 9 | "lint": { 10 | "outputs": [] 11 | }, 12 | "dev": { 13 | "cache": false 14 | } 15 | } 16 | } 17 | --------------------------------------------------------------------------------