├── .commitlintrc.json ├── .dockerignore ├── .editorconfig ├── .env ├── .eslintrc.cjs ├── .gitattributes ├── .github └── workflows │ ├── build_docker.yml │ └── ci.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.en.md ├── CONTRIBUTING.md ├── Dockerfile ├── README.en.md ├── README.md ├── config ├── index.ts └── proxy.ts ├── docker-compose ├── docker-compose.yml ├── nginx │ └── nginx.conf └── readme.md ├── docs ├── alipay.png ├── c1-2.8.0.png ├── c1.png ├── c2-2.8.0.png ├── c2.png ├── docker.png └── wechat.png ├── index.html ├── license ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.svg ├── pwa-192x192.png └── pwa-512x512.png ├── service ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .vscode │ ├── extensions.json │ └── settings.json ├── package.json ├── pnpm-lock.yaml ├── src │ ├── chatgpt │ │ └── index.ts │ ├── index.ts │ ├── middleware │ │ └── auth.ts │ ├── types.ts │ └── utils │ │ └── index.ts ├── tsconfig.json └── tsup.config.ts ├── src ├── App.vue ├── api │ └── index.ts ├── assets │ ├── avatar.jpg │ └── recommend.json ├── components │ ├── common │ │ ├── HoverButton │ │ │ ├── Button.vue │ │ │ └── index.vue │ │ ├── NaiveProvider │ │ │ └── index.vue │ │ ├── PromptStore │ │ │ └── index.vue │ │ ├── Setting │ │ │ ├── About.vue │ │ │ ├── General.vue │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ └── index.vue │ │ ├── UserAvatar │ │ │ └── index.vue │ │ └── index.ts │ └── custom │ │ ├── GithubSite.vue │ │ └── index.ts ├── hooks │ ├── useBasicLayout.ts │ ├── useIconRender.ts │ ├── useLanguage.ts │ └── useTheme.ts ├── icons │ ├── 403.vue │ ├── 404.svg │ └── 500.vue ├── locales │ ├── en-US.ts │ ├── index.ts │ ├── zh-CN.ts │ └── zh-TW.ts ├── main.ts ├── plugins │ ├── assets.ts │ └── index.ts ├── router │ ├── index.ts │ └── permission.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── helper.ts │ │ └── index.ts │ │ ├── auth │ │ ├── helper.ts │ │ └── index.ts │ │ ├── chat │ │ ├── helper.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── prompt │ │ ├── helper.ts │ │ └── index.ts │ │ └── user │ │ ├── helper.ts │ │ └── index.ts ├── styles │ ├── global.less │ └── lib │ │ ├── github-markdown.less │ │ ├── highlight.less │ │ └── tailwind.css ├── typings │ ├── chat.d.ts │ ├── env.d.ts │ └── global.d.ts ├── utils │ ├── crypto │ │ └── index.ts │ ├── format │ │ └── index.ts │ ├── functions │ │ └── index.ts │ ├── is │ │ └── index.ts │ ├── request │ │ ├── axios.ts │ │ └── index.ts │ └── storage │ │ ├── index.ts │ │ └── local.ts └── views │ ├── chat │ ├── components │ │ ├── Header │ │ │ └── index.vue │ │ ├── Message │ │ │ ├── Avatar.vue │ │ │ ├── Text.vue │ │ │ ├── index.vue │ │ │ └── style.less │ │ └── index.ts │ ├── hooks │ │ ├── useChat.ts │ │ ├── useCopyCode.ts │ │ ├── useScroll.ts │ │ └── useUsingContext.ts │ ├── index.vue │ └── layout │ │ ├── Layout.vue │ │ ├── Permission.vue │ │ ├── index.ts │ │ └── sider │ │ ├── Footer.vue │ │ ├── List.vue │ │ └── index.vue │ └── exception │ ├── 404 │ └── index.vue │ └── 500 │ └── index.vue ├── start.sh ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | */node_modules 3 | node_modules 4 | Dockerfile 5 | .* 6 | */.* 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Glob API URL 2 | VITE_GLOB_API_URL=/api 3 | 4 | VITE_APP_API_BASE_URL=http://localhost:3002/ 5 | 6 | # Whether long replies are supported, which may result in higher API fees 7 | VITE_GLOB_OPEN_LONG_REPLY=false 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@antfu'], 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.cjs" eol=lf 7 | "*.cts" eol=lf 8 | "*.mjs" eol=lf 9 | "*.mts" eol=lf 10 | "*.json" eol=lf 11 | "*.html" eol=lf 12 | "*.css" eol=lf 13 | "*.less" eol=lf 14 | "*.scss" eol=lf 15 | "*.sass" eol=lf 16 | "*.styl" eol=lf 17 | "*.md" eol=lf 18 | -------------------------------------------------------------------------------- /.github/workflows/build_docker.yml: -------------------------------------------------------------------------------- 1 | name: build_docker 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | release: 7 | types: [created] # 表示在创建新的 Release 时触发 8 | 9 | jobs: 10 | build_docker: 11 | name: Build docker 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - run: | 18 | echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)" 19 | echo 本次构建的版本为:${{ github.ref_name }} 20 | env 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v2 26 | - name: Login to DockerHub 27 | uses: docker/login-action@v2 28 | with: 29 | username: ${{ secrets.DOCKERHUB_USERNAME }} 30 | password: ${{ secrets.DOCKERHUB_TOKEN }} 31 | - name: Build and push 32 | id: docker_build 33 | uses: docker/build-push-action@v4 34 | with: 35 | context: . 36 | push: true 37 | labels: ${{ steps.meta.outputs.labels }} 38 | platforms: linux/amd64,linux/arm64 39 | tags: | 40 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }} 41 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest 42 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18.x 21 | 22 | - name: Setup 23 | run: npm i -g @antfu/ni 24 | 25 | - name: Install 26 | run: nci 27 | 28 | - name: Lint 29 | run: nr lint:fix 30 | 31 | typecheck: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Set node 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 18.x 39 | 40 | - name: Setup 41 | run: npm i -g @antfu/ni 42 | 43 | - name: Install 44 | run: nci 45 | 46 | - name: Typecheck 47 | run: nr type-check 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | package-lock.json 31 | 32 | # Environment variables files 33 | /service/.env 34 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "javascriptreact", 10 | "typescript", 11 | "typescriptreact", 12 | "vue", 13 | "html", 14 | "json", 15 | "jsonc", 16 | "json5", 17 | "yaml", 18 | "yml", 19 | "markdown" 20 | ], 21 | "cSpell.words": [ 22 | "antfu", 23 | "axios", 24 | "bumpp", 25 | "chatgpt", 26 | "chenzhaoyu", 27 | "commitlint", 28 | "davinci", 29 | "dockerhub", 30 | "esno", 31 | "GPTAPI", 32 | "highlightjs", 33 | "hljs", 34 | "iconify", 35 | "katex", 36 | "katexmath", 37 | "linkify", 38 | "logprobs", 39 | "mdhljs", 40 | "nodata", 41 | "OPENAI", 42 | "pinia", 43 | "Popconfirm", 44 | "rushstack", 45 | "Sider", 46 | "tailwindcss", 47 | "traptitech", 48 | "tsup", 49 | "Typecheck", 50 | "unplugin", 51 | "VITE", 52 | "vueuse", 53 | "Zhao" 54 | ], 55 | "i18n-ally.enabledParsers": [ 56 | "ts" 57 | ], 58 | "i18n-ally.sortKeys": true, 59 | "i18n-ally.keepFulfilled": true, 60 | "i18n-ally.localesPaths": [ 61 | "src/locales" 62 | ], 63 | "i18n-ally.keystyle": "nested" 64 | } 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.10.3 2 | 3 | `2023-03-10` 4 | 5 | > 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`,如果你觉得本项目有监控后门或有问题导致你的账号、API被封,那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。 6 | 7 | ## Feature 8 | - 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)] 9 | - 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)] 10 | 11 | ## Enhancement 12 | - 调整移动端按钮和优化布局 13 | - 调整 `iOS` 上安全距离 14 | - 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)] 15 | 16 | ## BugFix 17 | - 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)] 18 | - 修复设置文字过长时导致的设置按钮消失的问题 19 | 20 | ## Other 21 | - 更新依赖 22 | 23 | ## v2.10.2 24 | 25 | `2023-03-09` 26 | 27 | 衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1) 28 | 29 | ## Enhancement 30 | - 移动端下输入框获得焦点时左侧按钮隐藏 31 | 32 | ## BugFix 33 | - 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉 34 | - 回退 `2.10.1` 中前端变量影响 `Docker` 打包 35 | 36 | ## v2.10.1 37 | 38 | `2023-03-09` 39 | 40 | 注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下: 41 | 42 | - 在项目中添加 `.env` 从一开始就是个错误的示范 43 | - 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰 44 | - 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改 45 | 46 | 47 | 这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。 48 | 49 | ## Feature 50 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能 51 | 52 | ## Enhancement 53 | - 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题 54 | - 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL` 55 | - 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题 56 | 57 | ## BugFix 58 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题 59 | - 修复深色模式导出图片的样式问题 60 | 61 | 62 | ## v2.10.0 63 | 64 | `2023-03-07` 65 | 66 | - 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。 67 | - 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。 68 | - 演示图片请看最后 69 | 70 | ## Feature 71 | - 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码 72 | - 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译 73 | - 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能 74 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能 75 | 76 | 77 | ## Enhancement 78 | - 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息 79 | - 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性 80 | - 优化部分代码 81 | 82 | ## BugFix 83 | - 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充) 84 | 85 | ## Other 86 | - 更新依赖到最新 87 | 88 | ## 演示 89 | > 不是界面最新效果,有美化改动 90 | 91 | 权限 92 | 93 | ![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png) 94 | 95 | 聊天记录导出 96 | 97 | ![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif) 98 | 99 | 保存图片到本地 100 | 101 | ![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif) 102 | 103 | ## v2.9.3 104 | 105 | `2023-03-06` 106 | 107 | ## Enhancement 108 | - 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题 109 | - 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档 110 | - 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译 111 | 112 | ## BugFix 113 | - 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题 114 | - 修复复制代码的兼容性报错问题 115 | - 修复部分优化小问题 116 | 117 | ## v2.9.2 118 | 119 | `2023-03-04` 120 | 121 | 手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。 122 | 123 | ### Feature 124 | - 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持 125 | - 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持 126 | - 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249] 127 | ## Enhancement 128 | - 优化在高分屏上主题内容的最大宽度[#257] 129 | - 现在文字按单词截断[#215][#225] 130 | ### BugFix 131 | - 修复动态生成时代码块不能被复制的问题[#251][#260] 132 | - 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256] 133 | - 修复控制台渲染警告 134 | ## Other 135 | - 更新依赖至最新 136 | - 修改 `README` 内容 137 | 138 | ## v2.9.1 139 | 140 | `2023-03-02` 141 | 142 | ### Feature 143 | - 代码块添加当前代码语言显示和复制功能[#197][#196] 144 | - 完善多语言,现在可以切换中英文显示 145 | 146 | ## Enhancement 147 | - 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档 148 | 149 | ### BugFix 150 | - 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题 151 | ## Other 152 | - 更新依赖至最新 153 | - 修改 `README` 内容 154 | ## v2.9.0 155 | 156 | `2023-03-02` 157 | 158 | ### Feature 159 | - 现在能复制带格式的消息文本 160 | - 新设计的设定页面,可以自定义姓名、描述、头像(链接方式) 161 | - 新增`403`和`404`页面以便扩展 162 | 163 | ## Enhancement 164 | - 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认) 165 | - 取消了前端超时限制设定 166 | 167 | ## v2.8.3 168 | 169 | `2023-03-01` 170 | 171 | ### Feature 172 | - 消息已输出内容不会因为中断而消失[#167] 173 | - 添加复制消息按钮[#133] 174 | 175 | ### Other 176 | - `README` 添加声明内容 177 | 178 | ## v2.8.2 179 | 180 | `2023-02-28` 181 | ### Enhancement 182 | - 代码主题调整为 `One Dark - light|dark` 适配深色模式 183 | ### BugFix 184 | - 修复普通文本代码渲染和深色模式下的问题[#139][#154] 185 | 186 | ## v2.8.1 187 | 188 | `2023-02-27` 189 | 190 | ### BugFix 191 | - 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146] 192 | 193 | ## v2.8.0 194 | 195 | `2023-02-27` 196 | 197 | - 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容) 198 | 199 | - 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。 200 | 201 | - 建议更新到此版本获得比较完整的体验 202 | 203 | ### Enhancement 204 | - 优化了部份代码和类型提示 205 | - 输入框添加换行提示 206 | - 移动端输入框现在回车为换行,而不是直接提交 207 | - 移动端双击标题返回顶部,箭头返回底部 208 | 209 | ### BugFix 210 | - 流输出数据下的问题[#122] 211 | - 修复了 `API Key` 下部份代码不换行的问题 212 | - 修复移动端深色模式部份样式问题[#123][#126] 213 | - 修复主题模式图标不一致的问题[#126] 214 | 215 | ## v2.7.3 216 | 217 | `2023-02-25` 218 | 219 | ### Feature 220 | - 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103) 221 | ### BugFix 222 | - 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117) 223 | 224 | ## v2.7.2 225 | 226 | `2023-02-24` 227 | ### Enhancement 228 | - 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法 229 | - 移除测试无用函数 230 | 231 | ## v2.7.1 232 | 233 | `2023-02-23` 234 | 235 | 因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式 236 | 237 | ### Feature 238 | - 现在可以中断请求过长没有答复的消息 239 | - 现在可以删除单条消息 240 | - 设置中显示当前版本信息 241 | 242 | ### BugFix 243 | - 回退 `2.7.0` 的消息不稳定的问题 244 | 245 | ## v2.7.0 246 | 247 | `2023-02-23` 248 | 249 | ### Feature 250 | - 使用消息流返回信息,反应更迅速 251 | 252 | ### Enhancement 253 | - 样式的一点小改动 254 | 255 | ## v2.6.2 256 | 257 | `2023-02-22` 258 | ### BugFix 259 | - 还原修改代理导致的异常问题 260 | 261 | ## v2.6.1 262 | 263 | `2023-02-22` 264 | 265 | ### Feature 266 | - 新增 `Railway` 部署模版 267 | 268 | ### BugFix 269 | - 手动打包 `Proxy` 问题 270 | 271 | ## v2.6.0 272 | 273 | `2023-02-21` 274 | ### Feature 275 | - 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51) 276 | - 前端页面设置按钮显示查看当前后端服务配置 277 | 278 | ### Enhancement 279 | - 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62) 280 | 281 | ## v2.5.2 282 | 283 | `2023-02-21` 284 | ### Feature 285 | - 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77) 286 | ### BugFix 287 | - 重载会话时滚动条保持 288 | 289 | ## v2.5.1 290 | 291 | `2023-02-21` 292 | 293 | ### Enhancement 294 | - 调整路由模式为 `hash` 295 | - 调整新增会话添加到 296 | - 调整移动端样式 297 | 298 | 299 | ## v2.5.0 300 | 301 | `2023-02-20` 302 | 303 | ### Feature 304 | - 会话 `loading` 现在显示为光标动画 305 | - 会话现在可以再次生成回复 306 | - 会话异常可以再次进行请求 307 | - 所有删除选项添加确认操作 308 | 309 | ### Enhancement 310 | - 调整 `chat` 为路由页面而不是组件形式 311 | - 更新依赖至最新 312 | - 调整移动端体验 313 | 314 | ### BugFix 315 | - 修复移动端左侧菜单显示不完整的问题 316 | 317 | ## v2.4.1 318 | 319 | `2023-02-18` 320 | 321 | ### Enhancement 322 | - 调整部份移动端上的样式 323 | - 输入框支持换行 324 | 325 | ## v2.4.0 326 | 327 | `2023-02-17` 328 | 329 | ### Feature 330 | - 响应式支持移动端 331 | ### Enhancement 332 | - 修改部份描述错误 333 | 334 | ## v2.3.3 335 | 336 | `2023-02-16` 337 | 338 | ### Feature 339 | - 添加 `README` 部份说明和贡献列表 340 | - 添加 `docker` 镜像 341 | - 添加 `GitHub Action` 自动化构建 342 | 343 | ### BugFix 344 | - 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896) 345 | 346 | ## v2.3.2 347 | 348 | `2023-02-16` 349 | 350 | ### Enhancement 351 | - 更新依赖至最新 352 | - 优化部份内容 353 | 354 | ## v2.3.1 355 | 356 | `2023-02-15` 357 | 358 | ### BugFix 359 | - 修复多会话状态下一些意想不到的问题 360 | 361 | ## v2.3.0 362 | 363 | `2023-02-15` 364 | ### Feature 365 | - 代码类型信息高亮显示 366 | - 支持 `node ^16` 版本 367 | - 移动端响应式初步支持 368 | - `vite` 中 `proxy` 代理 369 | 370 | ### Enhancement 371 | - 调整超时处理范围 372 | 373 | ### BugFix 374 | - 修复取消请求错误提示会添加到信息中 375 | - 修复部份情况下提交请求不可用 376 | - 修复侧边栏宽度变化闪烁的问题 377 | 378 | ## v2.2.0 379 | 380 | `2023-02-14` 381 | ### Feature 382 | - 会话和上下文本地储存 383 | - 侧边栏本地储存 384 | 385 | ## v2.1.0 386 | 387 | `2023-02-14` 388 | ### Enhancement 389 | - 更新依赖至最新 390 | - 联想功能移动至前端提交,后端只做转发 391 | 392 | ### BugFix 393 | - 修复部份项目检测有关 `Bug` 394 | - 修复清除上下文按钮失效 395 | 396 | ## v2.0.0 397 | 398 | `2023-02-13` 399 | ### Refactor 400 | 重构并优化大部分内容 401 | 402 | ## v1.0.5 403 | 404 | `2023-02-12` 405 | 406 | ### Enhancement 407 | - 输入框焦点,连续提交 408 | 409 | ### BugFix 410 | - 修复信息框样式问题 411 | - 修复中文输入法提交问题 412 | 413 | ## v1.0.4 414 | 415 | `2023-02-11` 416 | 417 | ### Feature 418 | - 支持上下文联想 419 | 420 | ## v1.0.3 421 | 422 | `2023-02-11` 423 | 424 | ### Enhancement 425 | - 拆分 `service` 文件以便扩展 426 | - 调整 `Eslint` 相关验证 427 | 428 | ### BugFix 429 | - 修复部份控制台报错 430 | 431 | ## v1.0.2 432 | 433 | `2023-02-10` 434 | 435 | ### BugFix 436 | - 修复新增信息容器不会自动滚动到问题 437 | - 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1) 438 | -------------------------------------------------------------------------------- /CONTRIBUTING.en.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | Thank you for your valuable time. Your contributions will make this project better! Before submitting a contribution, please take some time to read the getting started guide below. 3 | 4 | ## Semantic Versioning 5 | This project follows semantic versioning. We release patch versions for important bug fixes, minor versions for new features or non-important changes, and major versions for significant and incompatible changes. 6 | 7 | Each major change will be recorded in the `changelog`. 8 | 9 | ## Submitting Pull Request 10 | 1. Fork [this repository](https://github.com/Chanzhaoyu/chatgpt-web) and create a branch from `main`. For new feature implementations, submit a pull request to the `feature` branch. For other changes, submit to the `main` branch. 11 | 2. Install the `pnpm` tool using `npm install pnpm -g`. 12 | 3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`. 13 | 4. Execute `pnpm bootstrap` in the root directory. 14 | 5. Execute `pnpm install` in the `/service/` directory. 15 | 6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done. 16 | 7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check. 17 | 8. Execute `pnpm type-check` in the root directory to perform a type check. 18 | 9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines). 19 | 10. Submit a `pull request`. If there is a corresponding `issue`, please link it using the [linking-a-pull-request-to-an-issue keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). 20 | 21 | ## Commit Guidelines 22 | 23 | Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | [optional scope]: 27 | 28 | [optional body] 29 | 30 | [optional footer] 31 | ``` 32 | 33 | ### Commit Types 34 | 35 | The following is a list of commit types: 36 | 37 | - feat: New feature or functionality 38 | - fix: Bug fix 39 | - docs: Documentation update 40 | - style: Code style or component style update 41 | - refactor: Code refactoring, no new features or bug fixes introduced 42 | - perf: Performance optimization 43 | - test: Unit test 44 | - chore: Other commits that do not modify src or test files 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。 3 | 4 | ## 语义化版本 5 | 该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。 6 | 7 | 每个重大更改都将记录在 `changelog` 中。 8 | 9 | ## 提交 Pull Request 10 | 1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 11 | 2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。 12 | 3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。 13 | 4. 根目录下执行 `pnpm bootstrap`。 14 | 5. `/service/` 目录下执行 `pnpm install`。 15 | 6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。 16 | 7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。 17 | 8. 请在根目录下执行 `pnpm type-check` 进行类型检查。 18 | 9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南) 19 | 10. 提交 `pull request`, 如果有对应的 `issue`,请进行[关联](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)。 20 | 21 | ## Commit 指南 22 | 23 | Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | <类型>[可选 范围]: <描述> 27 | 28 | [可选 正文] 29 | 30 | [可选 脚注] 31 | ``` 32 | 33 | ### Commit 类型 34 | 35 | 以下是 commit 类型列表: 36 | 37 | - feat: 新特性或功能 38 | - fix: 缺陷修复 39 | - docs: 文档更新 40 | - style: 代码风格或者组件样式更新 41 | - refactor: 代码重构,不引入新功能和缺陷修复 42 | - perf: 性能优化 43 | - test: 单元测试 44 | - chore: 其他不修改 src 或测试文件的提交 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build front-end 2 | FROM node:lts-alpine AS builder 3 | 4 | COPY ./ /app 5 | WORKDIR /app 6 | 7 | RUN apk add --no-cache git \ 8 | && npm install pnpm -g \ 9 | && pnpm install \ 10 | && pnpm run build \ 11 | && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* 12 | 13 | # service 14 | FROM node:lts-alpine 15 | 16 | COPY /service /app 17 | COPY --from=builder /app/dist /app/public 18 | 19 | WORKDIR /app 20 | RUN apk add --no-cache git \ 21 | && npm install pnpm -g \ 22 | && pnpm install --only=production \ 23 | && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* 24 | 25 | 26 | EXPOSE 3002 27 | 28 | CMD ["pnpm", "run", "start"] 29 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 |
4 | 中文 | 5 | English 6 |
7 |
8 | 9 | > Disclaimer: This project is only released on GitHub, under the MIT License, free and for open-source learning purposes. There will be no account selling, paid services, discussion groups, or forums. Beware of fraud. 10 | 11 | ![cover](./docs/c1.png) 12 | ![cover2](./docs/c2.png) 13 | 14 | - [ChatGPT Web](#chatgpt-web) 15 | - [Introduction](#introduction) 16 | - [Roadmap](#roadmap) 17 | - [Prerequisites](#prerequisites) 18 | - [Node](#node) 19 | - [PNPM](#pnpm) 20 | - [Fill in the Keys](#fill-in-the-keys) 21 | - [Install Dependencies](#install-dependencies) 22 | - [Backend](#backend) 23 | - [Frontend](#frontend) 24 | - [Run in Test Environment](#run-in-test-environment) 25 | - [Backend Service](#backend-service) 26 | - [Frontend Webpage](#frontend-webpage) 27 | - [Packaging](#packaging) 28 | - [Using Docker](#using-docker) 29 | - [Docker Parameter Example](#docker-parameter-example) 30 | - [Docker Build \& Run](#docker-build--run) 31 | - [Docker Compose](#docker-compose) 32 | - [Deployment with Railway](#deployment-with-railway) 33 | - [Railway Environment Variables](#railway-environment-variables) 34 | - [Manual packaging](#manual-packaging) 35 | - [Backend service](#backend-service-1) 36 | - [Frontend webpage](#frontend-webpage-1) 37 | - [Frequently Asked Questions](#frequently-asked-questions) 38 | - [Contributing](#contributing) 39 | - [Sponsorship](#sponsorship) 40 | - [License](#license) 41 | 42 | ## Introduction 43 | 44 | Supports dual models, provides two unofficial `ChatGPT API` methods: 45 | 46 | | Method | Free? | Reliability | Quality | 47 | | --------------------------------------------- | ------ | ----------- | ------- | 48 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | No | Reliable | Relatively clumsy | 49 | | `ChatGPTUnofficialProxyAPI(Web accessToken)` | Yes | Relatively unreliable | Smart | 50 | 51 | Comparison: 52 | 1. `ChatGPTAPI` uses `gpt-3.5-turbo-0301` to simulate `ChatGPT` through the official `OpenAI` completion `API` (the most reliable method, but it is not free and does not use models specifically tuned for chat). 53 | 2. `ChatGPTUnofficialProxyAPI` accesses `ChatGPT`'s backend `API` via an unofficial proxy server to bypass `Cloudflare` (uses the real `ChatGPT`, is very lightweight, but depends on third-party servers and has rate limits). 54 | 55 | [Details](https://github.com/Chanzhaoyu/chatgpt-web/issues/138) 56 | 57 | Switching Methods: 58 | 1. Go to the `service/.env.example` file and copy the contents to the `service/.env` file. 59 | 2. For `OpenAI API Key`, fill in the `OPENAI_API_KEY` field [(Get apiKey)](https://platform.openai.com/overview). 60 | 3. For `Web API`, fill in the `OPENAI_ACCESS_TOKEN` field [(Get accessToken)](https://chat.openai.com/api/auth/session). 61 | 4. When both are present, `OpenAI API Key` takes precedence. 62 | 63 | Reverse Proxy: 64 | 65 | Available when using `ChatGPTUnofficialProxyAPI`.[Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) 66 | 67 | ```shell 68 | # service/.env 69 | API_REVERSE_PROXY= 70 | ``` 71 | 72 | Environment Variables: 73 | 74 | For all parameter variables, check [here](#docker-parameter-example) or see: 75 | 76 | ``` 77 | /service/.env 78 | ``` 79 | 80 | ## Roadmap 81 | [✓] Dual models 82 | 83 | [✓] Multiple session storage and context logic 84 | 85 | [✓] Formatting and beautifying code-like message types 86 | 87 | [✓] Access rights control 88 | 89 | [✓] Data import and export 90 | 91 | [✓] Save message to local image 92 | 93 | [✓] Multilingual interface 94 | 95 | [✓] Interface themes 96 | 97 | [✗] More... 98 | 99 | ## Prerequisites 100 | 101 | ### Node 102 | 103 | `node` requires version `^16 || ^18` (`node >= 14` requires installation of [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)), and multiple local `node` versions can be managed using [nvm](https://github.com/nvm-sh/nvm). 104 | 105 | ```shell 106 | node -v 107 | ``` 108 | 109 | ### PNPM 110 | If you have not installed `pnpm` before: 111 | ```shell 112 | npm install pnpm -g 113 | ``` 114 | 115 | ### Fill in the Keys 116 | 117 | Get `Openai Api Key` or `accessToken` and fill in the local environment variables [jump](#introduction) 118 | 119 | ``` 120 | # service/.env file 121 | 122 | # OpenAI API Key - https://platform.openai.com/overview 123 | OPENAI_API_KEY= 124 | 125 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 126 | OPENAI_ACCESS_TOKEN= 127 | ``` 128 | 129 | ## Install Dependencies 130 | 131 | > To make it easier for `backend developers` to understand, we did not use the front-end `workspace` mode, but stored it in different folders. If you only need to do secondary development of the front-end page, delete the `service` folder. 132 | 133 | ### Backend 134 | 135 | Enter the `/service` folder and run the following command 136 | 137 | ```shell 138 | pnpm install 139 | ``` 140 | 141 | ### Frontend 142 | Run the following command in the root directory 143 | ```shell 144 | pnpm bootstrap 145 | ``` 146 | 147 | ## Run in Test Environment 148 | ### Backend Service 149 | 150 | Enter the `/service` folder and run the following command 151 | 152 | ```shell 153 | pnpm start 154 | ``` 155 | 156 | ### Frontend Webpage 157 | Run the following command in the root directory 158 | ```shell 159 | pnpm dev 160 | ``` 161 | 162 | ## Packaging 163 | 164 | ### Using Docker 165 | 166 | #### Docker Parameter Example 167 | 168 | - `OPENAI_API_KEY` one of two 169 | - `OPENAI_ACCESS_TOKEN` one of two, `OPENAI_API_KEY` takes precedence when both are present 170 | - `OPENAI_API_BASE_URL` optional, available when `OPENAI_API_KEY` is set 171 | - `OPENAI_API_MODEL` optional, available when `OPENAI_API_KEY` is set 172 | - `API_REVERSE_PROXY` optional, available when `OPENAI_ACCESS_TOKEN` is set [Reference](#introduction) 173 | - `AUTH_SECRET_KEY` Access Password,optional 174 | - `TIMEOUT_MS` timeout, in milliseconds, optional 175 | - `SOCKS_PROXY_HOST` optional, effective with SOCKS_PROXY_PORT 176 | - `SOCKS_PROXY_PORT` optional, effective with SOCKS_PROXY_HOST 177 | 178 | ![docker](./docs/docker.png) 179 | 180 | #### Docker Build & Run 181 | 182 | ```bash 183 | docker build -t chatgpt-web . 184 | 185 | # foreground operation 186 | docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 187 | 188 | # background operation 189 | docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 190 | 191 | # running address 192 | http://localhost:3002/ 193 | ``` 194 | 195 | #### Docker Compose 196 | 197 | [Hub Address](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) 198 | 199 | ```yml 200 | version: '3' 201 | 202 | services: 203 | app: 204 | image: chenzhaoyu94/chatgpt-web # always use latest, pull the tag image again when updating 205 | ports: 206 | - 3002:3002 207 | environment: 208 | # one of two 209 | OPENAI_API_KEY: xxxxxx 210 | # one of two 211 | OPENAI_ACCESS_TOKEN: xxxxxx 212 | # api interface url, optional, available when OPENAI_API_KEY is set 213 | OPENAI_API_BASE_URL: xxxx 214 | # api model, optional, available when OPENAI_API_KEY is set 215 | OPENAI_API_MODEL: xxxx 216 | # reverse proxy, optional 217 | API_REVERSE_PROXY: xxx 218 | # access password,optional 219 | AUTH_SECRET_KEY: xxx 220 | # timeout, in milliseconds, optional 221 | TIMEOUT_MS: 60000 222 | # socks proxy, optional, effective with SOCKS_PROXY_PORT 223 | SOCKS_PROXY_HOST: xxxx 224 | # socks proxy port, optional, effective with SOCKS_PROXY_HOST 225 | SOCKS_PROXY_PORT: xxxx 226 | ``` 227 | The `OPENAI_API_BASE_URL` is optional and only used when setting the `OPENAI_API_KEY`. 228 | The `OPENAI_API_MODEL` is optional and only used when setting the `OPENAI_API_KEY`. 229 | 230 | ### Deployment with Railway 231 | 232 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 233 | 234 | #### Railway Environment Variables 235 | 236 | | Environment Variable | Required | Description | 237 | | -------------------- | -------- | ------------------------------------------------------------------------------------------------- | 238 | | `PORT` | Required | Default: `3002` | 239 | | `AUTH_SECRET_KEY` | Optional | access password | 240 | | `TIMEOUT_MS` | Optional | Timeout in milliseconds | 241 | | `OPENAI_API_KEY` | Optional | Required for `OpenAI API`. `apiKey` can be obtained from [here](https://platform.openai.com/overview). | 242 | | `OPENAI_ACCESS_TOKEN`| Optional | Required for `Web API`. `accessToken` can be obtained from [here](https://chat.openai.com/api/auth/session).| 243 | | `OPENAI_API_BASE_URL` | Optional, only for `OpenAI API` | API endpoint. | 244 | | `OPENAI_API_MODEL` | Optional, only for `OpenAI API` | API model. | 245 | | `API_REVERSE_PROXY` | Optional, only for `Web API` | Reverse proxy address for `Web API`. [Details](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 246 | | `SOCKS_PROXY_HOST` | Optional, effective with `SOCKS_PROXY_PORT` | Socks proxy. | 247 | | `SOCKS_PROXY_PORT` | Optional, effective with `SOCKS_PROXY_HOST` | Socks proxy port. | 248 | 249 | > Note: Changing environment variables in Railway will cause re-deployment. 250 | 251 | ### Manual packaging 252 | 253 | #### Backend service 254 | 255 | > If you don't need the `node` interface of this project, you can skip the following steps. 256 | 257 | Copy the `service` folder to a server that has a `node` service environment. 258 | 259 | ```shell 260 | # Install 261 | pnpm install 262 | 263 | # Build 264 | pnpm build 265 | 266 | # Run 267 | pnpm prod 268 | ``` 269 | 270 | PS: You can also run `pnpm start` directly on the server without packaging. 271 | 272 | #### Frontend webpage 273 | 274 | 1. Refer to the root directory `.env.example` file content to create `.env` file, modify `VITE_APP_API_BASE_URL` in `.env` at the root directory to your actual backend interface address. 275 | 2. Run the following command in the root directory and then copy the files in the `dist` folder to the root directory of your website service. 276 | 277 | [Reference information](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) 278 | 279 | ```shell 280 | pnpm build 281 | ``` 282 | 283 | ## Frequently Asked Questions 284 | 285 | Q: Why does Git always report an error when committing? 286 | 287 | A: Because there is submission information verification, please follow the [Commit Guidelines](./CONTRIBUTING.en.md). 288 | 289 | Q: Where to change the request interface if only the frontend page is used? 290 | 291 | A: The `VITE_GLOB_API_URL` field in the `.env` file at the root directory. 292 | 293 | Q: All red when saving the file? 294 | 295 | A: For `vscode`, please install the recommended plug-in of the project or manually install the `Eslint` plug-in. 296 | 297 | Q: Why doesn't the frontend have a typewriter effect? 298 | 299 | A: One possible reason is that after Nginx reverse proxying, buffering is turned on, and Nginx will try to buffer a certain amount of data from the backend before sending it to the browser. Please try adding `proxy_buffering off;` after the reverse proxy parameter and then reloading Nginx. Other web server configurations are similar. 300 | 301 | ## Contributing 302 | 303 | Please read the [Contributing Guidelines](./CONTRIBUTING.en.md) before contributing. 304 | 305 | Thanks to all the contributors! 306 | 307 | 308 | 309 | 310 | 311 | ## Sponsorship 312 | 313 | If you find this project helpful and circumstances permit, you can give me a little support. Thank you very much for your support~ 314 | 315 |
316 |
317 | WeChat 318 |

WeChat Pay

319 |
320 |
321 | Alipay 322 |

Alipay

323 |
324 |
325 | 326 | ## License 327 | MIT © [ChenZhaoYu](./license) 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT Web 2 | 3 |
4 | 中文 | 5 | English 6 | [DEMO]国内服务器镜像(仅境内可访问) 7 | [DEMO]cloudflare镜像(境内境外都可访问) 8 |
9 |
10 | 11 | > 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。 12 | 13 | ![cover](./docs/c1.png) 14 | ![cover2](./docs/c2.png) 15 | 16 | - [ChatGPT Web](#chatgpt-web) 17 | - [介绍](#介绍) 18 | - [待实现路线](#待实现路线) 19 | - [前置要求](#前置要求) 20 | - [Node](#node) 21 | - [PNPM](#pnpm) 22 | - [填写密钥](#填写密钥) 23 | - [安装依赖](#安装依赖) 24 | - [后端](#后端) 25 | - [前端](#前端) 26 | - [测试环境运行](#测试环境运行) 27 | - [后端服务](#后端服务) 28 | - [前端网页](#前端网页) 29 | - [打包](#打包) 30 | - [使用 Docker](#使用-docker) 31 | - [Docker 参数示例](#docker-参数示例) 32 | - [Docker build \& Run](#docker-build--run) 33 | - [Docker compose](#docker-compose) 34 | - [使用 Railway 部署](#使用-railway-部署) 35 | - [Railway 环境变量](#railway-环境变量) 36 | - [手动打包](#手动打包) 37 | - [后端服务](#后端服务-1) 38 | - [前端网页](#前端网页-1) 39 | - [常见问题](#常见问题) 40 | - [参与贡献](#参与贡献) 41 | - [赞助](#赞助) 42 | - [License](#license) 43 | ## 介绍 44 | 45 | 支持双模型,提供了两种非官方 `ChatGPT API` 方法 46 | 47 | | 方式 | 免费? | 可靠性 | 质量 | 48 | | --------------------------------------------- | ------ | ---------- | ---- | 49 | | `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 | 50 | | `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 | 51 | 52 | 对比: 53 | 1. `ChatGPTAPI` 使用 `gpt-3.5-turbo-0301` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型) 54 | 2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(使用真实的的`ChatGPT`,非常轻量级,但依赖于第三方服务器,并且有速率限制) 55 | 56 | 警告: 57 | 1. 你应该使用 `API` 方式并自建代理使你使用的风险降到最低。 58 | 2. 使用 `accessToken` 方式时反向代理将向第三方暴露您的访问令牌。这样做应该不会产生任何不良影响,但在使用这种方法之前请考虑风险,修改代理地址时也请使用公开的[社区方案](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy),不要不要不要使用来源不明的地址! 59 | 2. 因为国内 `API` 被墙,如果服务器不在国外,则需要代理才能请求到官方接口,也非常不建议使用别人发出来的代理地址,请自己搭建。 60 | 3. 人性是丑陋的,你永远不知道你相信的某些乐于分享的`好人`在用你的账号做什么!!! 61 | 62 | 注:强烈建议使用`ChatGPTAPI`,因为它使用 `OpenAI` 官方支持的`API`。并且可能会在将来的版本中删除对`ChatGPTUnofficialProxyAPI`的支持。 63 | 64 | 切换方式: 65 | 1. 进入 `service/.env.example` 文件,复制内容到 `service/.env` 文件 66 | 2. 使用 `OpenAI API Key` 请填写 `OPENAI_API_KEY` 字段 [(获取 apiKey)](https://platform.openai.com/overview) 67 | 3. 使用 `Web API` 请填写 `OPENAI_ACCESS_TOKEN` 字段 [(获取 accessToken)](https://chat.openai.com/api/auth/session) 68 | 4. 同时存在时以 `OpenAI API Key` 优先 69 | 70 | 反向代理: 71 | 72 | `ChatGPTUnofficialProxyAPI`时可用,[详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) 73 | 74 | ```shell 75 | # service/.env 76 | API_REVERSE_PROXY= 77 | ``` 78 | 79 | 环境变量: 80 | 81 | 全部参数变量请查看或[这里](#docker-参数示例) 82 | 83 | ``` 84 | /service/.env 85 | ``` 86 | 87 | ## 待实现路线 88 | [✓] 双模型 89 | 90 | [✓] 多会话储存和上下文逻辑 91 | 92 | [✓] 对代码等消息类型的格式化美化处理 93 | 94 | [✓] 访问权限控制 95 | 96 | [✓] 数据导入、导出 97 | 98 | [✓] 保存消息到本地图片 99 | 100 | [✓] 界面多语言 101 | 102 | [✓] 界面主题 103 | 104 | [✗] More... 105 | 106 | ## 前置要求 107 | 108 | ### Node 109 | 110 | `node` 需要 `^16 || ^18` 版本(`node >= 14` 需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本 111 | 112 | ```shell 113 | node -v 114 | ``` 115 | 116 | ### PNPM 117 | 如果你没有安装过 `pnpm` 118 | ```shell 119 | npm install pnpm -g 120 | ``` 121 | 122 | ### 填写密钥 123 | 获取 `Openai Api Key` 或 `accessToken` 并填写本地环境变量 [跳转](#介绍) 124 | 125 | ``` 126 | # service/.env 文件 127 | 128 | # OpenAI API Key - https://platform.openai.com/overview 129 | OPENAI_API_KEY= 130 | 131 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 132 | OPENAI_ACCESS_TOKEN= 133 | ``` 134 | 135 | ## 安装依赖 136 | 137 | > 为了简便 `后端开发人员` 的了解负担,所以并没有采用前端 `workspace` 模式,而是分文件夹存放。如果只需要前端页面做二次开发,删除 `service` 文件夹即可。 138 | 139 | ### 后端 140 | 141 | 进入文件夹 `/service` 运行以下命令 142 | 143 | ```shell 144 | pnpm install 145 | ``` 146 | 147 | ### 前端 148 | 根目录下运行以下命令 149 | ```shell 150 | pnpm bootstrap 151 | ``` 152 | 153 | ## 测试环境运行 154 | ### 后端服务 155 | 156 | 进入文件夹 `/service` 运行以下命令 157 | 158 | ```shell 159 | pnpm start 160 | ``` 161 | 162 | ### 前端网页 163 | 根目录下运行以下命令 164 | ```shell 165 | pnpm dev 166 | ``` 167 | 168 | ## 打包 169 | 170 | ### 使用 Docker 171 | 172 | #### Docker 参数示例 173 | 174 | - `OPENAI_API_KEY` 二选一 175 | - `OPENAI_ACCESS_TOKEN` 二选一,同时存在时,`OPENAI_API_KEY` 优先 176 | - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 177 | - `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 178 | - `API_REVERSE_PROXY` 可选,设置 `OPENAI_ACCESS_TOKEN` 时可用 [参考](#介绍) 179 | - `AUTH_SECRET_KEY` 访问权限密钥,可选 180 | - `TIMEOUT_MS` 超时,单位毫秒,可选 181 | - `SOCKS_PROXY_HOST` 可选,和 SOCKS_PROXY_PORT 一起时生效 182 | - `SOCKS_PROXY_PORT` 可选,和 SOCKS_PROXY_HOST 一起时生效 183 | 184 | ![docker](./docs/docker.png) 185 | 186 | #### Docker build & Run 187 | 188 | ```bash 189 | docker build -t chatgpt-web . 190 | 191 | # 前台运行 192 | docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 193 | 194 | # 后台运行 195 | docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web 196 | 197 | # 运行地址 198 | http://localhost:3002/ 199 | ``` 200 | 201 | #### Docker compose 202 | 203 | [Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general) 204 | 205 | ```yml 206 | version: '3' 207 | 208 | services: 209 | app: 210 | image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可 211 | ports: 212 | - 3002:3002 213 | environment: 214 | # 二选一 215 | OPENAI_API_KEY: xxxxxx 216 | # 二选一 217 | OPENAI_ACCESS_TOKEN: xxxxxx 218 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用 219 | OPENAI_API_BASE_URL: xxxx 220 | # API模型,可选,设置 OPENAI_API_KEY 时可用 221 | OPENAI_API_MODEL: xxxx 222 | # 反向代理,可选 223 | API_REVERSE_PROXY: xxx 224 | # 访问权限密钥,可选 225 | AUTH_SECRET_KEY: xxx 226 | # 超时,单位毫秒,可选 227 | TIMEOUT_MS: 60000 228 | # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 229 | SOCKS_PROXY_HOST: xxxx 230 | # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 231 | SOCKS_PROXY_PORT: xxxx 232 | ``` 233 | - `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用 234 | - `OPENAI_API_MODEL` 可选,设置 `OPENAI_API_KEY` 时可用 235 | ### 使用 Railway 部署 236 | 237 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/yytmgc) 238 | 239 | #### Railway 环境变量 240 | 241 | | 环境变量名称 | 必填 | 备注 | 242 | | --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- | 243 | | `PORT` | 必填 | 默认 `3002` 244 | | `AUTH_SECRET_KEY` | 可选 | 访问权限密钥 | 245 | | `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒 | 246 | | `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) | 247 | | `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) | 248 | | `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 | 249 | | `OPENAI_API_MODEL` | 可选,`OpenAI API` 时可用 | `API`模型 | 250 | | `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) | 251 | | `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 | 252 | | `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 | 253 | 254 | > 注意: `Railway` 修改环境变量会重新 `Deploy` 255 | 256 | ### 手动打包 257 | #### 后端服务 258 | > 如果你不需要本项目的 `node` 接口,可以省略如下操作 259 | 260 | 复制 `service` 文件夹到你有 `node` 服务环境的服务器上。 261 | 262 | ```shell 263 | # 安装 264 | pnpm install 265 | 266 | # 打包 267 | pnpm build 268 | 269 | # 运行 270 | pnpm prod 271 | ``` 272 | 273 | PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可 274 | 275 | #### 前端网页 276 | 277 | 1、修改根目录下 `.env` 文件中的 `VITE_APP_API_BASE_URL` 为你的实际后端接口地址 278 | 279 | 2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下 280 | 281 | [参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app) 282 | 283 | ```shell 284 | pnpm build 285 | ``` 286 | 287 | ## 常见问题 288 | Q: 为什么 `Git` 提交总是报错? 289 | 290 | A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md) 291 | 292 | Q: 如果只使用前端页面,在哪里改请求接口? 293 | 294 | A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。 295 | 296 | Q: 文件保存时全部爆红? 297 | 298 | A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。 299 | 300 | Q: 前端没有打字机效果? 301 | 302 | A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web server 配置同理。 303 | 304 | ## 参与贡献 305 | 306 | 贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md) 307 | 308 | 感谢所有做过贡献的人! 309 | 310 | 311 | 312 | 313 | 314 | ## 赞助 315 | 316 | 如果你觉得这个项目对你有帮助,并且情况允许的话,可以给我一点点支持,总之非常感谢支持~ 317 | 318 |
319 |
320 | 微信 321 |

WeChat Pay

322 |
323 |
324 | 支付宝 325 |

Alipay

326 |
327 |
328 | 329 | ## License 330 | MIT © [ChenZhaoYu](./license) 331 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy' 2 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite' 2 | 3 | export function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) { 4 | if (!isOpenProxy) 5 | return 6 | 7 | const proxy: Record = { 8 | '/api': { 9 | target: viteEnv.VITE_APP_API_BASE_URL, 10 | changeOrigin: true, 11 | rewrite: path => path.replace('/api/', '/'), 12 | }, 13 | } 14 | 15 | return proxy 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可 6 | ports: 7 | - 3002:3002 8 | environment: 9 | # 二选一 10 | OPENAI_API_KEY: xxxx 11 | # 二选一 12 | OPENAI_ACCESS_TOKEN: xxxxxx 13 | # API接口地址,可选,设置 OPENAI_API_KEY 时可用 14 | OPENAI_API_BASE_URL: xxxx 15 | # API模型,可选,设置 OPENAI_API_KEY 时可用 16 | OPENAI_API_MODEL: xxxx 17 | # 反向代理,可选 18 | API_REVERSE_PROXY: xxx 19 | # 访问权限密钥,可选 20 | AUTH_SECRET_KEY: xxx 21 | # 超时,单位毫秒,可选 22 | TIMEOUT_MS: 60000 23 | # Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效 24 | SOCKS_PROXY_HOST: xxxx 25 | # Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效 26 | SOCKS_PROXY_PORT: xxxx 27 | nginx: 28 | image: nginx:alpine 29 | ports: 30 | - '80:80' 31 | expose: 32 | - '80' 33 | volumes: 34 | - ./nginx/html:/usr/share/nginx/html 35 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 36 | links: 37 | - app 38 | -------------------------------------------------------------------------------- /docker-compose/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | charset utf-8; 5 | error_page 500 502 503 504 /50x.html; 6 | location / { 7 | root /usr/share/nginx/html; 8 | try_files $uri /index.html; 9 | } 10 | 11 | location /api { 12 | proxy_set_header X-Real-IP $remote_addr; #转发用户IP 13 | proxy_pass http://app:3002; 14 | } 15 | 16 | proxy_set_header Host $host; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header REMOTE-HOST $remote_addr; 19 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose/readme.md: -------------------------------------------------------------------------------- 1 | ### docker-compose 部署教程 2 | - 将打包好的前端文件放到 `nginx/html` 目录下 3 | - ```shell 4 | # 启动 5 | docker-compose up -d 6 | ``` 7 | - ```shell 8 | # 查看运行状态 9 | docker ps 10 | ``` 11 | - ```shell 12 | # 结束运行 13 | docker-compose down 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/alipay.png -------------------------------------------------------------------------------- /docs/c1-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/c1-2.8.0.png -------------------------------------------------------------------------------- /docs/c1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/c1.png -------------------------------------------------------------------------------- /docs/c2-2.8.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/c2-2.8.0.png -------------------------------------------------------------------------------- /docs/c2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/c2.png -------------------------------------------------------------------------------- /docs/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/docker.png -------------------------------------------------------------------------------- /docs/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/docs/wechat.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 仅用于开发学习交流 17 | 18 | 50 | 59 | 60 | 61 | 62 |
63 | 120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ChenZhaoYu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-web", 3 | "version": "2.10.3", 4 | "private": false, 5 | "description": "ChatGPT Web", 6 | "author": "ChenZhaoYu ", 7 | "keywords": [ 8 | "chatgpt-web", 9 | "chatgpt", 10 | "chatbot", 11 | "vue" 12 | ], 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "run-p type-check build-only", 16 | "preview": "vite preview", 17 | "build-only": "vite build", 18 | "type-check": "vue-tsc --noEmit", 19 | "lint": "eslint .", 20 | "lint:fix": "eslint . --fix", 21 | "bootstrap": "pnpm install && pnpm run common:prepare", 22 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml", 23 | "common:prepare": "husky install" 24 | }, 25 | "dependencies": { 26 | "@traptitech/markdown-it-katex": "^3.6.0", 27 | "@vueuse/core": "^9.13.0", 28 | "highlight.js": "^11.7.0", 29 | "html2canvas": "^1.4.1", 30 | "katex": "^0.16.4", 31 | "markdown-it": "^13.0.1", 32 | "naive-ui": "^2.34.3", 33 | "pinia": "^2.0.32", 34 | "vue": "^3.2.47", 35 | "vue-i18n": "^9.2.2", 36 | "vue-router": "^4.1.6" 37 | }, 38 | "devDependencies": { 39 | "@antfu/eslint-config": "^0.35.3", 40 | "@babel/types": "^7.21.2", 41 | "@commitlint/cli": "^17.4.4", 42 | "@commitlint/config-conventional": "^17.4.4", 43 | "@iconify/vue": "^4.1.0", 44 | "@types/crypto-js": "^4.1.1", 45 | "@types/katex": "^0.16.0", 46 | "@types/markdown-it": "^12.2.3", 47 | "@types/node": "^18.14.6", 48 | "@vitejs/plugin-vue": "^4.0.0", 49 | "autoprefixer": "^10.4.13", 50 | "axios": "^1.3.4", 51 | "crypto-js": "^4.1.1", 52 | "eslint": "^8.35.0", 53 | "husky": "^8.0.3", 54 | "less": "^4.1.3", 55 | "lint-staged": "^13.1.2", 56 | "npm-run-all": "^4.1.5", 57 | "postcss": "^8.4.21", 58 | "rimraf": "^4.2.0", 59 | "tailwindcss": "^3.2.7", 60 | "typescript": "~4.9.5", 61 | "vite": "^4.1.4", 62 | "vite-plugin-pwa": "^0.14.4", 63 | "vue-tsc": "^1.2.0" 64 | }, 65 | "lint-staged": { 66 | "*.{ts,tsx,vue}": [ 67 | "pnpm lint:fix" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/public/pwa-512x512.png -------------------------------------------------------------------------------- /service/.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key - https://platform.openai.com/overview 2 | OPENAI_API_KEY= 3 | 4 | # change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response 5 | OPENAI_ACCESS_TOKEN= 6 | 7 | # OpenAI API Base URL - https://api.openai.com 8 | OPENAI_API_BASE_URL= 9 | 10 | # OpenAI API Model - https://platform.openai.com/docs/models 11 | OPENAI_API_MODEL= 12 | 13 | # Reverse Proxy 14 | API_REVERSE_PROXY= 15 | 16 | # timeout 17 | TIMEOUT_MS=100000 18 | 19 | # Secret key 20 | AUTH_SECRET_KEY= 21 | 22 | # Socks Proxy Host 23 | SOCKS_PROXY_HOST= 24 | 25 | # Socks Proxy Port 26 | SOCKS_PROXY_PORT= 27 | -------------------------------------------------------------------------------- /service/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["build"], 4 | "extends": ["@antfu"] 5 | } 6 | -------------------------------------------------------------------------------- /service/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/extensions.json 24 | .idea 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | build 32 | -------------------------------------------------------------------------------- /service/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true 2 | -------------------------------------------------------------------------------- /service/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /service/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "typescript", 10 | "json", 11 | "jsonc", 12 | "json5", 13 | "yaml" 14 | ], 15 | "cSpell.words": [ 16 | "antfu", 17 | "chatgpt", 18 | "esno", 19 | "GPTAPI", 20 | "OPENAI" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-web-service", 3 | "version": "1.0.0", 4 | "private": false, 5 | "description": "ChatGPT Web Service", 6 | "author": "ChenZhaoYu ", 7 | "keywords": [ 8 | "chatgpt-web", 9 | "chatgpt", 10 | "chatbot", 11 | "express" 12 | ], 13 | "engines": { 14 | "node": "^16 || ^18" 15 | }, 16 | "scripts": { 17 | "start": "esno ./src/index.ts", 18 | "dev": "esno watch ./src/index.ts", 19 | "prod": "esno ./build/index.js", 20 | "build": "pnpm clean && tsup", 21 | "clean": "rimraf build", 22 | "lint": "eslint .", 23 | "lint:fix": "eslint . --fix", 24 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml" 25 | }, 26 | "dependencies": { 27 | "chatgpt": "^5.0.9", 28 | "dotenv": "^16.0.3", 29 | "esno": "^0.16.3", 30 | "express": "^4.18.2", 31 | "isomorphic-fetch": "^3.0.0", 32 | "node-fetch": "^3.3.0", 33 | "socks-proxy-agent": "^7.0.0" 34 | }, 35 | "devDependencies": { 36 | "@antfu/eslint-config": "^0.35.3", 37 | "@types/express": "^4.17.17", 38 | "@types/node": "^18.14.6", 39 | "eslint": "^8.35.0", 40 | "rimraf": "^4.3.0", 41 | "tsup": "^6.6.3", 42 | "typescript": "^4.9.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /service/src/chatgpt/index.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import 'isomorphic-fetch' 3 | import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt' 4 | import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt' 5 | import { SocksProxyAgent } from 'socks-proxy-agent' 6 | import fetch from 'node-fetch' 7 | import { sendResponse } from '../utils' 8 | import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types' 9 | 10 | const ErrorCodeMessage: Record = { 11 | 401: '[OpenAI] 提供错误的API密钥 | Incorrect API key provided', 12 | 403: '[OpenAI] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later', 13 | 502: '[OpenAI] 错误的网关 | Bad Gateway', 14 | 503: '[OpenAI] 服务器繁忙,请稍后再试 | Server is busy, please try again later', 15 | 504: '[OpenAI] 网关超时 | Gateway Time-out', 16 | 500: '[OpenAI] 服务器繁忙,请稍后再试 | Internal Server Error', 17 | } 18 | 19 | dotenv.config() 20 | 21 | const timeoutMs: number = !isNaN(+process.env.TIMEOUT_MS) ? +process.env.TIMEOUT_MS : 30 * 1000 22 | 23 | let apiModel: ApiModel 24 | 25 | if (!process.env.OPENAI_API_KEY && !process.env.OPENAI_ACCESS_TOKEN) 26 | throw new Error('Missing OPENAI_API_KEY or OPENAI_ACCESS_TOKEN environment variable') 27 | 28 | let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI 29 | 30 | (async () => { 31 | // More Info: https://github.com/transitive-bullshit/chatgpt-api 32 | 33 | if (process.env.OPENAI_API_KEY) { 34 | const OPENAI_API_MODEL = process.env.OPENAI_API_MODEL 35 | const model = (typeof OPENAI_API_MODEL === 'string' && OPENAI_API_MODEL.length > 0) 36 | ? OPENAI_API_MODEL 37 | : 'gpt-3.5-turbo' 38 | 39 | const options: ChatGPTAPIOptions = { 40 | apiKey: process.env.OPENAI_API_KEY, 41 | completionParams: { model }, 42 | debug: false, 43 | } 44 | 45 | if (process.env.OPENAI_API_BASE_URL && process.env.OPENAI_API_BASE_URL.trim().length > 0) 46 | options.apiBaseUrl = process.env.OPENAI_API_BASE_URL 47 | 48 | if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) { 49 | const agent = new SocksProxyAgent({ 50 | hostname: process.env.SOCKS_PROXY_HOST, 51 | port: process.env.SOCKS_PROXY_PORT, 52 | }) 53 | options.fetch = (url, options) => { 54 | return fetch(url, { agent, ...options }) 55 | } 56 | } 57 | 58 | api = new ChatGPTAPI({ ...options }) 59 | apiModel = 'ChatGPTAPI' 60 | } 61 | else { 62 | const options: ChatGPTUnofficialProxyAPIOptions = { 63 | accessToken: process.env.OPENAI_ACCESS_TOKEN, 64 | debug: false, 65 | } 66 | 67 | if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) { 68 | const agent = new SocksProxyAgent({ 69 | hostname: process.env.SOCKS_PROXY_HOST, 70 | port: process.env.SOCKS_PROXY_PORT, 71 | }) 72 | options.fetch = (url, options) => { 73 | return fetch(url, { agent, ...options }) 74 | } 75 | } 76 | 77 | if (process.env.API_REVERSE_PROXY) 78 | options.apiReverseProxyUrl = process.env.API_REVERSE_PROXY 79 | 80 | api = new ChatGPTUnofficialProxyAPI({ ...options }) 81 | apiModel = 'ChatGPTUnofficialProxyAPI' 82 | } 83 | })() 84 | 85 | async function chatReplyProcess( 86 | message: string, 87 | lastContext?: { conversationId?: string; parentMessageId?: string }, 88 | process?: (chat: ChatMessage) => void, 89 | ) { 90 | // if (!message) 91 | // return sendResponse({ type: 'Fail', message: 'Message is empty' }) 92 | 93 | try { 94 | let options: SendMessageOptions = { timeoutMs } 95 | 96 | if (lastContext) { 97 | if (apiModel === 'ChatGPTAPI') 98 | options = { parentMessageId: lastContext.parentMessageId } 99 | else 100 | options = { ...lastContext } 101 | } 102 | 103 | const response = await api.sendMessage(message, { 104 | ...options, 105 | onProgress: (partialResponse) => { 106 | process?.(partialResponse) 107 | }, 108 | }) 109 | 110 | return sendResponse({ type: 'Success', data: response }) 111 | } 112 | catch (error: any) { 113 | const code = error.statusCode 114 | global.console.log(error) 115 | if (Reflect.has(ErrorCodeMessage, code)) 116 | return sendResponse({ type: 'Fail', message: ErrorCodeMessage[code] }) 117 | return sendResponse({ type: 'Fail', message: error.message ?? 'Please check the back-end console' }) 118 | } 119 | } 120 | 121 | async function chatConfig() { 122 | return sendResponse({ 123 | type: 'Success', 124 | data: { 125 | apiModel, 126 | reverseProxy: process.env.API_REVERSE_PROXY, 127 | timeoutMs, 128 | socksProxy: (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) ? (`${process.env.SOCKS_PROXY_HOST}:${process.env.SOCKS_PROXY_PORT}`) : '-', 129 | } as ModelConfig, 130 | }) 131 | } 132 | 133 | export type { ChatContext, ChatMessage } 134 | 135 | export { chatReplyProcess, chatConfig } 136 | -------------------------------------------------------------------------------- /service/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { ChatContext, ChatMessage } from './chatgpt' 3 | import { chatConfig, chatReplyProcess } from './chatgpt' 4 | import { auth } from './middleware/auth' 5 | 6 | const app = express() 7 | const router = express.Router() 8 | 9 | app.use(express.static('public')) 10 | app.use(express.json()) 11 | 12 | app.all('*', (_, res, next) => { 13 | res.header('Access-Control-Allow-Origin', '*') 14 | res.header('Access-Control-Allow-Headers', 'Content-Type') 15 | res.header('Access-Control-Allow-Methods', '*') 16 | next() 17 | }) 18 | 19 | router.post('/chat-process', auth, async (req, res) => { 20 | res.setHeader('Content-type', 'application/octet-stream') 21 | 22 | try { 23 | const { prompt, options = {} } = req.body as { prompt: string; options?: ChatContext } 24 | let firstChunk = true 25 | await chatReplyProcess(prompt, options, (chat: ChatMessage) => { 26 | res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`) 27 | firstChunk = false 28 | }) 29 | } 30 | catch (error) { 31 | res.write(JSON.stringify(error)) 32 | } 33 | finally { 34 | res.end() 35 | } 36 | }) 37 | 38 | router.post('/config', async (req, res) => { 39 | try { 40 | const response = await chatConfig() 41 | res.send(response) 42 | } 43 | catch (error) { 44 | res.send(error) 45 | } 46 | }) 47 | 48 | router.post('/session', async (req, res) => { 49 | try { 50 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY 51 | const hasAuth = typeof AUTH_SECRET_KEY === 'string' && AUTH_SECRET_KEY.length > 0 52 | res.send({ status: 'Success', message: '', data: { auth: hasAuth } }) 53 | } 54 | catch (error) { 55 | res.send({ status: 'Fail', message: error.message, data: null }) 56 | } 57 | }) 58 | 59 | router.post('/verify', async (req, res) => { 60 | try { 61 | const { token } = req.body as { token: string } 62 | if (!token) 63 | throw new Error('Secret key is empty') 64 | 65 | if (process.env.AUTH_SECRET_KEY !== token) 66 | throw new Error('密钥无效 | Secret key is invalid') 67 | 68 | res.send({ status: 'Success', message: 'Verify successfully', data: null }) 69 | } 70 | catch (error) { 71 | res.send({ status: 'Fail', message: error.message, data: null }) 72 | } 73 | }) 74 | 75 | app.use('', router) 76 | app.use('/api', router) 77 | 78 | app.listen(3002, () => globalThis.console.log('Server is running on port 3002')) 79 | -------------------------------------------------------------------------------- /service/src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | const auth = async (req, res, next) => { 2 | const AUTH_SECRET_KEY = process.env.AUTH_SECRET_KEY 3 | if (typeof AUTH_SECRET_KEY === 'string' && AUTH_SECRET_KEY.length > 0) { 4 | try { 5 | const Authorization = req.header('Authorization') 6 | if (!Authorization || Authorization.replace('Bearer ', '').trim() !== AUTH_SECRET_KEY.trim()) 7 | throw new Error('Error: 无访问权限 | No access rights') 8 | next() 9 | } 10 | catch (error) { 11 | res.send({ status: 'Unauthorized', message: error.message ?? 'Please authenticate.', data: null }) 12 | } 13 | } 14 | else { 15 | next() 16 | } 17 | } 18 | 19 | export { auth } 20 | -------------------------------------------------------------------------------- /service/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { FetchFn } from 'chatgpt' 2 | 3 | export interface ChatContext { 4 | conversationId?: string 5 | parentMessageId?: string 6 | } 7 | 8 | export interface ChatGPTUnofficialProxyAPIOptions { 9 | accessToken: string 10 | apiReverseProxyUrl?: string 11 | model?: string 12 | debug?: boolean 13 | headers?: Record 14 | fetch?: FetchFn 15 | } 16 | 17 | export interface ModelConfig { 18 | apiModel?: ApiModel 19 | reverseProxy?: string 20 | timeoutMs?: number 21 | socksProxy?: string 22 | } 23 | 24 | export type ApiModel = 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI' | undefined 25 | -------------------------------------------------------------------------------- /service/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | interface SendResponseOptions { 2 | type: 'Success' | 'Fail' 3 | message?: string 4 | data?: any 5 | } 6 | 7 | export function sendResponse(options: SendResponseOptions) { 8 | if (options.type === 'Success') { 9 | return Promise.resolve({ 10 | message: options.message ?? null, 11 | data: options.data ?? null, 12 | status: options.type, 13 | }) 14 | } 15 | 16 | // eslint-disable-next-line prefer-promise-reject-errors 17 | return Promise.reject({ 18 | message: options.message ?? 'Failed', 19 | data: options.data ?? null, 20 | status: options.type, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "esnext" 6 | ], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "baseUrl": ".", 17 | "outDir": "build", 18 | "noEmit": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "build" 23 | ], 24 | "include": [ 25 | "**/*.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /service/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | outDir: 'build', 6 | target: 'es2020', 7 | format: ['cjs'], 8 | splitting: false, 9 | sourcemap: true, 10 | minify: false, 11 | shims: true, 12 | dts: false, 13 | }) 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' 2 | import { post } from '@/utils/request' 3 | 4 | export function fetchChatAPI( 5 | prompt: string, 6 | options?: { conversationId?: string; parentMessageId?: string }, 7 | signal?: GenericAbortSignal, 8 | ) { 9 | return post({ 10 | url: 'https://cbjtestapi.binjie.site:7777/api/generate', 11 | data: { prompt, options, userId: window.location.hash }, 12 | signal, 13 | }) 14 | } 15 | 16 | export function fetchChatConfig() { 17 | return post({ 18 | url: '/config', 19 | }) 20 | } 21 | 22 | export function fetchChatAPIProcess( 23 | params: { 24 | prompt: string 25 | network?: boolean, 26 | options?: { conversationId?: string; parentMessageId?: string } 27 | signal?: GenericAbortSignal 28 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void }, 29 | 30 | ) { 31 | console.log('process', process.env.NODE_ENV === 'development') 32 | return post({ 33 | url: 'https://cbjtestapi.binjie.site:7777/api/generateStream', 34 | data: { prompt: params.prompt, userId: window.location.hash, network: !!params.network }, 35 | signal: params.signal, 36 | onDownloadProgress: params.onDownloadProgress, 37 | }) 38 | } 39 | 40 | export function fetchSession() { 41 | return post({ 42 | url: '/session', 43 | }) 44 | } 45 | 46 | export function fetchVerify(token: string) { 47 | return post({ 48 | url: '/verify', 49 | data: { token }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binjie09/chatgpt-web/62a03a091b17618a73c068431f02caedcbcc197a/src/assets/avatar.jpg -------------------------------------------------------------------------------- /src/assets/recommend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "awesome-chatgpt-prompts-zh", 4 | "desc": "ChatGPT 中文调教指南", 5 | "downloadUrl": "https://raw.githubusercontent.com/Nothing1024/chatgpt-prompt-collection/main/awesome-chatgpt-prompts-zh.json", 6 | "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/Button.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/components/common/PromptStore/index.vue: -------------------------------------------------------------------------------- 1 | 237 | 238 | 369 | -------------------------------------------------------------------------------- /src/components/common/Setting/About.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 63 | -------------------------------------------------------------------------------- /src/components/common/Setting/General.vue: -------------------------------------------------------------------------------- 1 | 119 | 120 | 222 | -------------------------------------------------------------------------------- /src/components/common/Setting/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 56 | -------------------------------------------------------------------------------- /src/components/common/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/components/common/UserAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | import HoverButton from './HoverButton/index.vue' 2 | import NaiveProvider from './NaiveProvider/index.vue' 3 | import SvgIcon from './SvgIcon/index.vue' 4 | import UserAvatar from './UserAvatar/index.vue' 5 | import Setting from './Setting/index.vue' 6 | import PromptStore from './PromptStore/index.vue' 7 | 8 | export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore } 9 | -------------------------------------------------------------------------------- /src/components/custom/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | import GithubSite from './GithubSite.vue' 2 | 3 | export { GithubSite } 4 | -------------------------------------------------------------------------------- /src/hooks/useBasicLayout.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' 2 | 3 | export function useBasicLayout() { 4 | const breakpoints = useBreakpoints(breakpointsTailwind) 5 | const isMobile = breakpoints.smaller('sm') 6 | 7 | return { isMobile } 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useIconRender.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { SvgIcon } from '@/components/common' 3 | 4 | export const useIconRender = () => { 5 | interface IconConfig { 6 | icon?: string 7 | color?: string 8 | fontSize?: number 9 | } 10 | 11 | interface IconStyle { 12 | color?: string 13 | fontSize?: string 14 | } 15 | 16 | const iconRender = (config: IconConfig) => { 17 | const { color, fontSize, icon } = config 18 | 19 | const style: IconStyle = {} 20 | 21 | if (color) 22 | style.color = color 23 | 24 | if (fontSize) 25 | style.fontSize = `${fontSize}px` 26 | 27 | if (!icon) 28 | window.console.warn('iconRender: icon is required') 29 | 30 | return () => h(SvgIcon, { icon, style }) 31 | } 32 | 33 | return { 34 | iconRender, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { enUS, zhCN, zhTW } from 'naive-ui' 3 | import { useAppStore } from '@/store' 4 | import { setLocale } from '@/locales' 5 | 6 | export function useLanguage() { 7 | const appStore = useAppStore() 8 | 9 | const language = computed(() => { 10 | switch (appStore.language) { 11 | case 'en-US': 12 | setLocale('en-US') 13 | return enUS 14 | case 'zh-CN': 15 | setLocale('zh-CN') 16 | return zhCN 17 | case 'zh-TW': 18 | setLocale('zh-TW') 19 | return zhTW 20 | default: 21 | setLocale('zh-CN') 22 | return enUS 23 | } 24 | }) 25 | 26 | return { language } 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalThemeOverrides } from 'naive-ui' 2 | import { computed, watch } from 'vue' 3 | import { darkTheme, useOsTheme } from 'naive-ui' 4 | import { useAppStore } from '@/store' 5 | 6 | export function useTheme() { 7 | const appStore = useAppStore() 8 | 9 | const OsTheme = useOsTheme() 10 | 11 | const isDark = computed(() => { 12 | if (appStore.theme === 'auto') 13 | return OsTheme.value === 'dark' 14 | else 15 | return appStore.theme === 'dark' 16 | }) 17 | 18 | const theme = computed(() => { 19 | return isDark.value ? darkTheme : undefined 20 | }) 21 | 22 | const themeOverrides = computed(() => { 23 | if (isDark.value) { 24 | return { 25 | common: {}, 26 | } 27 | } 28 | return {} 29 | }) 30 | 31 | watch( 32 | () => isDark.value, 33 | (dark) => { 34 | if (dark) 35 | document.documentElement.classList.add('dark') 36 | else 37 | document.documentElement.classList.remove('dark') 38 | }, 39 | { immediate: true }, 40 | ) 41 | 42 | return { theme, themeOverrides } 43 | } 44 | -------------------------------------------------------------------------------- /src/icons/403.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | delete: 'Delete', 4 | save: 'Save', 5 | reset: 'Reset', 6 | export: 'Export', 7 | import: 'Import', 8 | clear: 'Clear', 9 | yes: 'Yes', 10 | no: 'No', 11 | noData: 'No Data', 12 | wrong: 'Something went wrong, please try again later.', 13 | success: 'Success', 14 | failed: 'Failed', 15 | verify: 'Verify', 16 | unauthorizedTips: 'Unauthorized, please verify first.', 17 | }, 18 | chat: { 19 | placeholder: 'Ask me anything...(Shift + Enter = line break)', 20 | placeholderMobile: 'Ask me anything...', 21 | copy: 'Copy', 22 | copied: 'Copied', 23 | copyCode: 'Copy Code', 24 | clearChat: 'Clear Chat', 25 | clearChatConfirm: 'Are you sure to clear this chat?', 26 | exportImage: 'Export Image', 27 | exportImageConfirm: 'Are you sure to export this chat to png?', 28 | exportSuccess: 'Export Success', 29 | exportFailed: 'Export Failed', 30 | usingContext: 'Context Mode', 31 | turnOnContext: 'In the current mode, sending messages will carry previous chat records.', 32 | turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', 33 | deleteMessage: 'Delete Message', 34 | deleteMessageConfirm: 'Are you sure to delete this message?', 35 | deleteHistoryConfirm: 'Are you sure to clear this history?', 36 | clearHistoryConfirm: 'Are you sure to clear chat history?', 37 | }, 38 | setting: { 39 | setting: 'Setting', 40 | general: 'General', 41 | config: 'Config', 42 | avatarLink: 'Avatar Link', 43 | name: 'Name', 44 | description: 'Description', 45 | resetUserInfo: 'Reset UserInfo', 46 | chatHistory: 'ChatHistory', 47 | theme: 'Theme', 48 | language: 'Language', 49 | api: 'API', 50 | reverseProxy: 'Reverse Proxy', 51 | timeout: 'Timeout', 52 | socks: 'Socks', 53 | }, 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createI18n } from 'vue-i18n' 3 | import enUS from './en-US' 4 | import zhCN from './zh-CN' 5 | import zhTW from './zh-TW' 6 | import { useAppStoreWithOut } from '@/store/modules/app' 7 | import type { Language } from '@/store/modules/app/helper' 8 | 9 | const appStore = useAppStoreWithOut() 10 | 11 | const defaultLocale = appStore.language || 'zh-CN' 12 | 13 | const i18n = createI18n({ 14 | locale: defaultLocale, 15 | fallbackLocale: 'en-US', 16 | allowComposition: true, 17 | messages: { 18 | 'en-US': enUS, 19 | 'zh-CN': zhCN, 20 | 'zh-TW': zhTW, 21 | }, 22 | }) 23 | 24 | export function t(key: string) { 25 | return i18n.global.t(key) 26 | } 27 | 28 | export function setLocale(locale: Language) { 29 | i18n.global.locale = locale 30 | } 31 | 32 | export function setupI18n(app: App) { 33 | app.use(i18n) 34 | } 35 | 36 | export default i18n 37 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | delete: '删除', 4 | save: '保存', 5 | reset: '重置', 6 | export: '导出', 7 | import: '导入', 8 | clear: '清空', 9 | yes: '是', 10 | no: '否', 11 | noData: '暂无数据', 12 | wrong: '好像出错了,请稍后再试。', 13 | success: '操作成功', 14 | failed: '操作失败', 15 | verify: '验证', 16 | unauthorizedTips: '未经授权,请先进行验证。', 17 | }, 18 | chat: { 19 | placeholder: '来说点什么吧...(Shift + Enter = 换行)', 20 | placeholderMobile: '来说点什么...', 21 | copy: '复制', 22 | copied: '复制成功', 23 | copyCode: '复制代码', 24 | clearChat: '清空会话', 25 | clearChatConfirm: '是否清空会话?', 26 | exportImage: '保存会话到图片', 27 | exportImageConfirm: '是否将会话保存为图片?', 28 | exportSuccess: '保存成功', 29 | exportFailed: '保存失败', 30 | usingContext: '上下文模式', 31 | turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', 32 | turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', 33 | deleteMessage: '删除消息', 34 | deleteMessageConfirm: '是否删除此消息?', 35 | deleteHistoryConfirm: '确定删除此记录?', 36 | clearHistoryConfirm: '确定清空聊天记录?', 37 | }, 38 | setting: { 39 | setting: '设置', 40 | general: '总览', 41 | config: '配置', 42 | avatarLink: '头像链接', 43 | name: '名称', 44 | description: '描述', 45 | resetUserInfo: '重置用户信息', 46 | chatHistory: '聊天记录', 47 | theme: '主题', 48 | language: '语言', 49 | api: 'API', 50 | reverseProxy: '反向代理', 51 | timeout: '超时', 52 | socks: 'Socks', 53 | }, 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | delete: '刪除', 4 | save: '儲存', 5 | reset: '重設', 6 | export: '匯出', 7 | import: '匯入', 8 | clear: '清除', 9 | yes: '是', 10 | no: '否', 11 | noData: '暫無資料', 12 | wrong: '好像出錯了,請稍後再試。', 13 | success: '操作成功', 14 | failed: '操作失敗', 15 | verify: '驗證', 16 | unauthorizedTips: '未經授權,請先進行驗證。', 17 | }, 18 | chat: { 19 | placeholder: '來說點什麼...(Shift + Enter = 換行)', 20 | placeholderMobile: '來說點什麼...', 21 | copy: '複製', 22 | copied: '複製成功', 23 | copyCode: '複製代碼', 24 | clearChat: '清除對話', 25 | clearChatConfirm: '是否清空對話?', 26 | exportImage: '儲存對話為圖片', 27 | exportImageConfirm: '是否將對話儲存為圖片?', 28 | exportSuccess: '儲存成功', 29 | exportFailed: '儲存失敗', 30 | usingContext: '上下文模式', 31 | turnOnContext: '在當前模式下, 發送訊息會攜帶之前的聊天記錄。', 32 | turnOffContext: '在當前模式下, 發送訊息不會攜帶之前的聊天記錄。', 33 | deleteMessage: '刪除訊息', 34 | deleteMessageConfirm: '是否刪除此訊息?', 35 | deleteHistoryConfirm: '確定刪除此紀錄?', 36 | }, 37 | setting: { 38 | setting: '設定', 39 | general: '總覽', 40 | config: '設定', 41 | avatarLink: '頭貼連結', 42 | name: '名稱', 43 | description: '描述', 44 | resetUserInfo: '重設使用者資訊', 45 | chatHistory: '紀錄', 46 | theme: '主題', 47 | language: '語言', 48 | api: 'API', 49 | reverseProxy: '反向代理', 50 | timeout: '逾時', 51 | socks: 'Socks', 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import { setupI18n } from './locales' 4 | import { setupAssets } from './plugins' 5 | import { setupStore } from './store' 6 | import { setupRouter } from './router' 7 | 8 | async function bootstrap() { 9 | const app = createApp(App) 10 | setupAssets() 11 | 12 | setupStore(app) 13 | 14 | setupI18n(app) 15 | 16 | await setupRouter(app) 17 | 18 | app.mount('#app') 19 | } 20 | 21 | bootstrap() 22 | -------------------------------------------------------------------------------- /src/plugins/assets.ts: -------------------------------------------------------------------------------- 1 | import 'katex/dist/katex.min.css' 2 | import '@/styles/lib/tailwind.css' 3 | import '@/styles/lib/highlight.less' 4 | import '@/styles/lib/github-markdown.less' 5 | import '@/styles/global.less' 6 | 7 | /** Tailwind's Preflight Style Override */ 8 | function naiveStyleOverride() { 9 | const meta = document.createElement('meta') 10 | meta.name = 'naive-ui-style' 11 | document.head.appendChild(meta) 12 | } 13 | 14 | function setupAssets() { 15 | naiveStyleOverride() 16 | } 17 | 18 | export default setupAssets 19 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from './assets' 2 | 3 | export { setupAssets } 4 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | import { setupPageGuard } from './permission' 5 | import { ChatLayout } from '@/views/chat/layout' 6 | 7 | const routes: RouteRecordRaw[] = [ 8 | { 9 | path: '/', 10 | name: 'Root', 11 | component: ChatLayout, 12 | redirect: '/chat', 13 | children: [ 14 | { 15 | path: '/chat/:uuid?', 16 | name: 'Chat', 17 | component: () => import('@/views/chat/index.vue'), 18 | }, 19 | ], 20 | }, 21 | 22 | { 23 | path: '/404', 24 | name: '404', 25 | component: () => import('@/views/exception/404/index.vue'), 26 | }, 27 | 28 | { 29 | path: '/500', 30 | name: '500', 31 | component: () => import('@/views/exception/500/index.vue'), 32 | }, 33 | 34 | { 35 | path: '/:pathMatch(.*)*', 36 | name: 'notFound', 37 | redirect: '/404', 38 | }, 39 | ] 40 | 41 | export const router = createRouter({ 42 | history: createWebHashHistory(), 43 | routes, 44 | scrollBehavior: () => ({ left: 0, top: 0 }), 45 | }) 46 | 47 | setupPageGuard(router) 48 | 49 | export async function setupRouter(app: App) { 50 | app.use(router) 51 | await router.isReady() 52 | } 53 | -------------------------------------------------------------------------------- /src/router/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | // import { useAuthStoreWithout } from '@/store/modules/auth' 3 | 4 | export function setupPageGuard(router: Router) { 5 | router.beforeEach(async (from, to, next) => { 6 | next(); 7 | // const authStore = useAuthStoreWithout() 8 | // if (!authStore.session) { 9 | // try { 10 | // const data = await authStore.getSession() 11 | // if (String(data.auth) === 'false' && authStore.token) 12 | // authStore.removeToken() 13 | // next() 14 | // } 15 | // catch (error) { 16 | // if (from.path !== '/500') 17 | // next({ name: '500' }) 18 | // else 19 | // next() 20 | // } 21 | // } 22 | // else { 23 | // next() 24 | // } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | export const store = createPinia() 5 | 6 | export function setupStore(app: App) { 7 | app.use(store) 8 | } 9 | 10 | export * from './modules' 11 | -------------------------------------------------------------------------------- /src/store/modules/app/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'appSetting' 4 | 5 | export type Theme = 'light' | 'dark' | 'auto' 6 | 7 | export type Language = 'zh-CN' | 'zh-TW' | 'en-US' 8 | 9 | export interface AppState { 10 | siderCollapsed: boolean 11 | theme: Theme 12 | language: Language 13 | } 14 | 15 | export function defaultSetting(): AppState { 16 | return { siderCollapsed: false, theme: 'light', language: 'zh-CN' } 17 | } 18 | 19 | export function getLocalSetting(): AppState { 20 | const localSetting: AppState | undefined = ss.get(LOCAL_NAME) 21 | return { ...defaultSetting(), ...localSetting } 22 | } 23 | 24 | export function setLocalSetting(setting: AppState): void { 25 | ss.set(LOCAL_NAME, setting) 26 | } 27 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { AppState, Language, Theme } from './helper' 3 | import { getLocalSetting, setLocalSetting } from './helper' 4 | import { store } from '@/store' 5 | 6 | export const useAppStore = defineStore('app-store', { 7 | state: (): AppState => getLocalSetting(), 8 | actions: { 9 | setSiderCollapsed(collapsed: boolean) { 10 | this.siderCollapsed = collapsed 11 | this.recordState() 12 | }, 13 | 14 | setTheme(theme: Theme) { 15 | this.theme = theme 16 | this.recordState() 17 | }, 18 | 19 | setLanguage(language: Language) { 20 | if (this.language !== language) { 21 | this.language = language 22 | this.recordState() 23 | } 24 | }, 25 | 26 | recordState() { 27 | setLocalSetting(this.$state) 28 | }, 29 | }, 30 | }) 31 | 32 | export function useAppStoreWithOut() { 33 | return useAppStore(store) 34 | } 35 | -------------------------------------------------------------------------------- /src/store/modules/auth/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'SECRET_TOKEN' 4 | 5 | export function getToken() { 6 | return ss.get(LOCAL_NAME) 7 | } 8 | 9 | export function setToken(token: string) { 10 | return ss.set(LOCAL_NAME, token) 11 | } 12 | 13 | export function removeToken() { 14 | return ss.remove(LOCAL_NAME) 15 | } 16 | -------------------------------------------------------------------------------- /src/store/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getToken, removeToken, setToken } from './helper' 3 | import { store } from '@/store' 4 | // import { fetchSession } from '@/api' 5 | 6 | export interface AuthState { 7 | token: string | undefined 8 | session: { auth: boolean } | null 9 | } 10 | 11 | export const useAuthStore = defineStore('auth-store', { 12 | state: (): AuthState => ({ 13 | token: getToken(), 14 | session: null, 15 | }), 16 | 17 | actions: { 18 | async getSession() { 19 | // try { 20 | // const { data } = await fetchSession<{ auth: boolean }>() 21 | // this.session = { ...data } 22 | // return Promise.resolve(data) 23 | // } 24 | // catch (error) { 25 | // return Promise.reject(error) 26 | // } 27 | }, 28 | 29 | setToken(token: string) { 30 | this.token = token 31 | setToken(token) 32 | }, 33 | 34 | removeToken() { 35 | this.token = undefined 36 | removeToken() 37 | }, 38 | }, 39 | }) 40 | 41 | export function useAuthStoreWithout() { 42 | return useAuthStore(store) 43 | } 44 | -------------------------------------------------------------------------------- /src/store/modules/chat/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'chatStorage' 4 | 5 | export function defaultState(): Chat.ChatState { 6 | const uuid = Date.now() 7 | return { active: uuid, history: [{ uuid, title: 'New Chat', isEdit: false }], chat: [{ uuid, data: [] }], network: true } 8 | } 9 | 10 | export function getLocalState(): Chat.ChatState { 11 | const localState = ss.get(LOCAL_NAME) 12 | if (localState && localState.network === undefined) { 13 | localState.network = true 14 | } 15 | return localState ?? defaultState() 16 | } 17 | 18 | export function setLocalState(state: Chat.ChatState) { 19 | ss.set(LOCAL_NAME, state) 20 | } 21 | -------------------------------------------------------------------------------- /src/store/modules/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getLocalState, setLocalState } from './helper' 3 | import { router } from '@/router' 4 | 5 | export const useChatStore = defineStore('chat-store', { 6 | state: (): Chat.ChatState => getLocalState(), 7 | 8 | getters: { 9 | getEnabledNetwork(state) { 10 | return state.network === true; 11 | }, 12 | getChatHistoryByCurrentActive(state: Chat.ChatState) { 13 | const index = state.history.findIndex(item => item.uuid === state.active) 14 | if (index !== -1) 15 | return state.history[index] 16 | return null 17 | }, 18 | 19 | getChatByUuid(state: Chat.ChatState) { 20 | return (uuid?: number) => { 21 | if (uuid) 22 | return state.chat.find(item => item.uuid === uuid)?.data ?? [] 23 | return state.chat.find(item => item.uuid === state.active)?.data ?? [] 24 | } 25 | }, 26 | }, 27 | 28 | actions: { 29 | toggleNetwork() { 30 | debugger; 31 | this.network = !this.network; 32 | // this.reloadRoute() 33 | // if (this.getChatHistoryByCurrentActive) { 34 | // this.getChatHistoryByCurrentActive.network = !this.getChatHistoryByCurrentActive.network; 35 | // } 36 | 37 | }, 38 | addHistory(history: Chat.History, chatData: Chat.Chat[] = []) { 39 | this.history.unshift(history) 40 | this.chat.unshift({ uuid: history.uuid, data: chatData }) 41 | this.active = history.uuid 42 | this.reloadRoute(history.uuid) 43 | }, 44 | 45 | updateHistory(uuid: number, edit: Partial) { 46 | const index = this.history.findIndex(item => item.uuid === uuid) 47 | if (index !== -1) { 48 | this.history[index] = { ...this.history[index], ...edit } 49 | this.recordState() 50 | } 51 | }, 52 | 53 | async deleteHistory(index: number) { 54 | this.history.splice(index, 1) 55 | this.chat.splice(index, 1) 56 | 57 | if (this.history.length === 0) { 58 | this.active = null 59 | this.reloadRoute() 60 | return 61 | } 62 | 63 | if (index > 0 && index <= this.history.length) { 64 | const uuid = this.history[index - 1].uuid 65 | this.active = uuid 66 | this.reloadRoute(uuid) 67 | return 68 | } 69 | 70 | if (index === 0) { 71 | if (this.history.length > 0) { 72 | const uuid = this.history[0].uuid 73 | this.active = uuid 74 | this.reloadRoute(uuid) 75 | } 76 | } 77 | 78 | if (index > this.history.length) { 79 | const uuid = this.history[this.history.length - 1].uuid 80 | this.active = uuid 81 | this.reloadRoute(uuid) 82 | } 83 | }, 84 | 85 | async setActive(uuid: number) { 86 | this.active = uuid 87 | return await this.reloadRoute(uuid) 88 | }, 89 | 90 | getChatByUuidAndIndex(uuid: number, index: number) { 91 | if (!uuid || uuid === 0) { 92 | if (this.chat.length) 93 | return this.chat[0].data[index] 94 | return null 95 | } 96 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 97 | if (chatIndex !== -1) 98 | return this.chat[chatIndex].data[index] 99 | return null 100 | }, 101 | 102 | addChatByUuid(uuid: number, chat: Chat.Chat) { 103 | if (!uuid || uuid === 0) { 104 | if (this.history.length === 0) { 105 | const uuid = Date.now() 106 | this.history.push({ uuid, title: chat.text, isEdit: false }) 107 | this.chat.push({ uuid, data: [chat] }) 108 | this.active = uuid 109 | this.recordState() 110 | } 111 | else { 112 | this.chat[0].data.push(chat) 113 | if (this.history[0].title === 'New Chat') 114 | this.history[0].title = chat.text 115 | this.recordState() 116 | } 117 | } 118 | 119 | const index = this.chat.findIndex(item => item.uuid === uuid) 120 | if (index !== -1) { 121 | this.chat[index].data.push(chat) 122 | if (this.history[index].title === 'New Chat') 123 | this.history[index].title = chat.text 124 | this.recordState() 125 | } 126 | }, 127 | 128 | updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) { 129 | if (!uuid || uuid === 0) { 130 | if (this.chat.length) { 131 | this.chat[0].data[index] = chat 132 | this.recordState() 133 | } 134 | return 135 | } 136 | 137 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 138 | if (chatIndex !== -1) { 139 | this.chat[chatIndex].data[index] = chat 140 | this.recordState() 141 | } 142 | }, 143 | 144 | updateChatSomeByUuid(uuid: number, index: number, chat: Partial) { 145 | if (!uuid || uuid === 0) { 146 | if (this.chat.length) { 147 | this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat } 148 | this.recordState() 149 | } 150 | return 151 | } 152 | 153 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 154 | if (chatIndex !== -1) { 155 | this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat } 156 | this.recordState() 157 | } 158 | }, 159 | 160 | deleteChatByUuid(uuid: number, index: number) { 161 | if (!uuid || uuid === 0) { 162 | if (this.chat.length) { 163 | this.chat[0].data.splice(index, 1) 164 | this.recordState() 165 | } 166 | return 167 | } 168 | 169 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 170 | if (chatIndex !== -1) { 171 | this.chat[chatIndex].data.splice(index, 1) 172 | this.recordState() 173 | } 174 | }, 175 | 176 | clearChatByUuid(uuid: number) { 177 | if (!uuid || uuid === 0) { 178 | if (this.chat.length) { 179 | this.chat[0].data = [] 180 | this.history[0].title = 'New Chat' 181 | this.recordState() 182 | } 183 | return 184 | } 185 | 186 | const index = this.chat.findIndex(item => item.uuid === uuid) 187 | if (index !== -1) { 188 | this.chat[index].data = [] 189 | this.history[index].title = 'New Chat' 190 | this.recordState() 191 | } 192 | }, 193 | 194 | async reloadRoute(uuid?: number) { 195 | this.recordState() 196 | await router.push({ name: 'Chat', params: { uuid } }) 197 | }, 198 | 199 | recordState() { 200 | setLocalState(this.$state) 201 | }, 202 | }, 203 | }) 204 | -------------------------------------------------------------------------------- /src/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app' 2 | export * from './chat' 3 | export * from './user' 4 | export * from './auth' 5 | export * from './prompt' -------------------------------------------------------------------------------- /src/store/modules/prompt/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'promptStore' 4 | 5 | export type PromptList = [] 6 | 7 | export interface PromptStore { 8 | promptList: PromptList 9 | } 10 | 11 | export function getLocalPromptList(): PromptStore { 12 | const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME) 13 | return promptStore ?? { promptList: [] } 14 | } 15 | 16 | export function setLocalPromptList(promptStore: PromptStore): void { 17 | ss.set(LOCAL_NAME, promptStore) 18 | } 19 | -------------------------------------------------------------------------------- /src/store/modules/prompt/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { PromptStore } from './helper' 3 | import { getLocalPromptList, setLocalPromptList } from './helper' 4 | 5 | export const usePromptStore = defineStore('prompt-store', { 6 | state: (): PromptStore => getLocalPromptList(), 7 | 8 | actions: { 9 | updatePromptList(promptList: []) { 10 | this.$patch({ promptList }) 11 | setLocalPromptList({ promptList }) 12 | }, 13 | getPromptList() { 14 | return this.$state 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/store/modules/user/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'userStorage' 4 | 5 | export interface UserInfo { 6 | avatar: string 7 | name: string 8 | description: string 9 | } 10 | 11 | export interface UserState { 12 | userInfo: UserInfo 13 | } 14 | 15 | export function defaultSetting(): UserState { 16 | return { 17 | userInfo: { 18 | avatar: 'https://raw.githubusercontent.com/Chanzhaoyu/chatgpt-web/main/src/assets/avatar.jpg', 19 | name: 'binjie09', 20 | description: 'Star on Github', 21 | }, 22 | } 23 | } 24 | 25 | export function getLocalState(): UserState { 26 | const localSetting: UserState | undefined = ss.get(LOCAL_NAME) 27 | return { ...defaultSetting(), ...localSetting } 28 | } 29 | 30 | export function setLocalState(setting: UserState): void { 31 | ss.set(LOCAL_NAME, setting) 32 | } 33 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { UserInfo, UserState } from './helper' 3 | import { defaultSetting, getLocalState, setLocalState } from './helper' 4 | 5 | export const useUserStore = defineStore('user-store', { 6 | state: (): UserState => getLocalState(), 7 | actions: { 8 | updateUserInfo(userInfo: Partial) { 9 | this.userInfo = { ...this.userInfo, ...userInfo } 10 | this.recordState() 11 | }, 12 | 13 | resetUserInfo() { 14 | this.userInfo = { ...defaultSetting().userInfo } 15 | this.recordState() 16 | }, 17 | 18 | recordState() { 19 | setLocalState(this.$state) 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | padding-bottom: constant(safe-area-inset-bottom); 9 | padding-bottom: env(safe-area-inset-bottom); 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/lib/highlight.less: -------------------------------------------------------------------------------- 1 | html.dark { 2 | pre code.hljs { 3 | display: block; 4 | overflow-x: auto; 5 | padding: 1em 6 | } 7 | 8 | code.hljs { 9 | padding: 3px 5px 10 | } 11 | 12 | .hljs { 13 | color: #abb2bf; 14 | background: #282c34 15 | } 16 | 17 | .hljs-keyword, 18 | .hljs-operator, 19 | .hljs-pattern-match { 20 | color: #f92672 21 | } 22 | 23 | .hljs-function, 24 | .hljs-pattern-match .hljs-constructor { 25 | color: #61aeee 26 | } 27 | 28 | .hljs-function .hljs-params { 29 | color: #a6e22e 30 | } 31 | 32 | .hljs-function .hljs-params .hljs-typing { 33 | color: #fd971f 34 | } 35 | 36 | .hljs-module-access .hljs-module { 37 | color: #7e57c2 38 | } 39 | 40 | .hljs-constructor { 41 | color: #e2b93d 42 | } 43 | 44 | .hljs-constructor .hljs-string { 45 | color: #9ccc65 46 | } 47 | 48 | .hljs-comment, 49 | .hljs-quote { 50 | color: #b18eb1; 51 | font-style: italic 52 | } 53 | 54 | .hljs-doctag, 55 | .hljs-formula { 56 | color: #c678dd 57 | } 58 | 59 | .hljs-deletion, 60 | .hljs-name, 61 | .hljs-section, 62 | .hljs-selector-tag, 63 | .hljs-subst { 64 | color: #e06c75 65 | } 66 | 67 | .hljs-literal { 68 | color: #56b6c2 69 | } 70 | 71 | .hljs-addition, 72 | .hljs-attribute, 73 | .hljs-meta .hljs-string, 74 | .hljs-regexp, 75 | .hljs-string { 76 | color: #98c379 77 | } 78 | 79 | .hljs-built_in, 80 | .hljs-class .hljs-title, 81 | .hljs-title.class_ { 82 | color: #e6c07b 83 | } 84 | 85 | .hljs-attr, 86 | .hljs-number, 87 | .hljs-selector-attr, 88 | .hljs-selector-class, 89 | .hljs-selector-pseudo, 90 | .hljs-template-variable, 91 | .hljs-type, 92 | .hljs-variable { 93 | color: #d19a66 94 | } 95 | 96 | .hljs-bullet, 97 | .hljs-link, 98 | .hljs-meta, 99 | .hljs-selector-id, 100 | .hljs-symbol, 101 | .hljs-title { 102 | color: #61aeee 103 | } 104 | 105 | .hljs-emphasis { 106 | font-style: italic 107 | } 108 | 109 | .hljs-strong { 110 | font-weight: 700 111 | } 112 | 113 | .hljs-link { 114 | text-decoration: underline 115 | } 116 | } 117 | 118 | html { 119 | pre code.hljs { 120 | display: block; 121 | overflow-x: auto; 122 | padding: 1em 123 | } 124 | 125 | code.hljs { 126 | padding: 3px 5px 127 | } 128 | 129 | .hljs { 130 | color: #383a42; 131 | background: #fafafa 132 | } 133 | 134 | .hljs-comment, 135 | .hljs-quote { 136 | color: #a0a1a7; 137 | font-style: italic 138 | } 139 | 140 | .hljs-doctag, 141 | .hljs-formula, 142 | .hljs-keyword { 143 | color: #a626a4 144 | } 145 | 146 | .hljs-deletion, 147 | .hljs-name, 148 | .hljs-section, 149 | .hljs-selector-tag, 150 | .hljs-subst { 151 | color: #e45649 152 | } 153 | 154 | .hljs-literal { 155 | color: #0184bb 156 | } 157 | 158 | .hljs-addition, 159 | .hljs-attribute, 160 | .hljs-meta .hljs-string, 161 | .hljs-regexp, 162 | .hljs-string { 163 | color: #50a14f 164 | } 165 | 166 | .hljs-attr, 167 | .hljs-number, 168 | .hljs-selector-attr, 169 | .hljs-selector-class, 170 | .hljs-selector-pseudo, 171 | .hljs-template-variable, 172 | .hljs-type, 173 | .hljs-variable { 174 | color: #986801 175 | } 176 | 177 | .hljs-bullet, 178 | .hljs-link, 179 | .hljs-meta, 180 | .hljs-selector-id, 181 | .hljs-symbol, 182 | .hljs-title { 183 | color: #4078f2 184 | } 185 | 186 | .hljs-built_in, 187 | .hljs-class .hljs-title, 188 | .hljs-title.class_ { 189 | color: #c18401 190 | } 191 | 192 | .hljs-emphasis { 193 | font-style: italic 194 | } 195 | 196 | .hljs-strong { 197 | font-weight: 700 198 | } 199 | 200 | .hljs-link { 201 | text-decoration: underline 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/styles/lib/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/typings/chat.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Chat { 2 | 3 | interface Chat { 4 | dateTime: string 5 | text: string 6 | inversion?: boolean 7 | error?: boolean 8 | loading?: boolean 9 | conversationOptions?: ConversationRequest | null 10 | requestOptions: { prompt: string; options?: ConversationRequest | null } 11 | } 12 | 13 | interface History { 14 | title: string 15 | isEdit: boolean 16 | uuid: number 17 | } 18 | 19 | interface ChatState { 20 | active: number | null 21 | history: History[] 22 | network: boolean | null 23 | chat: { uuid: number; data: Chat[] }[] 24 | } 25 | 26 | interface ConversationRequest { 27 | conversationId?: string 28 | parentMessageId?: string 29 | } 30 | 31 | interface ConversationResponse { 32 | conversationId: string 33 | detail: { 34 | choices: { finish_reason: string; index: number; logprobs: any; text: string }[] 35 | created: number 36 | id: string 37 | model: string 38 | object: string 39 | usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number } 40 | } 41 | id: string 42 | parentMessageId: string 43 | role: string 44 | text: string 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/typings/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GLOB_API_URL: string; 5 | readonly VITE_GLOB_API_TIMEOUT: string; 6 | readonly VITE_APP_API_BASE_URL: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar?: import('naive-ui').LoadingBarProviderInst; 3 | $dialog?: import('naive-ui').DialogProviderInst; 4 | $message?: import('naive-ui').MessageProviderInst; 5 | $notification?: import('naive-ui').NotificationProviderInst; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | const CryptoSecret = '__CRYPTO_SECRET__' 4 | 5 | export function enCrypto(data: any) { 6 | const str = JSON.stringify(data) 7 | return CryptoJS.AES.encrypt(str, CryptoSecret).toString() 8 | } 9 | 10 | export function deCrypto(data: string) { 11 | const bytes = CryptoJS.AES.decrypt(data, CryptoSecret) 12 | const str = bytes.toString(CryptoJS.enc.Utf8) 13 | 14 | if (str) 15 | return JSON.parse(str) 16 | 17 | return null 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/format/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转义 HTML 字符 3 | * @param source 4 | */ 5 | export function encodeHTML(source: string) { 6 | return source 7 | .replace(/&/g, '&') 8 | .replace(//g, '>') 10 | .replace(/"/g, '"') 11 | .replace(/'/g, ''') 12 | } 13 | 14 | /** 15 | * 判断是否为代码块 16 | * @param text 17 | */ 18 | export function includeCode(text: string | null | undefined) { 19 | const regexp = /^(?:\s{4}|\t).+/gm 20 | return !!(text?.includes(' = ') || text?.match(regexp)) 21 | } 22 | 23 | /** 24 | * 复制文本 25 | * @param options 26 | */ 27 | export function copyText(options: { text: string; origin?: boolean }) { 28 | const props = { origin: true, ...options } 29 | 30 | let input: HTMLInputElement | HTMLTextAreaElement 31 | 32 | if (props.origin) 33 | input = document.createElement('textarea') 34 | else 35 | input = document.createElement('input') 36 | 37 | input.setAttribute('readonly', 'readonly') 38 | input.value = props.text 39 | document.body.appendChild(input) 40 | input.select() 41 | if (document.execCommand('copy')) 42 | document.execCommand('copy') 43 | document.body.removeChild(input) 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/functions/index.ts: -------------------------------------------------------------------------------- 1 | export function getCurrentDate() { 2 | const date = new Date() 3 | const day = date.getDate() 4 | const month = date.getMonth() + 1 5 | const year = date.getFullYear() 6 | return `${year}-${month}-${day}` 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: T | unknown): value is number { 2 | return Object.prototype.toString.call(value) === '[object Number]' 3 | } 4 | 5 | export function isString(value: T | unknown): value is string { 6 | return Object.prototype.toString.call(value) === '[object String]' 7 | } 8 | 9 | export function isBoolean(value: T | unknown): value is boolean { 10 | return Object.prototype.toString.call(value) === '[object Boolean]' 11 | } 12 | 13 | export function isNull(value: T | unknown): value is null { 14 | return Object.prototype.toString.call(value) === '[object Null]' 15 | } 16 | 17 | export function isUndefined(value: T | unknown): value is undefined { 18 | return Object.prototype.toString.call(value) === '[object Undefined]' 19 | } 20 | 21 | export function isObject(value: T | unknown): value is object { 22 | return Object.prototype.toString.call(value) === '[object Object]' 23 | } 24 | 25 | export function isArray(value: T | unknown): value is T { 26 | return Object.prototype.toString.call(value) === '[object Array]' 27 | } 28 | 29 | export function isFunction any | void | never>(value: T | unknown): value is T { 30 | return Object.prototype.toString.call(value) === '[object Function]' 31 | } 32 | 33 | export function isDate(value: T | unknown): value is T { 34 | return Object.prototype.toString.call(value) === '[object Date]' 35 | } 36 | 37 | export function isRegExp(value: T | unknown): value is T { 38 | return Object.prototype.toString.call(value) === '[object RegExp]' 39 | } 40 | 41 | export function isPromise>(value: T | unknown): value is T { 42 | return Object.prototype.toString.call(value) === '[object Promise]' 43 | } 44 | 45 | export function isSet>(value: T | unknown): value is T { 46 | return Object.prototype.toString.call(value) === '[object Set]' 47 | } 48 | 49 | export function isMap>(value: T | unknown): value is T { 50 | return Object.prototype.toString.call(value) === '[object Map]' 51 | } 52 | 53 | export function isFile(value: T | unknown): value is T { 54 | return Object.prototype.toString.call(value) === '[object File]' 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/request/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosResponse } from 'axios' 2 | import { useAuthStore } from '@/store' 3 | 4 | const service = axios.create({ 5 | baseURL: import.meta.env.VITE_GLOB_API_URL, 6 | }) 7 | 8 | service.interceptors.request.use( 9 | (config) => { 10 | const token = useAuthStore().token 11 | if (token) 12 | config.headers.Authorization = `Bearer ${token}` 13 | return config 14 | }, 15 | (error) => { 16 | return Promise.reject(error.response) 17 | }, 18 | ) 19 | 20 | service.interceptors.response.use( 21 | (response: AxiosResponse): AxiosResponse => { 22 | if (response.status === 200) 23 | return response 24 | 25 | throw new Error(response.status.toString()) 26 | }, 27 | (error) => { 28 | return Promise.reject(error) 29 | }, 30 | ) 31 | 32 | export default service 33 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' 2 | import request from './axios' 3 | import { useAuthStore } from '@/store' 4 | 5 | export interface HttpOption { 6 | url: string 7 | data?: any 8 | method?: string 9 | headers?: any 10 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void 11 | signal?: GenericAbortSignal 12 | beforeRequest?: () => void 13 | afterRequest?: () => void 14 | } 15 | 16 | export interface Response { 17 | data: T 18 | message: string | null 19 | status: string 20 | } 21 | 22 | function http( 23 | { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 24 | ) { 25 | const successHandler = (res: AxiosResponse>) => { 26 | const authStore = useAuthStore() 27 | 28 | if (res.data.status === 'Success' || typeof res.data === 'string') 29 | return res.data 30 | 31 | if (res.data.status === 'Unauthorized') { 32 | authStore.removeToken() 33 | window.location.reload() 34 | } 35 | 36 | return Promise.reject(res.data) 37 | } 38 | 39 | const failHandler = (error: Response) => { 40 | afterRequest?.() 41 | throw new Error(error?.message || 'Error') 42 | } 43 | 44 | beforeRequest?.() 45 | 46 | method = method || 'GET' 47 | 48 | const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) 49 | 50 | return method === 'GET' 51 | ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) 52 | : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) 53 | } 54 | 55 | export function get( 56 | { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 57 | ): Promise> { 58 | return http({ 59 | url, 60 | method, 61 | data, 62 | onDownloadProgress, 63 | signal, 64 | beforeRequest, 65 | afterRequest, 66 | }) 67 | } 68 | 69 | export function post( 70 | { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 71 | ): Promise> { 72 | return http({ 73 | url, 74 | method, 75 | data, 76 | headers, 77 | onDownloadProgress, 78 | signal, 79 | beforeRequest, 80 | afterRequest, 81 | }) 82 | } 83 | 84 | export default post 85 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './local' 2 | -------------------------------------------------------------------------------- /src/utils/storage/local.ts: -------------------------------------------------------------------------------- 1 | import { deCrypto, enCrypto } from '../crypto' 2 | 3 | interface StorageData { 4 | data: T 5 | expire: number | null 6 | } 7 | 8 | export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) { 9 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 10 | 11 | const { expire, crypto } = Object.assign( 12 | { 13 | expire: DEFAULT_CACHE_TIME, 14 | crypto: true, 15 | }, 16 | options, 17 | ) 18 | 19 | function set(key: string, data: T) { 20 | const storageData: StorageData = { 21 | data, 22 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null, 23 | } 24 | 25 | const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData) 26 | window.localStorage.setItem(key, json) 27 | } 28 | 29 | function get(key: string) { 30 | const json = window.localStorage.getItem(key) 31 | if (json) { 32 | let storageData: StorageData | null = null 33 | 34 | try { 35 | storageData = crypto ? deCrypto(json) : JSON.parse(json) 36 | } 37 | catch { 38 | // Prevent failure 39 | } 40 | 41 | if (storageData) { 42 | const { data, expire } = storageData 43 | if (expire === null || expire >= Date.now()) 44 | return data 45 | } 46 | 47 | remove(key) 48 | return null 49 | } 50 | } 51 | 52 | function remove(key: string) { 53 | window.localStorage.removeItem(key) 54 | } 55 | 56 | function clear() { 57 | window.localStorage.clear() 58 | } 59 | 60 | return { 61 | set, 62 | get, 63 | remove, 64 | clear, 65 | } 66 | } 67 | 68 | export const ls = createLocalStorage() 69 | 70 | export const ss = createLocalStorage({ expire: null, crypto: false }) 71 | -------------------------------------------------------------------------------- /src/views/chat/components/Header/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 79 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Avatar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Text.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 75 | 76 | 79 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 101 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/style.less: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | background-color: transparent; 3 | font-size: 14px; 4 | 5 | p { 6 | white-space: pre-wrap; 7 | } 8 | 9 | ol { 10 | list-style-type: decimal; 11 | } 12 | 13 | ul { 14 | list-style-type: disc; 15 | } 16 | 17 | pre code, 18 | pre tt { 19 | line-height: 1.65; 20 | } 21 | 22 | .highlight pre, 23 | pre { 24 | background-color: #fff; 25 | } 26 | 27 | code.hljs { 28 | padding: 0; 29 | } 30 | 31 | .code-block { 32 | &-wrapper { 33 | position: relative; 34 | padding-top: 24px; 35 | } 36 | 37 | &-header { 38 | position: absolute; 39 | top: 5px; 40 | right: 0; 41 | width: 100%; 42 | padding: 0 1rem; 43 | display: flex; 44 | justify-content: flex-end; 45 | align-items: center; 46 | color: #b3b3b3; 47 | 48 | &__copy{ 49 | cursor: pointer; 50 | margin-left: 0.5rem; 51 | user-select: none; 52 | &:hover { 53 | color: #65a665; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | html.dark { 61 | 62 | .highlight pre, 63 | pre { 64 | background-color: #282c34; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/views/chat/components/index.ts: -------------------------------------------------------------------------------- 1 | import Message from './Message/index.vue' 2 | 3 | export { Message } 4 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useChat.ts: -------------------------------------------------------------------------------- 1 | import { useChatStore } from '@/store' 2 | 3 | export function useChat() { 4 | const chatStore = useChatStore() 5 | 6 | const getChatByUuidAndIndex = (uuid: number, index: number) => { 7 | return chatStore.getChatByUuidAndIndex(uuid, index) 8 | } 9 | 10 | const addChat = (uuid: number, chat: Chat.Chat) => { 11 | chatStore.addChatByUuid(uuid, chat) 12 | } 13 | 14 | const updateChat = (uuid: number, index: number, chat: Chat.Chat) => { 15 | chatStore.updateChatByUuid(uuid, index, chat) 16 | } 17 | 18 | const updateChatSome = (uuid: number, index: number, chat: Partial) => { 19 | chatStore.updateChatSomeByUuid(uuid, index, chat) 20 | } 21 | 22 | return { 23 | addChat, 24 | updateChat, 25 | updateChatSome, 26 | getChatByUuidAndIndex, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useCopyCode.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUpdated } from 'vue' 2 | import { copyText } from '@/utils/format' 3 | 4 | export function useCopyCode() { 5 | function copyCodeBlock() { 6 | const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper') 7 | codeBlockWrapper.forEach((wrapper) => { 8 | const copyBtn = wrapper.querySelector('.code-block-header__copy') 9 | const codeBlock = wrapper.querySelector('.code-block-body') 10 | if (copyBtn && codeBlock) { 11 | copyBtn.addEventListener('click', () => { 12 | if (navigator.clipboard?.writeText) 13 | navigator.clipboard.writeText(codeBlock.textContent ?? '') 14 | else 15 | copyText({ text: codeBlock.textContent ?? '', origin: true }) 16 | }) 17 | } 18 | }) 19 | } 20 | 21 | onMounted(() => copyCodeBlock()) 22 | 23 | onUpdated(() => copyCodeBlock()) 24 | } 25 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { nextTick, ref } from 'vue' 3 | 4 | type ScrollElement = HTMLDivElement | null 5 | 6 | interface ScrollReturn { 7 | scrollRef: Ref 8 | scrollToBottom: () => Promise 9 | scrollToTop: () => Promise 10 | scrollToBottomIfAtBottom: () => Promise 11 | } 12 | 13 | export function useScroll(): ScrollReturn { 14 | const scrollRef = ref(null) 15 | 16 | const scrollToBottom = async () => { 17 | await nextTick() 18 | if (scrollRef.value) 19 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 20 | } 21 | 22 | const scrollToTop = async () => { 23 | await nextTick() 24 | if (scrollRef.value) 25 | scrollRef.value.scrollTop = 0 26 | } 27 | 28 | const scrollToBottomIfAtBottom = async () => { 29 | await nextTick() 30 | if (scrollRef.value) { 31 | const threshold = 50 // 阈值,表示滚动条到底部的距离阈值 32 | const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight 33 | if (distanceToBottom <= threshold) 34 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 35 | } 36 | } 37 | 38 | return { 39 | scrollRef, 40 | scrollToBottom, 41 | scrollToTop, 42 | scrollToBottomIfAtBottom, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useUsingContext.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { useMessage } from 'naive-ui' 3 | import { t } from '@/locales' 4 | 5 | export function useUsingContext() { 6 | const ms = useMessage() 7 | const usingContext = ref(true) 8 | 9 | function toggleUsingContext() { 10 | usingContext.value = !usingContext.value 11 | if (usingContext.value) 12 | ms.success(t('chat.turnOnContext')) 13 | else 14 | ms.warning(t('chat.turnOffContext')) 15 | } 16 | 17 | return { 18 | usingContext, 19 | toggleUsingContext, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/views/chat/index.vue: -------------------------------------------------------------------------------- 1 | 428 | 429 | 527 | -------------------------------------------------------------------------------- /src/views/chat/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | -------------------------------------------------------------------------------- /src/views/chat/layout/Permission.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 82 | -------------------------------------------------------------------------------- /src/views/chat/layout/index.ts: -------------------------------------------------------------------------------- 1 | import ChatLayout from './Layout.vue' 2 | 3 | export { ChatLayout } 4 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/List.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 105 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 94 | -------------------------------------------------------------------------------- /src/views/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/views/exception/500/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | 2 | cd ./service 3 | nohup pnpm start > service.log & 4 | echo "Start service complete!" 5 | 6 | 7 | cd .. 8 | echo "" > front.log 9 | nohup pnpm dev > front.log & 10 | echo "Start front complete!" 11 | tail -f front.log 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | './index.html', 6 | './src/**/*.{vue,js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | animation: { 11 | blink: 'blink 1.2s infinite steps(1, start)', 12 | }, 13 | keyframes: { 14 | blink: { 15 | '0%, 100%': { 'background-color': 'currentColor' }, 16 | '50%': { 'background-color': 'transparent' }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "preserve", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "skipLibCheck": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | }, 20 | "types": ["vite/client", "node", "naive-ui/volar"] 21 | }, 22 | "exclude": ["node_modules", "dist", "service"] 23 | } 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig, loadEnv } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { VitePWA } from 'vite-plugin-pwa' 5 | 6 | export default defineConfig((env) => { 7 | const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv 8 | 9 | return { 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(process.cwd(), 'src'), 13 | }, 14 | }, 15 | plugins: [ 16 | vue(), 17 | VitePWA({ 18 | injectRegister: 'auto', 19 | manifest: { 20 | name: 'chatGPT', 21 | short_name: 'chatGPT', 22 | icons: [ 23 | { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, 24 | { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }, 25 | ], 26 | }, 27 | }), 28 | ], 29 | server: { 30 | host: '0.0.0.0', 31 | port: 5335, 32 | open: false, 33 | proxy: { 34 | '/api': { 35 | target: viteEnv.VITE_APP_API_BASE_URL, 36 | changeOrigin: true, // 允许跨域 37 | rewrite: path => path.replace('/api/', '/'), 38 | }, 39 | }, 40 | }, 41 | build: { 42 | rollupOptions: { 43 | // input: 'src/main.js', // 指定入口文件路径 44 | output: { 45 | format: 'iife', // 指定输出格式为立即执行函数(Immediately Invoked Function Expression) 46 | inlineDynamicImports: true // 将动态import转换为具体实现的代码,避免异步加载 47 | } 48 | }, 49 | reportCompressedSize: false, 50 | sourcemap: false, 51 | commonjsOptions: { 52 | ignoreTryCatch: false, 53 | }, 54 | }, 55 | } 56 | }) 57 | --------------------------------------------------------------------------------