frontend 兼容

This commit is contained in:
1iaan
2026-04-06 11:38:45 +08:00
parent f433490e0d
commit 94bdba930e
323 changed files with 49507 additions and 34744 deletions

View File

@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

View File

@@ -0,0 +1,8 @@
**/node_modules
*/node_modules
node_modules
Dockerfile
.*
*/.*
!.env
!.env.production

11
ai-chat-web/.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

12
ai-chat-web/.env.develop Normal file
View File

@@ -0,0 +1,12 @@
# Glob API URL
VITE_GLOB_API_URL=/api
VITE_APP_API_BASE_URL=http://localhost:7080/
# Whether long replies are supported, which may result in higher API fees
VITE_GLOB_OPEN_LONG_REPLY=false
# When you want to use PWA
VITE_GLOB_APP_PWA=false
VITE_USER_CENTER="http://localhost:8082?sys=ai"

View File

@@ -0,0 +1,8 @@
VITE_GLOB_API_URL=/api
# Whether long replies are supported, which may result in higher API fees
VITE_GLOB_OPEN_LONG_REPLY=false
# When you want to use PWA
VITE_GLOB_APP_PWA=false
VITE_USER_CENTER="https://user.0voice.com?sys=ai"

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['@antfu'],
}

17
ai-chat-web/.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.cjs" eol=lf
"*.cts" eol=lf
"*.mjs" eol=lf
"*.mts" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.less" eol=lf
"*.scss" eol=lf
"*.sass" eol=lf
"*.styl" eol=lf
"*.md" eol=lf

View File

@@ -0,0 +1,41 @@
name: build_docker
on:
push:
branches: [main]
release:
types: [created] # 表示在创建新的 Release 时触发
jobs:
build_docker:
name: Build docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- run: |
echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)"
echo 本次构建的版本为:${{ github.ref_name }}
env
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4
with:
context: .
push: true
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }}
${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest

47
ai-chat-web/.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Setup
run: npm i -g @antfu/ni
- name: Install
run: nci
- name: Lint
run: nr lint:fix
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Setup
run: npm i -g @antfu/ni
- name: Install
run: nci
- name: Typecheck
run: nr type-check

32
ai-chat-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables files
/service/.env

View File

@@ -0,0 +1,59 @@
variables:
DEPLOY_ENV: ''
DEPLOY_IMG: ''
REPO: 'chatgpt-frontend'
SERVICE_NAME: 'chatgpt-stack_chatgpt-frontend'
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "dev" && $CI_PIPELINE_SOURCE == "push"
variables:
DEPLOY_ENV: 'dev'
DEPLOY_IMG: "${REPO}:${CI_COMMIT_SHORT_SHA}"
- if: $CI_COMMIT_TAG
variables:
DEPLOY_ENV: 'prod'
DEPLOY_IMG: "${REPO}:${CI_COMMIT_TAG}"
- when: never
stages:
# 编译阶段
- build
# 部署阶段(部署到测试环境/部署到生产环境)
- deploy
build-job:
stage: build
tags:
- builder
before_script:
- docker login -u ${DOCKER_REGISTRY_USER} -p ${DOCKER_REGISTRY_PWD} ${DOCKER_REGISTRY}
script:
- docker build -t ${DOCKER_REGISTRY}/${DEPLOY_IMG} .
- docker push ${DOCKER_REGISTRY}/${DEPLOY_IMG}
deploy-dev-job:
only:
variables:
- $DEPLOY_ENV == "dev"
stage: deploy
tags:
- deployer
variables:
CONF_RM_STR: ""
before_script:
- docker login -u ${DOCKER_REGISTRY_USER} -p ${DOCKER_REGISTRY_PWD} ${DOCKER_REGISTRY}
script:
- docker service update ${SERVICE_NAME} --image ${DOCKER_REGISTRY}/${DEPLOY_IMG}
deploy-prod-job:
only:
variables:
- $DEPLOY_ENV == "prod"
stage: deploy
tags:
- deployer
variables:
CONF_RM_STR: ""
before_script:
- docker login -u ${DOCKER_REGISTRY_USER} -p ${DOCKER_REGISTRY_PWD} ${DOCKER_REGISTRY}
script:
- docker service update ${SERVICE_NAME} --image ${DOCKER_REGISTRY}/${DEPLOY_IMG}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

1
ai-chat-web/.npmrc Normal file
View File

@@ -0,0 +1 @@
strict-peer-dependencies=false

3
ai-chat-web/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"]
}

65
ai-chat-web/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,65 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"json",
"jsonc",
"json5",
"yaml",
"yml",
"markdown"
],
"cSpell.words": [
"antfu",
"axios",
"bumpp",
"chatgpt",
"chenzhaoyu",
"commitlint",
"davinci",
"dockerhub",
"esno",
"GPTAPI",
"highlightjs",
"hljs",
"iconify",
"katex",
"katexmath",
"linkify",
"logprobs",
"mdhljs",
"mila",
"nodata",
"OPENAI",
"pinia",
"Popconfirm",
"rushstack",
"Sider",
"tailwindcss",
"traptitech",
"tsup",
"Typecheck",
"unplugin",
"VITE",
"vueuse",
"Zhao"
],
"i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.keystyle": "nested"
}

548
ai-chat-web/CHANGELOG.md Normal file
View File

@@ -0,0 +1,548 @@
## v2.10.8
`2023-03-23`
如遇问题,请删除 `node_modules` 重新安装依赖。
## Feature
- 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)]
- 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)]
- 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)]
- `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token``API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)]
- 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)]
## Enhancement
- 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)]
-`API``gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)]
- 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)]
- 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)]
- 渲染的链接新窗口打开
- 查询余额可选 `API_BASE_URL` 代理地址
- `config` 接口添加验证防止被无限制调用
- `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量
- 当网络连接时,刷新页面,`500` 错误页自动跳转到主页
## BugFix
- `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)]
- 重置异常的 `loading` 会话
## Common
- 创建 `start.cmd``windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)]
- 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)]
- 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)]
## Other
- 依赖更新
## v2.10.7
`2023-03-17`
## BugFix
- 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效
- 修复缺省状态的 `usingContext` 默认值
## v2.10.6
`2023-03-17`
## Feature
- 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)]
## Enhancement
- 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)]
- 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)]
- 上下文开关改为全局开关,现在记录在本地缓存中
- 配置信息按接口类型显示
## Perf
- 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)]
- 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)]
- 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)]
## BugFix
- 修复 `Prompt` 导入、导出兼容性错误
- 修复 `highlight.js` 控制台兼容性警告
## Other
- 依赖更新
## v2.10.5
`2023-03-13`
更新依赖,`access_token` 默认代理为 [acheong08](https://github.com/acheong08) 的 `https://bypass.duti.tech/api/conversation`
## Feature
- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521)
- 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308)
- `Prompt` 添加查询筛选
## Enhancement
- 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502)
- 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520)
- `Prompt` 添加翻译和优化布局
- 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542)
- 语言选择调整为下路框形式
- 权限输入框类型调整为密码形式
## BugFix
- `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523)
- 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499)
- 确定清空上下文时不应该重置会话标题
## Other
- 调整文档
- 更新依赖
## v2.10.4
`2023-03-11`
## Feature
- 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持
## Enhancement
- 设置添加关闭按钮[#495]
## Demo
![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966)
## v2.10.3
`2023-03-10`
> 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`如果你觉得本项目有监控后门或有问题导致你的账号、API被封那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。
## Feature
- 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)]
- 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)]
## Enhancement
- 调整移动端按钮和优化布局
- 调整 `iOS` 上安全距离
- 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)]
## BugFix
- 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)]
- 修复设置文字过长时导致的设置按钮消失的问题
## Other
- 更新依赖
## v2.10.2
`2023-03-09`
衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1)
## Enhancement
- 移动端下输入框获得焦点时左侧按钮隐藏
## BugFix
- 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉
- 回退 `2.10.1` 中前端变量影响 `Docker` 打包
## v2.10.1
`2023-03-09`
注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下:
- 在项目中添加 `.env` 从一开始就是个错误的示范
- 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰
- 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改
这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。
## Feature
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能
## Enhancement
- 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题
- 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL`
- 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题
## BugFix
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题
- 修复深色模式导出图片的样式问题
## v2.10.0
`2023-03-07`
- 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。
- 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。
- 演示图片请看最后
## Feature
- 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码
- 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译
- 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能
## Enhancement
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息
- 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性
- 优化部分代码
## BugFix
- 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充)
## Other
- 更新依赖到最新
## 演示
> 不是界面最新效果,有美化改动
权限
![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png)
聊天记录导出
![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif)
保存图片到本地
![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif)
## v2.9.3
`2023-03-06`
## Enhancement
- 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题
- 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档
- 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译
## BugFix
- 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题
- 修复复制代码的兼容性报错问题
- 修复部分优化小问题
## v2.9.2
`2023-03-04`
手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。
### Feature
- 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持
- 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持
- 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249]
## Enhancement
- 优化在高分屏上主题内容的最大宽度[#257]
- 现在文字按单词截断[#215][#225]
### BugFix
- 修复动态生成时代码块不能被复制的问题[#251][#260]
- 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256]
- 修复控制台渲染警告
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.1
`2023-03-02`
### Feature
- 代码块添加当前代码语言显示和复制功能[#197][#196]
- 完善多语言,现在可以切换中英文显示
## Enhancement
- 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档
### BugFix
- 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.0
`2023-03-02`
### Feature
- 现在能复制带格式的消息文本
- 新设计的设定页面,可以自定义姓名、描述、头像(链接方式)
- 新增`403``404`页面以便扩展
## Enhancement
- 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认)
- 取消了前端超时限制设定
## v2.8.3
`2023-03-01`
### Feature
- 消息已输出内容不会因为中断而消失[#167]
- 添加复制消息按钮[#133]
### Other
- `README` 添加声明内容
## v2.8.2
`2023-02-28`
### Enhancement
- 代码主题调整为 `One Dark - light|dark` 适配深色模式
### BugFix
- 修复普通文本代码渲染和深色模式下的问题[#139][#154]
## v2.8.1
`2023-02-27`
### BugFix
- 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146]
## v2.8.0
`2023-02-27`
- 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容)
- 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。
- 建议更新到此版本获得比较完整的体验
### Enhancement
- 优化了部份代码和类型提示
- 输入框添加换行提示
- 移动端输入框现在回车为换行,而不是直接提交
- 移动端双击标题返回顶部,箭头返回底部
### BugFix
- 流输出数据下的问题[#122]
- 修复了 `API Key` 下部份代码不换行的问题
- 修复移动端深色模式部份样式问题[#123][#126]
- 修复主题模式图标不一致的问题[#126]
## v2.7.3
`2023-02-25`
### Feature
- 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103)
### BugFix
- 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117)
## v2.7.2
`2023-02-24`
### Enhancement
- 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法
- 移除测试无用函数
## v2.7.1
`2023-02-23`
因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式
### Feature
- 现在可以中断请求过长没有答复的消息
- 现在可以删除单条消息
- 设置中显示当前版本信息
### BugFix
- 回退 `2.7.0` 的消息不稳定的问题
## v2.7.0
`2023-02-23`
### Feature
- 使用消息流返回信息,反应更迅速
### Enhancement
- 样式的一点小改动
## v2.6.2
`2023-02-22`
### BugFix
- 还原修改代理导致的异常问题
## v2.6.1
`2023-02-22`
### Feature
- 新增 `Railway` 部署模版
### BugFix
- 手动打包 `Proxy` 问题
## v2.6.0
`2023-02-21`
### Feature
- 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51)
- 前端页面设置按钮显示查看当前后端服务配置
### Enhancement
- 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62)
## v2.5.2
`2023-02-21`
### Feature
- 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77)
### BugFix
- 重载会话时滚动条保持
## v2.5.1
`2023-02-21`
### Enhancement
- 调整路由模式为 `hash`
- 调整新增会话添加到
- 调整移动端样式
## v2.5.0
`2023-02-20`
### Feature
- 会话 `loading` 现在显示为光标动画
- 会话现在可以再次生成回复
- 会话异常可以再次进行请求
- 所有删除选项添加确认操作
### Enhancement
- 调整 `chat` 为路由页面而不是组件形式
- 更新依赖至最新
- 调整移动端体验
### BugFix
- 修复移动端左侧菜单显示不完整的问题
## v2.4.1
`2023-02-18`
### Enhancement
- 调整部份移动端上的样式
- 输入框支持换行
## v2.4.0
`2023-02-17`
### Feature
- 响应式支持移动端
### Enhancement
- 修改部份描述错误
## v2.3.3
`2023-02-16`
### Feature
- 添加 `README` 部份说明和贡献列表
- 添加 `docker` 镜像
- 添加 `GitHub Action` 自动化构建
### BugFix
- 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896)
## v2.3.2
`2023-02-16`
### Enhancement
- 更新依赖至最新
- 优化部份内容
## v2.3.1
`2023-02-15`
### BugFix
- 修复多会话状态下一些意想不到的问题
## v2.3.0
`2023-02-15`
### Feature
- 代码类型信息高亮显示
- 支持 `node ^16` 版本
- 移动端响应式初步支持
- `vite``proxy` 代理
### Enhancement
- 调整超时处理范围
### BugFix
- 修复取消请求错误提示会添加到信息中
- 修复部份情况下提交请求不可用
- 修复侧边栏宽度变化闪烁的问题
## v2.2.0
`2023-02-14`
### Feature
- 会话和上下文本地储存
- 侧边栏本地储存
## v2.1.0
`2023-02-14`
### Enhancement
- 更新依赖至最新
- 联想功能移动至前端提交,后端只做转发
### BugFix
- 修复部份项目检测有关 `Bug`
- 修复清除上下文按钮失效
## v2.0.0
`2023-02-13`
### Refactor
重构并优化大部分内容
## v1.0.5
`2023-02-12`
### Enhancement
- 输入框焦点,连续提交
### BugFix
- 修复信息框样式问题
- 修复中文输入法提交问题
## v1.0.4
`2023-02-11`
### Feature
- 支持上下文联想
## v1.0.3
`2023-02-11`
### Enhancement
- 拆分 `service` 文件以便扩展
- 调整 `Eslint` 相关验证
### BugFix
- 修复部份控制台报错
## v1.0.2
`2023-02-10`
### BugFix
- 修复新增信息容器不会自动滚动到问题
- 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1)

View File

@@ -0,0 +1,49 @@
# Contribution Guide
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.
## Semantic Versioning
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.
Each major change will be recorded in the `changelog`.
## Submitting Pull Request
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.
2. Install the `pnpm` tool using `npm install pnpm -g`.
3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`.
4. Execute `pnpm bootstrap` in the root directory.
5. Execute `pnpm install` in the `/service/` directory.
6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done.
7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check.
8. Execute `pnpm type-check` in the root directory to perform a type check.
9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines).
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).
## Commit Guidelines
Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/):
```bash
<type>[optional scope]: <description>
[optional body]
[optional footer]
```
### Commit Types
The following is a list of commit types:
- feat: New feature or functionality
- fix: Bug fix
- docs: Documentation update
- style: Code style or component style update
- refactor: Code refactoring, no new features or bug fixes introduced
- perf: Performance optimization
- test: Unit test
- chore: Other commits that do not modify src or test files
## License
[MIT](./license)

View File

@@ -0,0 +1,49 @@
# 贡献指南
感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。
## 语义化版本
该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。
每个重大更改都将记录在 `changelog` 中。
## 提交 Pull Request
1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。
2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。
3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。
4. 根目录下执行 `pnpm bootstrap`
5. `/service/` 目录下执行 `pnpm install`
6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。
7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。
8. 请在根目录下执行 `pnpm type-check` 进行类型检查。
9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南)
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)。
## Commit 指南
Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)
```bash
<类型>[可选 范围]: <描述>
[可选 正文]
[可选 脚注]
```
### Commit 类型
以下是 commit 类型列表:
- feat: 新特性或功能
- fix: 缺陷修复
- docs: 文档更新
- style: 代码风格或者组件样式更新
- refactor: 代码重构,不引入新功能和缺陷修复
- perf: 性能优化
- test: 单元测试
- chore: 其他不修改 src 或测试文件的提交
## License
[MIT](./license)

12
ai-chat-web/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM quay.io/0voice/node:lts-alpine AS frontend
RUN npm install pnpm -g
COPY . /app
WORKDIR /app
RUN pnpm bootstrap
RUN pnpm run build-only
FROM quay.io/0voice/nginx:1.25.4 AS web
COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=frontend /app/dist/ /usr/share/nginx/html/
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,64 @@
# ai-chat-web
## node 安装
### windows
1. 上[官网](https://nodejs.org/en)下载18.16.0 LTS版本
2. 查看node 是否安装成功
```
node -v
npm -v
```
3. 安装pnpm
```
npm install pnpm -g
```
### ubuntu
1. 设置 apt 源,设置后可查看/etc/apt/sources.list.d/nodesource.list 文件
```
curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash -
```
2. 安装nodejs
```
sudo apt-get install -y nodejs
```
3. 验证
```
node -v
npm -v
```
4. 安装pnpm
```
sudo npm install pnpm -g
```
## 编译运行
1. 依赖安装
```
pnpm bootstrap
```
2. 本地运行
```
pnpm dev
```
3. 打包发布版本
```
pnpm build-only
```
## 提交代码的规则
```
* commitlint 规则是指在提交代码时要遵循的规范,常见的 commitlint 规则如下:
* type用于说明 commit 的类型,例如 feat新功能、fix修复 bug、docs文档更新、style样式修改、refactor重构代码等。
* scope用于说明 commit 影响的范围,例如组件、模块、页面等。
* subject用于简短地描述 commit 的内容,建议不超过 50 个字符。
* body用于详细描述 commit 的改动内容,可以分成多行。
* footer用于关闭 issue 或者添加相关链接等信息。
* 长度限制commit message 不应该过长,一般不超过 72 个字符。
```

View File

@@ -0,0 +1,3 @@
{
"extends": ["@commitlint/config-conventional"]
}

View File

@@ -0,0 +1,8 @@
**/node_modules
*/node_modules
node_modules
Dockerfile
.*
*/.*
!.env
!.env.production

View File

@@ -0,0 +1,11 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = tab
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

View File

@@ -0,0 +1,12 @@
# Glob API URL
VITE_GLOB_API_URL=/api
VITE_APP_API_BASE_URL=http://localhost:7080/
# Whether long replies are supported, which may result in higher API fees
VITE_GLOB_OPEN_LONG_REPLY=false
# When you want to use PWA
VITE_GLOB_APP_PWA=false
VITE_USER_CENTER="http://localhost:8082?sys=ai"

View File

@@ -0,0 +1,8 @@
VITE_GLOB_API_URL=/api
# Whether long replies are supported, which may result in higher API fees
VITE_GLOB_OPEN_LONG_REPLY=false
# When you want to use PWA
VITE_GLOB_APP_PWA=false
VITE_USER_CENTER="https://user.0voice.com?sys=ai"

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['@antfu'],
}

17
ai-chat-web/ai-chat-web/.gitattributes vendored Normal file
View File

@@ -0,0 +1,17 @@
"*.vue" eol=lf
"*.js" eol=lf
"*.ts" eol=lf
"*.jsx" eol=lf
"*.tsx" eol=lf
"*.cjs" eol=lf
"*.cts" eol=lf
"*.mjs" eol=lf
"*.mts" eol=lf
"*.json" eol=lf
"*.html" eol=lf
"*.css" eol=lf
"*.less" eol=lf
"*.scss" eol=lf
"*.sass" eol=lf
"*.styl" eol=lf
"*.md" eol=lf

View File

@@ -0,0 +1,41 @@
name: build_docker
on:
push:
branches: [main]
release:
types: [created] # 表示在创建新的 Release 时触发
jobs:
build_docker:
name: Build docker
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- run: |
echo "本次构建的版本为:${GITHUB_REF_NAME} (但是这个变量目前上下文中无法获取到)"
echo 本次构建的版本为:${{ github.ref_name }}
env
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v4
with:
context: .
push: true
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:${{ github.ref_name }}
${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-web:latest

View File

@@ -0,0 +1,47 @@
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Setup
run: npm i -g @antfu/ni
- name: Install
run: nci
- name: Lint
run: nr lint:fix
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: Setup
run: npm i -g @antfu/ni
- name: Install
run: nci
- name: Typecheck
run: nr type-check

32
ai-chat-web/ai-chat-web/.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables files
/service/.env

View File

@@ -0,0 +1,59 @@
variables:
DEPLOY_ENV: ''
DEPLOY_IMG: ''
REPO: 'chatgpt-frontend'
SERVICE_NAME: 'chatgpt-stack_chatgpt-frontend'
workflow:
rules:
- if: $CI_COMMIT_BRANCH == "dev" && $CI_PIPELINE_SOURCE == "push"
variables:
DEPLOY_ENV: 'dev'
DEPLOY_IMG: "${REPO}:${CI_COMMIT_SHORT_SHA}"
- if: $CI_COMMIT_TAG
variables:
DEPLOY_ENV: 'prod'
DEPLOY_IMG: "${REPO}:${CI_COMMIT_TAG}"
- when: never
stages:
# 编译阶段
- build
# 部署阶段(部署到测试环境/部署到生产环境)
- deploy
build-job:
stage: build
tags:
- builder
before_script:
- docker login -u ${DOCKER_REGISTRY_USER} -p ${DOCKER_REGISTRY_PWD} ${DOCKER_REGISTRY}
script:
- docker build -t ${DOCKER_REGISTRY}/${DEPLOY_IMG} .
- docker push ${DOCKER_REGISTRY}/${DEPLOY_IMG}
deploy-dev-job:
only:
variables:
- $DEPLOY_ENV == "dev"
stage: deploy
tags:
- deployer
variables:
CONF_RM_STR: ""
before_script:
- docker login -u ${DOCKER_REGISTRY_USER} -p ${DOCKER_REGISTRY_PWD} ${DOCKER_REGISTRY}
script:
- docker service update ${SERVICE_NAME} --image ${DOCKER_REGISTRY}/${DEPLOY_IMG}
deploy-prod-job:
only:
variables:
- $DEPLOY_ENV == "prod"
stage: deploy
tags:
- deployer
variables:
CONF_RM_STR: ""
before_script:
- docker login -u ${DOCKER_REGISTRY_USER} -p ${DOCKER_REGISTRY_PWD} ${DOCKER_REGISTRY}
script:
- docker service update ${SERVICE_NAME} --image ${DOCKER_REGISTRY}/${DEPLOY_IMG}

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

View File

@@ -0,0 +1 @@
strict-peer-dependencies=false

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"]
}

View File

@@ -0,0 +1,65 @@
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"json",
"jsonc",
"json5",
"yaml",
"yml",
"markdown"
],
"cSpell.words": [
"antfu",
"axios",
"bumpp",
"chatgpt",
"chenzhaoyu",
"commitlint",
"davinci",
"dockerhub",
"esno",
"GPTAPI",
"highlightjs",
"hljs",
"iconify",
"katex",
"katexmath",
"linkify",
"logprobs",
"mdhljs",
"mila",
"nodata",
"OPENAI",
"pinia",
"Popconfirm",
"rushstack",
"Sider",
"tailwindcss",
"traptitech",
"tsup",
"Typecheck",
"unplugin",
"VITE",
"vueuse",
"Zhao"
],
"i18n-ally.enabledParsers": [
"ts"
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.keystyle": "nested"
}

View File

@@ -0,0 +1,548 @@
## v2.10.8
`2023-03-23`
如遇问题,请删除 `node_modules` 重新安装依赖。
## Feature
- 显示回复消息原文的选项 [[yilozt](https://github.com/Chanzhaoyu/chatgpt-web/pull/672)]
- 添加单 `IP` 每小时请求限制。环境变量: `MAX_REQUEST_PER_HOUR` [[zhuxindong ](https://github.com/Chanzhaoyu/chatgpt-web/pull/718)]
- 前端添加角色设定,仅 `API` 方式可见 [[quzard](https://github.com/Chanzhaoyu/chatgpt-web/pull/768)]
- `OPENAI_API_MODEL` 变量现在对 `ChatGPTUnofficialProxyAPI` 也生效,注意:`Token``API` 的模型命名不一致,不能直接填入 `gpt-3.5` 或者 `gpt-4` [[hncboy](https://github.com/Chanzhaoyu/chatgpt-web/pull/632)]
- 添加繁体中文 `Prompts` [[PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/796)]
## Enhancement
- 重置回答时滚动定位至该回答 [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/781)]
-`API``gpt-4` 时增加可用的 `Max Tokens` [[simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/729)]
- 判断和忽略回复字符 [[liut](https://github.com/Chanzhaoyu/chatgpt-web/pull/474)]
- 切换会话时,自动聚焦输入框 [[JS-an](https://github.com/Chanzhaoyu/chatgpt-web/pull/735)]
- 渲染的链接新窗口打开
- 查询余额可选 `API_BASE_URL` 代理地址
- `config` 接口添加验证防止被无限制调用
- `PWA` 默认不开启,现在需手动修改 `.env` 文件 `VITE_GLOB_APP_PWA` 变量
- 当网络连接时,刷新页面,`500` 错误页自动跳转到主页
## BugFix
- `scrollToBottom` 调回 `scrollToBottomIfAtBottom` [[shunyue1320](https://github.com/Chanzhaoyu/chatgpt-web/pull/771)]
- 重置异常的 `loading` 会话
## Common
- 创建 `start.cmd``windows` 下也可以运行 [vulgatecnn](https://github.com/Chanzhaoyu/chatgpt-web/pull/656)]
- 添加 `visual-studio-code` 中调试配置 [[ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/296)]
- 修复文档中 `docker` 端口为本地 [[kilvn](https://github.com/Chanzhaoyu/chatgpt-web/pull/802)]
## Other
- 依赖更新
## v2.10.7
`2023-03-17`
## BugFix
- 回退 `chatgpt` 版本,原因:导致 `OPENAI_API_BASE_URL` 代理失效
- 修复缺省状态的 `usingContext` 默认值
## v2.10.6
`2023-03-17`
## Feature
- 显示 `API` 余额 [[pzcn](https://github.com/Chanzhaoyu/chatgpt-web/pull/582)]
## Enhancement
- 美化滚动条样式和 `UI` 保持一致 [[haydenull](https://github.com/Chanzhaoyu/chatgpt-web/pull/617)]
- 优化移动端 `Prompt` 样式 [[CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/608)]
- 上下文开关改为全局开关,现在记录在本地缓存中
- 配置信息按接口类型显示
## Perf
- 优化函数方法 [[kirklin](https://github.com/Chanzhaoyu/chatgpt-web/pull/583)]
- 字符错误 [[pdsuwwz](https://github.com/Chanzhaoyu/chatgpt-web/pull/585)]
- 文档描述错误 [[lizhongyuan3](https://github.com/Chanzhaoyu/chatgpt-web/pull/636)]
## BugFix
- 修复 `Prompt` 导入、导出兼容性错误
- 修复 `highlight.js` 控制台兼容性警告
## Other
- 依赖更新
## v2.10.5
`2023-03-13`
更新依赖,`access_token` 默认代理为 [acheong08](https://github.com/acheong08) 的 `https://bypass.duti.tech/api/conversation`
## Feature
- `Prompt` 商店在线导入可以导入两种 `recommend.json`里提到的模板 [simonwu53](https://github.com/Chanzhaoyu/chatgpt-web/pull/521)
- 支持 `HTTPS_PROXY` [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/308)
- `Prompt` 添加查询筛选
## Enhancement
- 调整输入框最大行数 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/502)
- 优化 `docker` 打包 [whatwewant](https://github.com/Chanzhaoyu/chatgpt-web/pull/520)
- `Prompt` 添加翻译和优化布局
- 「繁体中文」补全和审阅 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/542)
- 语言选择调整为下路框形式
- 权限输入框类型调整为密码形式
## BugFix
- `JSON` 导入检查 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/523)
- 修复 `AUTH_SECRET_KEY` 模式下跨域异常并添加对 `node.js 19` 版本的支持 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/499)
- 确定清空上下文时不应该重置会话标题
## Other
- 调整文档
- 更新依赖
## v2.10.4
`2023-03-11`
## Feature
- 感谢 [Nothing1024](https://github.com/Chanzhaoyu/chatgpt-web/pull/268) 添加 `Prompt` 模板和 `Prompt` 商店支持
## Enhancement
- 设置添加关闭按钮[#495]
## Demo
![Prompt](https://camo.githubusercontent.com/6a51af751eb29238cb7ef4f8fbd89f63db837562f97f33273095424e62dc9194/68747470733a2f2f73312e6c6f63696d672e636f6d2f323032332f30332f30342f333036326665633163613562632e676966)
## v2.10.3
`2023-03-10`
> 声明:除 `ChatGPTUnofficialProxyAPI` 使用的非官方代理外,本项目代码包括上游引用包均开源在 `GitHub`如果你觉得本项目有监控后门或有问题导致你的账号、API被封那我很抱歉。我可能`BUG`写的多,但我不缺德。此次主要为前端界面调整,周末愉快。
## Feature
- 支持长回复 [[yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)][[详情](https://github.com/Chanzhaoyu/chatgpt-web/pull/450)]
- 支持 `PWA` [[chenxch](https://github.com/Chanzhaoyu/chatgpt-web/pull/452)]
## Enhancement
- 调整移动端按钮和优化布局
- 调整 `iOS` 上安全距离
- 简化 `docker-compose` 部署 [[cloudGrin](https://github.com/Chanzhaoyu/chatgpt-web/pull/466)]
## BugFix
- 修复清空会话侧边栏标题不会重置的问题 [[RyanXinOne](https://github.com/Chanzhaoyu/chatgpt-web/pull/453)]
- 修复设置文字过长时导致的设置按钮消失的问题
## Other
- 更新依赖
## v2.10.2
`2023-03-09`
衔接 `2.10.1` 版本[详情](https://github.com/Chanzhaoyu/chatgpt-web/releases/tag/v2.10.1)
## Enhancement
- 移动端下输入框获得焦点时左侧按钮隐藏
## BugFix
- 修复 `2.10.1` 中添加 `OPENAI_API_MODEL` 变量的判断错误,会导致默认模型指定失效,抱歉
- 回退 `2.10.1` 中前端变量影响 `Docker` 打包
## v2.10.1
`2023-03-09`
注意:删除了 `.env` 文件改用 `.env.example` 代替,如果是手动部署的同学现在需要手动创建 `.env` 文件并从 `.env.example` 中复制需要的变量,并且 `.env` 文件现在会在 `Git` 提交中被忽略,原因如下:
- 在项目中添加 `.env` 从一开始就是个错误的示范
- 如果是 `Fork` 项目进行修改测试总是会被 `Git` 修改提示给打扰
- 感谢 [yi-ge](https://github.com/Chanzhaoyu/chatgpt-web/pull/395) 的提醒和修改
这两天开始,官方已经开始对第三方代理进行了拉闸, `accessToken` 即将或已经开始可能会不可使用。异常 `API` 使用也开始封号,封号缘由不明,如果出现使用 `API` 提示错误,请查看后端控制台信息,或留意邮箱。
## Feature
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/393) 添加是否发送上下文开关功能
## Enhancement
- 感谢 [nagaame](https://github.com/Chanzhaoyu/chatgpt-web/pull/415) 优化`docker`打包镜像文件过大的问题
- 感谢 [xieccc](https://github.com/Chanzhaoyu/chatgpt-web/pull/404) 新增 `API` 模型配置变量 `OPENAI_API_MODEL`
- 感谢 [acongee](https://github.com/Chanzhaoyu/chatgpt-web/pull/394) 优化输出时滚动条问题
## BugFix
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/392) 修复导出图片会丢失头像的问题
- 修复深色模式导出图片的样式问题
## v2.10.0
`2023-03-07`
- 老规矩,手动部署的同学需要删除 `node_modules` 安装包重新安装降低出错概率,其他部署不受影响,但是可能会有缓存问题。
- 虽然说了更新放缓,但是 `issues` 不看, `PR` 不改我睡不着,我的邮箱从每天早上`8`点到凌晨`12`永远在滴滴滴,所以求求各位,超时的`issues`自己关闭下哈,我真的需要缓冲一下。
- 演示图片请看最后
## Feature
- 添加权限功能,用法:`service/.env` 中的 `AUTH_SECRET_KEY` 变量添加密码
- 感谢 [PeterDaveHello](https://github.com/Chanzhaoyu/chatgpt-web/pull/348) 添加「繁体中文」翻译
- 感谢 [GermMC](https://github.com/Chanzhaoyu/chatgpt-web/pull/369) 添加聊天记录导入、导出、清空的功能
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/374) 添加会话保存为本地图片的功能
## Enhancement
- 感谢 [CornerSkyless](https://github.com/Chanzhaoyu/chatgpt-web/pull/363) 添加 `ctrl+enter` 发送消息
- 现在新消息只有在结束了之后才滚动到底部,而不是之前的强制性
- 优化部分代码
## BugFix
- 转义状态码前端显示,防止直接暴露 `key`(我可能需要更多的状态码补充)
## Other
- 更新依赖到最新
## 演示
> 不是界面最新效果,有美化改动
权限
![权限](https://user-images.githubusercontent.com/24789441/223438518-80d58d42-e344-4e39-b87c-251ff73925ed.png)
聊天记录导出
![聊天记录导出](https://user-images.githubusercontent.com/57023771/223372153-6d8e9ec1-d82c-42af-b4bd-232e50504a25.gif)
保存图片到本地
![保存图片到本地](https://user-images.githubusercontent.com/13901424/223423555-b69b95ef-8bcf-4951-a7c9-98aff2677e18.gif)
## v2.9.3
`2023-03-06`
## Enhancement
- 感谢 [ChandlerVer5](https://github.com/Chanzhaoyu/chatgpt-web/pull/305) 使用 `markdown-it` 替换 `marked`,解决代码块闪烁的问题
- 感谢 [shansing](https://github.com/Chanzhaoyu/chatgpt-web/pull/277) 改善文档
- 感谢 [nalf3in](https://github.com/Chanzhaoyu/chatgpt-web/pull/293) 添加英文翻译
## BugFix
- 感谢[sepcnt ](https://github.com/Chanzhaoyu/chatgpt-web/pull/279) 修复切换记录时编辑状态未关闭的问题
- 修复复制代码的兼容性报错问题
- 修复部分优化小问题
## v2.9.2
`2023-03-04`
手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。
### Feature
- 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持
- 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持
- 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249]
## Enhancement
- 优化在高分屏上主题内容的最大宽度[#257]
- 现在文字按单词截断[#215][#225]
### BugFix
- 修复动态生成时代码块不能被复制的问题[#251][#260]
- 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256]
- 修复控制台渲染警告
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.1
`2023-03-02`
### Feature
- 代码块添加当前代码语言显示和复制功能[#197][#196]
- 完善多语言,现在可以切换中英文显示
## Enhancement
- 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档
### BugFix
- 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题
## Other
- 更新依赖至最新
- 修改 `README` 内容
## v2.9.0
`2023-03-02`
### Feature
- 现在能复制带格式的消息文本
- 新设计的设定页面,可以自定义姓名、描述、头像(链接方式)
- 新增`403``404`页面以便扩展
## Enhancement
- 更新 `chatgpt` 使 `ChatGPTAPI` 支持 `gpt-3.5-turbo-0301`(默认)
- 取消了前端超时限制设定
## v2.8.3
`2023-03-01`
### Feature
- 消息已输出内容不会因为中断而消失[#167]
- 添加复制消息按钮[#133]
### Other
- `README` 添加声明内容
## v2.8.2
`2023-02-28`
### Enhancement
- 代码主题调整为 `One Dark - light|dark` 适配深色模式
### BugFix
- 修复普通文本代码渲染和深色模式下的问题[#139][#154]
## v2.8.1
`2023-02-27`
### BugFix
- 修复 `API` 版本不是 `Markdown` 时,普通 `HTML` 代码会被渲染的问题 [#146]
## v2.8.0
`2023-02-27`
- 感谢 [puppywang](https://github.com/Chanzhaoyu/chatgpt-web/commit/628187f5c3348bda0d0518f90699a86525d19018) 修复了 `2.7.0` 版本中关于流输出数据的问题(使用 `nginx` 需要自行配置 `octet-stream` 相关内容)
- 关于为什么使用 `octet-stream` 而不是 `sse`,是因为更好的兼容之前的模式。
- 建议更新到此版本获得比较完整的体验
### Enhancement
- 优化了部份代码和类型提示
- 输入框添加换行提示
- 移动端输入框现在回车为换行,而不是直接提交
- 移动端双击标题返回顶部,箭头返回底部
### BugFix
- 流输出数据下的问题[#122]
- 修复了 `API Key` 下部份代码不换行的问题
- 修复移动端深色模式部份样式问题[#123][#126]
- 修复主题模式图标不一致的问题[#126]
## v2.7.3
`2023-02-25`
### Feature
- 适配系统深色模式 [#118](https://github.com/Chanzhaoyu/chatgpt-web/issues/103)
### BugFix
- 修复用户消息能被渲染为 `HTML` 问题 [#117](https://github.com/Chanzhaoyu/chatgpt-web/issues/117)
## v2.7.2
`2023-02-24`
### Enhancement
- 消息使用 [github-markdown-css](https://www.npmjs.com/package/github-markdown-css) 进行美化,现在支持全语法
- 移除测试无用函数
## v2.7.1
`2023-02-23`
因为消息流在 `accessToken` 中存在解析失败和消息不完整等一系列的问题,调整回正常消息形式
### Feature
- 现在可以中断请求过长没有答复的消息
- 现在可以删除单条消息
- 设置中显示当前版本信息
### BugFix
- 回退 `2.7.0` 的消息不稳定的问题
## v2.7.0
`2023-02-23`
### Feature
- 使用消息流返回信息,反应更迅速
### Enhancement
- 样式的一点小改动
## v2.6.2
`2023-02-22`
### BugFix
- 还原修改代理导致的异常问题
## v2.6.1
`2023-02-22`
### Feature
- 新增 `Railway` 部署模版
### BugFix
- 手动打包 `Proxy` 问题
## v2.6.0
`2023-02-21`
### Feature
- 新增对 `网页 accessToken` 调用 `ChatGPT`,更智能不过不太稳定 [#51](https://github.com/Chanzhaoyu/chatgpt-web/issues/51)
- 前端页面设置按钮显示查看当前后端服务配置
### Enhancement
- 新增 `TIMEOUT_MS` 环境变量设定后端超时时常(单位:毫秒)[#62](https://github.com/Chanzhaoyu/chatgpt-web/issues/62)
## v2.5.2
`2023-02-21`
### Feature
- 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77)
### BugFix
- 重载会话时滚动条保持
## v2.5.1
`2023-02-21`
### Enhancement
- 调整路由模式为 `hash`
- 调整新增会话添加到
- 调整移动端样式
## v2.5.0
`2023-02-20`
### Feature
- 会话 `loading` 现在显示为光标动画
- 会话现在可以再次生成回复
- 会话异常可以再次进行请求
- 所有删除选项添加确认操作
### Enhancement
- 调整 `chat` 为路由页面而不是组件形式
- 更新依赖至最新
- 调整移动端体验
### BugFix
- 修复移动端左侧菜单显示不完整的问题
## v2.4.1
`2023-02-18`
### Enhancement
- 调整部份移动端上的样式
- 输入框支持换行
## v2.4.0
`2023-02-17`
### Feature
- 响应式支持移动端
### Enhancement
- 修改部份描述错误
## v2.3.3
`2023-02-16`
### Feature
- 添加 `README` 部份说明和贡献列表
- 添加 `docker` 镜像
- 添加 `GitHub Action` 自动化构建
### BugFix
- 回退依赖更新导致的 [Eslint 报错](https://github.com/eslint/eslint/issues/16896)
## v2.3.2
`2023-02-16`
### Enhancement
- 更新依赖至最新
- 优化部份内容
## v2.3.1
`2023-02-15`
### BugFix
- 修复多会话状态下一些意想不到的问题
## v2.3.0
`2023-02-15`
### Feature
- 代码类型信息高亮显示
- 支持 `node ^16` 版本
- 移动端响应式初步支持
- `vite``proxy` 代理
### Enhancement
- 调整超时处理范围
### BugFix
- 修复取消请求错误提示会添加到信息中
- 修复部份情况下提交请求不可用
- 修复侧边栏宽度变化闪烁的问题
## v2.2.0
`2023-02-14`
### Feature
- 会话和上下文本地储存
- 侧边栏本地储存
## v2.1.0
`2023-02-14`
### Enhancement
- 更新依赖至最新
- 联想功能移动至前端提交,后端只做转发
### BugFix
- 修复部份项目检测有关 `Bug`
- 修复清除上下文按钮失效
## v2.0.0
`2023-02-13`
### Refactor
重构并优化大部分内容
## v1.0.5
`2023-02-12`
### Enhancement
- 输入框焦点,连续提交
### BugFix
- 修复信息框样式问题
- 修复中文输入法提交问题
## v1.0.4
`2023-02-11`
### Feature
- 支持上下文联想
## v1.0.3
`2023-02-11`
### Enhancement
- 拆分 `service` 文件以便扩展
- 调整 `Eslint` 相关验证
### BugFix
- 修复部份控制台报错
## v1.0.2
`2023-02-10`
### BugFix
- 修复新增信息容器不会自动滚动到问题
- 修复文本过长不换行到问题 [#1](https://github.com/Chanzhaoyu/chatgpt-web/issues/1)

View File

@@ -0,0 +1,49 @@
# Contribution Guide
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.
## Semantic Versioning
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.
Each major change will be recorded in the `changelog`.
## Submitting Pull Request
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.
2. Install the `pnpm` tool using `npm install pnpm -g`.
3. Install the `Eslint` plugin for `VSCode`, or enable `eslint` functionality for other editors such as `WebStorm`.
4. Execute `pnpm bootstrap` in the root directory.
5. Execute `pnpm install` in the `/service/` directory.
6. Make changes to the codebase. If applicable, ensure that appropriate testing has been done.
7. Execute `pnpm lint:fix` in the root directory to perform a code formatting check.
8. Execute `pnpm type-check` in the root directory to perform a type check.
9. Submit a git commit, following the [Commit Guidelines](#commit-guidelines).
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).
## Commit Guidelines
Commit messages should follow the [conventional-changelog standard](https://www.conventionalcommits.org/en/v1.0.0/):
```bash
<type>[optional scope]: <description>
[optional body]
[optional footer]
```
### Commit Types
The following is a list of commit types:
- feat: New feature or functionality
- fix: Bug fix
- docs: Documentation update
- style: Code style or component style update
- refactor: Code refactoring, no new features or bug fixes introduced
- perf: Performance optimization
- test: Unit test
- chore: Other commits that do not modify src or test files
## License
[MIT](./license)

View File

@@ -0,0 +1,49 @@
# 贡献指南
感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。
## 语义化版本
该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。
每个重大更改都将记录在 `changelog` 中。
## 提交 Pull Request
1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。
2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。
3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。
4. 根目录下执行 `pnpm bootstrap`
5. `/service/` 目录下执行 `pnpm install`
6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。
7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。
8. 请在根目录下执行 `pnpm type-check` 进行类型检查。
9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南)
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)。
## Commit 指南
Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)
```bash
<类型>[可选 范围]: <描述>
[可选 正文]
[可选 脚注]
```
### Commit 类型
以下是 commit 类型列表:
- feat: 新特性或功能
- fix: 缺陷修复
- docs: 文档更新
- style: 代码风格或者组件样式更新
- refactor: 代码重构,不引入新功能和缺陷修复
- perf: 性能优化
- test: 单元测试
- chore: 其他不修改 src 或测试文件的提交
## License
[MIT](./license)

View File

@@ -0,0 +1,14 @@
FROM quay.io/0voice/node:lts-alpine AS frontend
RUN npm install pnpm -g
COPY . /app
WORKDIR /app
RUN pnpm bootstrap
RUN pnpm run build-only
FROM quay.io/0voice/nginx:1.25.4 as web
COPY --from=frontend /app/dist/ /usr/share/nginx/html/
# 指定入口程序
ENTRYPOINT ["/docker-entrypoint.sh"]
# 指定容器的启动命令或者入口程序的参数
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,64 @@
# ai-chat-web
## node 安装
### windows
1. 上[官网](https://nodejs.org/en)下载18.16.0 LTS版本
2. 查看node 是否安装成功
```
node -v
npm -v
```
3. 安装pnpm
```
npm install pnpm -g
```
### ubuntu
1. 设置 apt 源,设置后可查看/etc/apt/sources.list.d/nodesource.list 文件
```
curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash -
```
2. 安装nodejs
```
sudo apt-get install -y nodejs
```
3. 验证
```
node -v
npm -v
```
4. 安装pnpm
```
sudo npm install pnpm -g
```
## 编译运行
1. 依赖安装
```
pnpm bootstrap
```
2. 本地运行
```
pnpm dev
```
3. 打包发布版本
```
pnpm build-only
```
## 提交代码的规则
```
* commitlint 规则是指在提交代码时要遵循的规范,常见的 commitlint 规则如下:
* type用于说明 commit 的类型,例如 feat新功能、fix修复 bug、docs文档更新、style样式修改、refactor重构代码等。
* scope用于说明 commit 影响的范围,例如组件、模块、页面等。
* subject用于简短地描述 commit 的内容,建议不超过 50 个字符。
* body用于详细描述 commit 的改动内容,可以分成多行。
* footer用于关闭 issue 或者添加相关链接等信息。
* 长度限制commit message 不应该过长,一般不超过 72 个字符。
```

View File

@@ -0,0 +1 @@
export * from './proxy'

View File

@@ -0,0 +1,16 @@
import type { ProxyOptions } from 'vite'
export function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) {
if (!isOpenProxy)
return
const proxy: Record<string, string | ProxyOptions> = {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true,
rewrite: path => path.replace('/api/', '/'),
},
}
return proxy
}

View File

@@ -0,0 +1,41 @@
version: '3'
services:
app:
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
ports:
- 3002:3002
environment:
# 二选一
OPENAI_API_KEY: sk-xxx
# 二选一
OPENAI_ACCESS_TOKEN: xxx
# API接口地址可选设置 OPENAI_API_KEY 时可用
OPENAI_API_BASE_URL: xxx
# API模型可选设置 OPENAI_API_KEY 时可用
OPENAI_API_MODEL: xxx
# 反向代理,可选
API_REVERSE_PROXY: xxx
# 访问权限密钥,可选
AUTH_SECRET_KEY: xxx
# 每小时最大请求次数,可选,默认无限
MAX_REQUEST_PER_HOUR: 0
# 超时,单位毫秒,可选
TIMEOUT_MS: 60000
# Socks代理可选和 SOCKS_PROXY_PORT 一起时生效
SOCKS_PROXY_HOST: xxx
# Socks代理端口可选和 SOCKS_PROXY_HOST 一起时生效
SOCKS_PROXY_PORT: xxx
# HTTPS_PROXY 代理,可选
HTTPS_PROXY: http://xxx:7890
nginx:
image: nginx:alpine
ports:
- '80:80'
expose:
- '80'
volumes:
- ./nginx/html:/usr/share/nginx/html
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
links:
- app

View File

@@ -0,0 +1,20 @@
server {
listen 80;
server_name localhost;
charset utf-8;
error_page 500 502 503 504 /50x.html;
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
location /api {
proxy_set_header X-Real-IP $remote_addr; #转发用户IP
proxy_pass http://app:3002;
}
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

View File

@@ -0,0 +1,14 @@
### docker-compose 部署教程
- 将打包好的前端文件放到 `nginx/html` 目录下
- ```shell
# 启动
docker-compose up -d
```
- ```shell
# 查看运行状态
docker ps
```
- ```shell
# 结束运行
docker-compose down
```

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/favicon.jpg">
<meta content="yes" name="apple-mobile-web-app-capable"/>
<link rel="apple-touch-icon" href="/favicon.ico">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
<title>零声教学AI助手公测</title>
</head>
<body class="dark:bg-black">
<div id="app">
<style>
.loading-wrap {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.balls {
width: 4em;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
}
.balls div {
width: 0.8em;
height: 0.8em;
border-radius: 50%;
background-color: #4b9e5f;
}
.balls div:nth-of-type(1) {
transform: translateX(-100%);
animation: left-swing 0.5s ease-in alternate infinite;
}
.balls div:nth-of-type(3) {
transform: translateX(-95%);
animation: right-swing 0.5s ease-out alternate infinite;
}
@keyframes left-swing {
50%,
100% {
transform: translateX(95%);
}
}
@keyframes right-swing {
50% {
transform: translateX(-95%);
}
100% {
transform: translateX(100%);
}
}
@media (prefers-color-scheme: dark) {
body {
background: #121212;
}
}
</style>
<div class="loading-wrap">
<div class="balls">
<div></div>
<div></div>
<div></div>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
<script>
var _hmt = _hmt || [];
(function () {
const hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?29783f1f3a946661c3d41e96752536d6'
const s = document.getElementsByTagName('script')[0]
s.parentNode.insertBefore(hm, s)
})()
</script>
</html>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 ChenZhaoYu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,71 @@
{
"name": "chatgpt-web",
"version": "2.10.8",
"private": false,
"description": "零声教学AI助手",
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"vue"
],
"scripts": {
"dev": "vite --mode develop",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build --mode production",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bootstrap": "pnpm install && pnpm run common:prepare",
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml",
"common:prepare": "husky install"
},
"dependencies": {
"@traptitech/markdown-it-katex": "^3.6.0",
"@vueuse/core": "^9.13.0",
"highlight.js": "^11.7.0",
"html2canvas": "^1.4.1",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"naive-ui": "^2.34.3",
"pinia": "^2.0.33",
"vue": "^3.2.47",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@antfu/eslint-config": "^0.35.3",
"@commitlint/cli": "^17.4.4",
"@commitlint/config-conventional": "^17.4.4",
"@iconify/vue": "^4.1.0",
"@types/crypto-js": "^4.1.1",
"@types/katex": "^0.16.0",
"@types/markdown-it": "^12.2.3",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^4.0.0",
"autoprefixer": "^10.4.13",
"axios": "^1.3.4",
"crypto-js": "^4.1.1",
"eslint": "^8.35.0",
"husky": "^8.0.3",
"less": "^4.1.3",
"lint-staged": "^13.1.2",
"markdown-it-link-attributes": "^4.0.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.21",
"rimraf": "^4.2.0",
"tailwindcss": "^3.2.7",
"typescript": "~4.9.5",
"vite": "^4.2.0",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
}
}

6903
ai-chat-web/ai-chat-web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

View File

@@ -0,0 +1 @@
<svg id="openai-symbol" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import { NConfigProvider } from 'naive-ui'
import { NaiveProvider } from '@/components/common'
import { useTheme } from '@/hooks/useTheme'
import { useLanguage } from '@/hooks/useLanguage'
const { theme, themeOverrides } = useTheme()
const { language } = useLanguage()
</script>
<template>
<NConfigProvider
class="h-full"
:theme="theme"
:theme-overrides="themeOverrides"
:locale="language"
>
<NaiveProvider>
<RouterView />
</NaiveProvider>
</NConfigProvider>
</template>

View File

@@ -0,0 +1,65 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { post } from '@/utils/request'
import { useSettingStore } from '@/store'
export function fetchChatAPI<T = any>(
prompt: string,
options?: { conversationId?: string; parentMessageId?: string },
signal?: GenericAbortSignal,
) {
return post<T>({
url: '/chat',
data: { prompt, options },
signal,
})
}
export function fetchChatConfig<T = any>() {
return post<T>({
url: '/config',
})
}
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
options?: { conversationId?: string; parentMessageId?: string }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void },
) {
const settingStore = useSettingStore()
return post<T>({
url: '/chat-process',
data: { prompt: params.prompt, options: params.options, systemMessage: settingStore.systemMessage },
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
export function fetchSession<T>() {
return post<T>({
url: '/session',
})
}
export function fetchVerify<T>(token: string) {
return post<T>({
url: '/verify',
data: { token },
})
}
export function fetchCode<T>(phone: string) {
return post<T>({
url: '/v1/sms/send/code',
data: { phone },
})
}
export function login<T>(phone: string, code: string) {
return post<T>({
url: '/v1/user/login',
data: { user_name: phone, pwd: code, type: 1 },
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,14 @@
[
{
"key": "awesome-chatgpt-prompts-zh",
"desc": "ChatGPT 中文调教指南",
"downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh.json",
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
},
{
"key": "awesome-chatgpt-prompts-zh-TW",
"desc": "ChatGPT 中文調教指南 (透過 OpenAI / OpenCC 協助,從簡體中文轉換為繁體中文的版本)",
"downloadUrl": "https://raw.githubusercontent.com/PlexPt/awesome-chatgpt-prompts-zh/main/prompts-zh-TW.json",
"url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh"
}
]

View File

@@ -0,0 +1,20 @@
<script setup lang='ts'>
interface Emit {
(e: 'click'): void
}
const emit = defineEmits<Emit>()
function handleClick() {
emit('click')
}
</script>
<template>
<button
class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]"
@click="handleClick"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang='ts'>
import { computed } from 'vue'
import type { PopoverPlacement } from 'naive-ui'
import { NTooltip } from 'naive-ui'
import Button from './Button.vue'
interface Props {
tooltip?: string
placement?: PopoverPlacement
}
interface Emit {
(e: 'click'): void
}
const props = withDefaults(defineProps<Props>(), {
tooltip: '',
placement: 'bottom',
})
const emit = defineEmits<Emit>()
const showTooltip = computed(() => Boolean(props.tooltip))
function handleClick() {
emit('click')
}
</script>
<template>
<div v-if="showTooltip">
<NTooltip :placement="placement" trigger="hover">
<template #trigger>
<Button @click="handleClick">
<slot />
</Button>
</template>
{{ tooltip }}
</NTooltip>
</div>
<div v-else>
<Button @click="handleClick">
<slot />
</Button>
</div>
</template>

View File

@@ -0,0 +1,43 @@
<script setup lang="ts">
import { defineComponent, h } from 'vue'
import {
NDialogProvider,
NLoadingBarProvider,
NMessageProvider,
NNotificationProvider,
useDialog,
useLoadingBar,
useMessage,
useNotification,
} from 'naive-ui'
function registerNaiveTools() {
window.$loadingBar = useLoadingBar()
window.$dialog = useDialog()
window.$message = useMessage()
window.$notification = useNotification()
}
const NaiveProviderContent = defineComponent({
name: 'NaiveProviderContent',
setup() {
registerNaiveTools()
},
render() {
return h('div')
},
})
</script>
<template>
<NLoadingBarProvider>
<NDialogProvider>
<NNotificationProvider>
<NMessageProvider>
<slot />
<NaiveProviderContent />
</NMessageProvider>
</NNotificationProvider>
</NDialogProvider>
</NLoadingBarProvider>
</template>

View File

@@ -0,0 +1,478 @@
<script setup lang='ts'>
import type { DataTableColumns } from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui'
import PromptRecommend from '../../../assets/recommend.json'
import { SvgIcon } from '..'
import { usePromptStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
interface DataProps {
renderKey: string
renderValue: string
key: string
value: string
}
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const message = useMessage()
const show = computed({
get: () => props.visible,
set: (visible: boolean) => emit('update:visible', visible),
})
const showModal = ref(false)
const importLoading = ref(false)
const exportLoading = ref(false)
const searchValue = ref<string>('')
// 移动端自适应相关
const { isMobile } = useBasicLayout()
const promptStore = usePromptStore()
// Prompt在线导入推荐List,根据部署者喜好进行修改(assets/recommend.json)
const promptRecommendList = PromptRecommend
const promptList = ref<any>(promptStore.promptList)
// 用于添加修改的临时prompt参数
const tempPromptKey = ref('')
const tempPromptValue = ref('')
// Modal模式根据不同模式渲染不同的Modal内容
const modalMode = ref('')
// 这个是为了后期的修改Prompt内容考虑因为要针对无uuid的list进行修改且考虑到不能出现标题和内容的冲突所以就需要一个临时item来记录一下
const tempModifiedItem = ref<any>({})
// 添加修改导入都使用一个Modal, 临时修改内容占用tempPromptKey,切换状态前先将内容都清楚
const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => {
if (mode === 'add') {
tempPromptKey.value = ''
tempPromptValue.value = ''
}
else if (mode === 'modify') {
tempModifiedItem.value = { ...selected }
tempPromptKey.value = selected.key
tempPromptValue.value = selected.value
}
else if (mode === 'local_import') {
tempPromptKey.value = 'local_import'
tempPromptValue.value = ''
}
showModal.value = !showModal.value
modalMode.value = mode
}
// 在线导入相关
const downloadURL = ref('')
const downloadDisabled = computed(() => downloadURL.value.trim().length < 1)
const setDownloadURL = (url: string) => {
downloadURL.value = url
}
// 控制 input 按钮
const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1)
// Prompt模板相关操作
const addPromptTemplate = () => {
for (const i of promptList.value) {
if (i.key === tempPromptKey.value) {
message.error(t('store.addRepeatTitleTips'))
return
}
if (i.value === tempPromptValue.value) {
message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value }))
return
}
}
promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never)
message.success(t('common.addSuccess'))
changeShowModal('add')
}
const modifyPromptTemplate = () => {
let index = 0
// 通过临时索引把待修改项摘出来
for (const i of promptList.value) {
if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value)
break
index = index + 1
}
const tempList = promptList.value.filter((_: any, i: number) => i !== index)
// 搜索有冲突的部分
for (const i of tempList) {
if (i.key === tempPromptKey.value) {
message.error(t('store.editRepeatTitleTips'))
return
}
if (i.value === tempPromptValue.value) {
message.error(t('store.editRepeatContentTips', { msg: i.key }))
return
}
}
promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never
message.success(t('common.editSuccess'))
changeShowModal('modify')
}
const deletePromptTemplate = (row: { key: string; value: string }) => {
promptList.value = [
...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key),
] as never
message.success(t('common.deleteSuccess'))
}
const clearPromptTemplate = () => {
promptList.value = []
message.success(t('common.clearSuccess'))
}
const importPromptTemplate = () => {
try {
const jsonData = JSON.parse(tempPromptValue.value)
let key = ''
let value = ''
// 可以扩展加入更多模板字典的key
if ('key' in jsonData[0]) {
key = 'key'
value = 'value'
}
else if ('act' in jsonData[0]) {
key = 'act'
value = 'prompt'
}
else {
// 不支持的字典的key防止导入 以免破坏prompt商店打开
message.warning('prompt key not supported.')
throw new Error('prompt key not supported.')
}
for (const i of jsonData) {
if (!('key' in i) || !('value' in i))
throw new Error(t('store.importError'))
let safe = true
for (const j of promptList.value) {
if (j.key === i[key]) {
message.warning(t('store.importRepeatTitle', { msg: i[key] }))
safe = false
break
}
if (j.value === i[value]) {
message.warning(t('store.importRepeatContent', { msg: i[key] }))
safe = false
break
}
}
if (safe)
promptList.value.unshift({ key: i[key], value: i[value] } as never)
}
message.success(t('common.importSuccess'))
}
catch {
message.error('JSON 格式错误,请检查 JSON 格式')
}
}
// 模板导出
const exportPromptTemplate = () => {
exportLoading.value = true
const jsonDataStr = JSON.stringify(promptList.value)
const blob = new Blob([jsonDataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'ChatGPTPromptTemplate.json'
link.click()
URL.revokeObjectURL(url)
exportLoading.value = false
}
// 模板在线导入
const downloadPromptTemplate = async () => {
try {
importLoading.value = true
const response = await fetch(downloadURL.value)
const jsonData = await response.json()
if ('key' in jsonData[0] && 'value' in jsonData[0])
tempPromptValue.value = JSON.stringify(jsonData)
if ('act' in jsonData[0] && 'prompt' in jsonData[0]) {
const newJsonData = jsonData.map((item: { act: string; prompt: string }) => {
return {
key: item.act,
value: item.prompt,
}
})
tempPromptValue.value = JSON.stringify(newJsonData)
}
importPromptTemplate()
downloadURL.value = ''
}
catch {
message.error(t('store.downloadError'))
downloadURL.value = ''
}
finally {
importLoading.value = false
}
}
// 移动端自适应相关
const renderTemplate = () => {
const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50]
return promptList.value.map((item: { key: string; value: string }) => {
return {
renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`,
renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`,
key: item.key,
value: item.value,
}
})
}
const pagination = computed(() => {
const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15]
return {
pageSize, pageSlot,
}
})
// table相关
const createColumns = (): DataTableColumns<DataProps> => {
return [
{
title: t('store.title'),
key: 'renderKey',
},
{
title: t('store.description'),
key: 'renderValue',
},
{
title: t('common.action'),
key: 'actions',
width: 100,
align: 'center',
render(row) {
return h('div', { class: 'flex items-center flex-col gap-2' }, {
default: () => [h(
NButton,
{
tertiary: true,
size: 'small',
type: 'info',
onClick: () => changeShowModal('modify', row),
},
{ default: () => t('common.edit') },
),
h(
NButton,
{
tertiary: true,
size: 'small',
type: 'error',
onClick: () => deletePromptTemplate(row),
},
{ default: () => t('common.delete') },
),
],
})
},
},
]
}
const columns = createColumns()
watch(
() => promptList,
() => {
promptStore.updatePromptList(promptList.value)
},
{ deep: true },
)
const dataSource = computed(() => {
const data = renderTemplate()
const value = searchValue.value
if (value && value !== '') {
return data.filter((item: DataProps) => {
return item.renderKey.includes(value) || item.renderValue.includes(value)
})
}
return data
})
</script>
<template>
<NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card">
<div class="space-y-4">
<NTabs type="segment">
<NTabPane name="local" :tab="$t('store.local')">
<div
class="flex gap-3 mb-4"
:class="[isMobile ? 'flex-col' : 'flex-row justify-between']"
>
<div class="flex items-center space-x-4">
<NButton
type="primary"
size="small"
@click="changeShowModal('add')"
>
{{ $t('common.add') }}
</NButton>
<NButton
size="small"
@click="changeShowModal('local_import')"
>
{{ $t('common.import') }}
</NButton>
<NButton
size="small"
:loading="exportLoading"
@click="exportPromptTemplate()"
>
{{ $t('common.export') }}
</NButton>
<NPopconfirm @positive-click="clearPromptTemplate">
<template #trigger>
<NButton size="small">
{{ $t('common.clear') }}
</NButton>
</template>
{{ $t('store.clearStoreConfirm') }}
</NPopconfirm>
</div>
<div class="flex items-center">
<NInput v-model:value="searchValue" style="width: 100%" />
</div>
</div>
<NDataTable
v-if="!isMobile"
:max-height="400"
:columns="columns"
:data="dataSource"
:pagination="pagination"
:bordered="false"
/>
<NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;">
<NListItem v-for="(item, index) of dataSource" :key="index">
<NThing :title="item.renderKey" :description="item.renderValue" />
<template #suffix>
<div class="flex flex-col items-center gap-2">
<NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)">
{{ t('common.edit') }}
</NButton>
<NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)">
{{ t('common.delete') }}
</NButton>
</div>
</template>
</NListItem>
</NList>
</NTabPane>
<NTabPane name="download" :tab="$t('store.online')">
<p class="mb-4">
{{ $t('store.onlineImportWarning') }}
</p>
<div class="flex items-center gap-4">
<NInput v-model:value="downloadURL" placeholder="" />
<NButton
strong
secondary
:disabled="downloadDisabled"
:loading="importLoading"
@click="downloadPromptTemplate()"
>
{{ $t('common.download') }}
</NButton>
</div>
<NDivider />
<div class="max-h-[360px] overflow-y-auto space-y-4">
<NCard
v-for="info in promptRecommendList"
:key="info.key" :title="info.key"
:bordered="true"
embedded
>
<p
class="overflow-hidden text-ellipsis whitespace-nowrap"
:title="info.desc"
>
{{ info.desc }}
</p>
<template #footer>
<div class="flex items-center justify-end space-x-4">
<NButton text>
<a
:href="info.url"
target="_blank"
>
<SvgIcon class="text-xl" icon="ri:link" />
</a>
</NButton>
<NButton text @click="setDownloadURL(info.downloadUrl) ">
<SvgIcon class="text-xl" icon="ri:add-fill" />
</NButton>
</div>
</template>
</NCard>
</div>
</NTabPane>
</NTabs>
</div>
</NModal>
<NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card">
<NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical>
{{ t('store.title') }}
<NInput v-model:value="tempPromptKey" />
{{ t('store.description') }}
<NInput v-model:value="tempPromptValue" type="textarea" />
<NButton
block
type="primary"
:disabled="inputStatus"
@click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }"
>
{{ t('common.confirm') }}
</NButton>
</NSpace>
<NSpace v-if="modalMode === 'local_import'" vertical>
<NInput
v-model:value="tempPromptValue"
:placeholder="t('store.importPlaceholder')"
:autosize="{ minRows: 3, maxRows: 15 }"
type="textarea"
/>
<NButton
block
type="primary"
:disabled="inputStatus"
@click="() => { importPromptTemplate() }"
>
{{ t('common.import') }}
</NButton>
</NSpace>
</NModal>
</template>

View File

@@ -0,0 +1,75 @@
<script setup lang='ts'>
import { computed, onMounted, ref } from 'vue'
import { NSpin } from 'naive-ui'
import { fetchChatConfig } from '@/api'
import pkg from '@/../package.json'
import { useAuthStore } from '@/store'
interface ConfigState {
timeoutMs?: number
reverseProxy?: string
apiModel?: string
socksProxy?: string
httpsProxy?: string
balance?: string
}
const authStore = useAuthStore()
const loading = ref(false)
const config = ref<ConfigState>()
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
async function fetchConfig() {
try {
loading.value = true
const { data } = await fetchChatConfig<ConfigState>()
config.value = data
}
finally {
loading.value = false
}
}
onMounted(() => {
fetchConfig()
})
</script>
<template>
<NSpin :show="loading">
<div class="p-4 space-y-4">
<h2 class="text-xl font-bold">
Version - {{ pkg.version }}
</h2>
<div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
<p>
此项目开源于
<a
class="text-blue-600 dark:text-blue-500"
href="https://github.com/Chanzhaoyu/chatgpt-web"
target="_blank"
>
Github
</a>
免费且基于 MIT 协议没有任何形式的付费行为
</p>
<p>
如果你觉得此项目对你有帮助请在 Github 帮我点个 Star 或者给予一点赞助谢谢
</p>
</div>
<p>{{ $t("setting.api") }}{{ config?.apiModel ?? '-' }}</p>
<p v-if="isChatGPTAPI">
{{ $t("setting.balance") }}{{ config?.balance ?? '-' }}
</p>
<p v-if="!isChatGPTAPI">
{{ $t("setting.reverseProxy") }}{{ config?.reverseProxy ?? '-' }}
</p>
<p>{{ $t("setting.timeout") }}{{ config?.timeoutMs ?? '-' }}</p>
<p>{{ $t("setting.socks") }}{{ config?.socksProxy ?? '-' }}</p>
<p>{{ $t("setting.httpsProxy") }}{{ config?.httpsProxy ?? '-' }}</p>
</div>
</NSpin>
</template>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { NButton, NInput, useMessage } from 'naive-ui'
import { useSettingStore } from '@/store'
import type { SettingsState } from '@/store/modules/settings/helper'
import { t } from '@/locales'
const settingStore = useSettingStore()
const ms = useMessage()
const systemMessage = ref(settingStore.systemMessage ?? '')
function updateSettings(options: Partial<SettingsState>) {
settingStore.updateSetting(options)
ms.success(t('common.success'))
}
function handleReset() {
settingStore.resetSetting()
ms.success(t('common.success'))
window.location.reload()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.role') }}</span>
<div class="flex-1">
<NInput v-model:value="systemMessage" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">&nbsp;</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,223 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
import type { Language, Theme } from '@/store/modules/app/helper'
import { SvgIcon } from '@/components/common'
import { useAppStore, useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper'
import { getCurrentDate } from '@/utils/functions'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
const appStore = useAppStore()
const userStore = useUserStore()
const { isMobile } = useBasicLayout()
const ms = useMessage()
const theme = computed(() => appStore.theme)
const userInfo = computed(() => userStore.userInfo)
const avatar = ref(userInfo.value.avatar ?? '')
const name = ref(userInfo.value.name ?? '')
const description = ref(userInfo.value.description ?? '')
const language = computed({
get() {
return appStore.language
},
set(value: Language) {
appStore.setLanguage(value)
},
})
const themeOptions: { label: string; key: Theme; icon: string }[] = [
{
label: 'Auto',
key: 'auto',
icon: 'ri:contrast-line',
},
{
label: 'Light',
key: 'light',
icon: 'ri:sun-foggy-line',
},
{
label: 'Dark',
key: 'dark',
icon: 'ri:moon-foggy-line',
},
]
const languageOptions: { label: string; key: Language; value: Language }[] = [
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
{ label: 'English', key: 'en-US', value: 'en-US' },
]
function updateUserInfo(options: Partial<UserInfo>) {
userStore.updateUserInfo(options)
ms.success(t('common.success'))
}
function handleReset() {
userStore.resetUserInfo()
ms.success(t('common.success'))
window.location.reload()
}
function exportData(): void {
const date = getCurrentDate()
const data: string = localStorage.getItem('chatStorage') || '{}'
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
const url: string = URL.createObjectURL(blob)
const link: HTMLAnchorElement = document.createElement('a')
link.href = url
link.download = `chat-store_${date}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
function importData(event: Event): void {
const target = event.target as HTMLInputElement
if (!target || !target.files)
return
const file: File = target.files[0]
if (!file)
return
const reader: FileReader = new FileReader()
reader.onload = () => {
try {
const data = JSON.parse(reader.result as string)
localStorage.setItem('chatStorage', JSON.stringify(data))
ms.success(t('common.success'))
location.reload()
}
catch (error) {
ms.error(t('common.invalidFileFormat'))
}
}
reader.readAsText(file)
}
function clearData(): void {
localStorage.removeItem('chatStorage')
location.reload()
}
function handleImportButtonClick(): void {
const fileInput = document.getElementById('fileInput') as HTMLElement
if (fileInput)
fileInput.click()
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
<div class="flex-1">
<NInput v-model:value="avatar" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]">
<NInput v-model:value="name" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
<div class="flex-1">
<NInput v-model:value="description" placeholder="" />
</div>
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
{{ $t('common.save') }}
</NButton>
</div>
<div
class="flex items-center space-x-4"
:class="isMobile && 'items-start'"
>
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NButton size="small" @click="exportData">
<template #icon>
<SvgIcon icon="ri:download-2-fill" />
</template>
{{ $t('common.export') }}
</NButton>
<input id="fileInput" type="file" style="display:none" @change="importData">
<NButton size="small" @click="handleImportButtonClick">
<template #icon>
<SvgIcon icon="ri:upload-2-fill" />
</template>
{{ $t('common.import') }}
</NButton>
<NPopconfirm placement="bottom" @positive-click="clearData">
<template #trigger>
<NButton size="small">
<template #icon>
<SvgIcon icon="ri:close-circle-line" />
</template>
{{ $t('common.clear') }}
</NButton>
</template>
{{ $t('chat.clearHistoryConfirm') }}
</NPopconfirm>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
<div class="flex flex-wrap items-center gap-4">
<template v-for="item of themeOptions" :key="item.key">
<NButton
size="small"
:type="item.key === theme ? 'primary' : undefined"
@click="appStore.setTheme(item.key)"
>
<template #icon>
<SvgIcon :icon="item.icon" />
</template>
</NButton>
</template>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
<div class="flex flex-wrap items-center gap-4">
<NSelect
style="width: 140px"
:value="language"
:options="languageOptions"
@update-value="value => appStore.setLanguage(value)"
/>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
<NButton size="small" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,70 @@
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NModal, NTabPane, NTabs } from 'naive-ui'
import General from './General.vue'
import Advanced from './Advanced.vue'
import About from './About.vue'
import { useAuthStore } from '@/store'
import { SvgIcon } from '@/components/common'
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const authStore = useAuthStore()
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
const active = ref('General')
const show = computed({
get() {
return props.visible
},
set(visible: boolean) {
emit('update:visible', visible)
},
})
</script>
<template>
<NModal v-model:show="show" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
<div>
<NTabs v-model:value="active" type="line" animated>
<NTabPane name="General" tab="General">
<template #tab>
<SvgIcon class="text-lg" icon="ri:file-user-line" />
<span class="ml-2">{{ $t('setting.general') }}</span>
</template>
<div class="min-h-[100px]">
<General />
</div>
</NTabPane>
<NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
<template #tab>
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
<span class="ml-2">{{ $t('setting.advanced') }}</span>
</template>
<div class="min-h-[100px]">
<Advanced />
</div>
</NTabPane>
<NTabPane name="Config" tab="Config">
<template #tab>
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
<span class="ml-2">{{ $t('setting.config') }}</span>
</template>
<About />
</NTabPane>
</NTabs>
</div>
</NModal>
</template>

View File

@@ -0,0 +1,21 @@
<script setup lang='ts'>
import { computed, useAttrs } from 'vue'
import { Icon } from '@iconify/vue'
interface Props {
icon?: string
}
defineProps<Props>()
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || '',
}))
</script>
<template>
<Icon :icon="icon" v-bind="bindAttrs" />
</template>

View File

@@ -0,0 +1,40 @@
<script setup lang='ts'>
import { computed } from 'vue'
import { NAvatar } from 'naive-ui'
import { useUserStore } from '@/store'
import defaultAvatar from '@/assets/avatar.jpg'
import { isString } from '@/utils/is'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
</script>
<template>
<div class="flex items-center overflow-hidden">
<div class="w-10 h-10 overflow-hidden rounded-full shrink-0">
<template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
<NAvatar
size="large"
round
:src="userInfo.avatar"
:fallback-src="defaultAvatar"
/>
</template>
<template v-else>
<NAvatar size="large" round :src="defaultAvatar" />
</template>
</div>
<div class="flex-1 min-w-0 ml-2">
<h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap">
{{ userInfo.name ?? 'ChenZhaoYu' }}
</h2>
<p class="overflow-hidden text-xs text-gray-500 text-ellipsis whitespace-nowrap">
<span
v-if="isString(userInfo.description) && userInfo.description !== ''"
v-html="userInfo.description"
/>
</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,8 @@
import HoverButton from './HoverButton/index.vue'
import NaiveProvider from './NaiveProvider/index.vue'
import SvgIcon from './SvgIcon/index.vue'
import UserAvatar from './UserAvatar/index.vue'
import Setting from './Setting/index.vue'
import PromptStore from './PromptStore/index.vue'
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore }

View File

@@ -0,0 +1,8 @@
<template>
<div class="text-neutral-400">
<span>Star on</span>
<a href="https://github.com/Chanzhaoyu/chatgpt-bot" target="_blank" class="text-blue-500">
GitHub
</a>
</div>
</template>

View File

@@ -0,0 +1,3 @@
import GithubSite from './GithubSite.vue'
export { GithubSite }

View File

@@ -0,0 +1,8 @@
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
export function useBasicLayout() {
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('sm')
return { isMobile }
}

View File

@@ -0,0 +1,36 @@
import { h } from 'vue'
import { SvgIcon } from '@/components/common'
export const useIconRender = () => {
interface IconConfig {
icon?: string
color?: string
fontSize?: number
}
interface IconStyle {
color?: string
fontSize?: string
}
const iconRender = (config: IconConfig) => {
const { color, fontSize, icon } = config
const style: IconStyle = {}
if (color)
style.color = color
if (fontSize)
style.fontSize = `${fontSize}px`
if (!icon)
window.console.warn('iconRender: icon is required')
return () => h(SvgIcon, { icon, style })
}
return {
iconRender,
}
}

View File

@@ -0,0 +1,27 @@
import { computed } from 'vue'
import { enUS, zhCN, zhTW } from 'naive-ui'
import { useAppStore } from '@/store'
import { setLocale } from '@/locales'
export function useLanguage() {
const appStore = useAppStore()
const language = computed(() => {
switch (appStore.language) {
case 'en-US':
setLocale('en-US')
return enUS
case 'zh-CN':
setLocale('zh-CN')
return zhCN
case 'zh-TW':
setLocale('zh-TW')
return zhTW
default:
setLocale('zh-CN')
return enUS
}
})
return { language }
}

View File

@@ -0,0 +1,44 @@
import type { GlobalThemeOverrides } from 'naive-ui'
import { computed, watch } from 'vue'
import { darkTheme, useOsTheme } from 'naive-ui'
import { useAppStore } from '@/store'
export function useTheme() {
const appStore = useAppStore()
const OsTheme = useOsTheme()
const isDark = computed(() => {
return true
// if (appStore.theme === 'auto')
// return OsTheme.value === 'dark'
// else
// return appStore.theme === 'dark'
})
const theme = computed(() => {
return isDark.value ? darkTheme : undefined
})
const themeOverrides = computed<GlobalThemeOverrides>(() => {
if (isDark.value) {
return {
common: {},
}
}
return {}
})
watch(
() => isDark.value,
(dark) => {
if (dark)
document.documentElement.classList.add('dark')
else
document.documentElement.classList.remove('dark')
},
{ immediate: true },
)
return { theme, themeOverrides }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,89 @@
export default {
common: {
add: 'Add',
addSuccess: 'Add Success',
edit: 'Edit',
editSuccess: 'Edit Success',
delete: 'Delete',
deleteSuccess: 'Delete Success',
save: 'Save',
saveSuccess: 'Save Success',
reset: 'Reset',
action: 'Action',
export: 'Export',
exportSuccess: 'Export Success',
import: 'Import',
importSuccess: 'Import Success',
clear: 'Clear',
clearSuccess: 'Clear Success',
yes: 'Yes',
no: 'No',
confirm: 'Confirm',
download: 'Download',
noData: 'No Data',
wrong: 'Something went wrong, please try again later.',
success: 'Success',
failed: 'Failed',
verify: 'Verify',
unauthorizedTips: 'Unauthorized, please verify first.',
},
chat: {
placeholder: 'Ask me anything...(Shift + Enter = line break)',
placeholderMobile: 'Ask me anything...',
copy: 'Copy',
copied: 'Copied',
copyCode: 'Copy Code',
clearChat: 'Clear Chat',
clearChatConfirm: 'Are you sure to clear this chat?',
exportImage: 'Export Image',
exportImageConfirm: 'Are you sure to export this chat to png?',
exportSuccess: 'Export Success',
exportFailed: 'Export Failed',
usingContext: 'Context Mode',
turnOnContext: 'In the current mode, sending messages will carry previous chat records.',
turnOffContext: 'In the current mode, sending messages will not carry previous chat records.',
deleteMessage: 'Delete Message',
deleteMessageConfirm: 'Are you sure to delete this message?',
deleteHistoryConfirm: 'Are you sure to clear this history?',
clearHistoryConfirm: 'Are you sure to clear chat history?',
preview: 'Preview',
showRawText: 'Show as raw text',
},
setting: {
setting: 'Setting',
general: 'General',
advanced: 'Advanced',
config: 'Config',
avatarLink: 'Avatar Link',
name: 'Name',
description: 'Description',
role: 'Role',
resetUserInfo: 'Reset UserInfo',
chatHistory: 'ChatHistory',
theme: 'Theme',
language: 'Language',
api: 'API',
reverseProxy: 'Reverse Proxy',
timeout: 'Timeout',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API Balance',
},
store: {
local: 'Local',
online: 'Online',
title: 'Title',
description: 'Description',
clearStoreConfirm: 'Whether to clear the data?',
importPlaceholder: 'Please paste the JSON data here',
addRepeatTitleTips: 'Title duplicate, please re-enter',
addRepeatContentTips: 'Content duplicate: {msg}, please re-enter',
editRepeatTitleTips: 'Title conflict, please revise',
editRepeatContentTips: 'Content conflict {msg} , please re-modify',
importError: 'Key value mismatch',
importRepeatTitle: 'Title repeatedly skipped: {msg}',
importRepeatContent: 'Content is repeatedly skipped: {msg}',
onlineImportWarning: 'Note: Please check the JSON file source!',
downloadError: 'Please check the network status and JSON file validity',
},
}

View File

@@ -0,0 +1,34 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import enUS from './en-US'
import zhCN from './zh-CN'
import zhTW from './zh-TW'
import { useAppStoreWithOut } from '@/store/modules/app'
import type { Language } from '@/store/modules/app/helper'
const appStore = useAppStoreWithOut()
const defaultLocale = appStore.language || 'zh-CN'
const i18n = createI18n({
locale: defaultLocale,
fallbackLocale: 'en-US',
allowComposition: true,
messages: {
'en-US': enUS,
'zh-CN': zhCN,
'zh-TW': zhTW,
},
})
export const t = i18n.global.t
export function setLocale(locale: Language) {
i18n.global.locale = locale
}
export function setupI18n(app: App) {
app.use(i18n)
}
export default i18n

View File

@@ -0,0 +1,89 @@
export default {
common: {
add: '添加',
addSuccess: '添加成功',
edit: '编辑',
editSuccess: '编辑成功',
delete: '删除',
deleteSuccess: '删除成功',
save: '保存',
saveSuccess: '保存成功',
reset: '重置',
action: '操作',
export: '导出',
exportSuccess: '导出成功',
import: '导入',
importSuccess: '导入成功',
clear: '清空',
clearSuccess: '清空成功',
yes: '是',
no: '否',
confirm: '确定',
download: '下载',
noData: '暂无数据',
wrong: '好像出错了,请稍后再试。',
success: '操作成功',
failed: '操作失败',
verify: '登录',
unauthorizedTips: '未经授权,请先进行验证。',
},
chat: {
placeholder: '来说点什么吧...Shift + Enter = 换行)',
placeholderMobile: '来说点什么...',
copy: '复制',
copied: '复制成功',
copyCode: '复制代码',
clearChat: '清空会话',
clearChatConfirm: '是否清空会话?',
exportImage: '保存会话到图片',
exportImageConfirm: '是否将会话保存为图片?',
exportSuccess: '保存成功',
exportFailed: '保存失败',
usingContext: '上下文模式',
turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录',
turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录',
deleteMessage: '删除消息',
deleteMessageConfirm: '是否删除此消息?',
deleteHistoryConfirm: '确定删除此记录?',
clearHistoryConfirm: '确定清空聊天记录?',
preview: '预览',
showRawText: '显示原文',
},
setting: {
setting: '设置',
general: '总览',
advanced: '高级',
config: '配置',
avatarLink: '头像链接',
name: '名称',
description: '描述',
role: '角色设定',
resetUserInfo: '重置用户信息',
chatHistory: '聊天记录',
theme: '主题',
language: '语言',
api: 'API',
reverseProxy: '反向代理',
timeout: '超时',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API余额',
},
store: {
local: '本地',
online: '在线',
title: '标题',
description: '描述',
clearStoreConfirm: '是否清空数据?',
importPlaceholder: '请粘贴 JSON 数据到此处',
addRepeatTitleTips: '标题重复,请重新输入',
addRepeatContentTips: '内容重复:{msg},请重新输入',
editRepeatTitleTips: '标题冲突,请重新修改',
editRepeatContentTips: '内容冲突{msg} ,请重新修改',
importError: '键值不匹配',
importRepeatTitle: '标题重复跳过:{msg}',
importRepeatContent: '内容重复跳过:{msg}',
onlineImportWarning: '注意:请检查 JSON 文件来源!',
downloadError: '请检查网络状态与 JSON 文件有效性',
},
}

View File

@@ -0,0 +1,89 @@
export default {
common: {
add: '新增',
addSuccess: '新增成功',
edit: '編輯',
editSuccess: '編輯成功',
delete: '刪除',
deleteSuccess: '刪除成功',
save: '儲存',
saveSuccess: '儲存成功',
reset: '重設',
action: '操作',
export: '匯出',
exportSuccess: '匯出成功',
import: '匯入',
importSuccess: '匯入成功',
clear: '清除',
clearSuccess: '清除成功',
yes: '是',
no: '否',
confirm: '確認',
download: '下載',
noData: '目前無資料',
wrong: '發生錯誤,請稍後再試。',
success: '操作成功',
failed: '操作失敗',
verify: '驗證',
unauthorizedTips: '未經授權,請先進行驗證。',
},
chat: {
placeholder: '來說點什麼...Shift + Enter = 換行)',
placeholderMobile: '來說點什麼...',
copy: '複製',
copied: '複製成功',
copyCode: '複製代碼',
clearChat: '清除對話',
clearChatConfirm: '是否清空對話?',
exportImage: '儲存對話為圖片',
exportImageConfirm: '是否將對話儲存為圖片?',
exportSuccess: '儲存成功',
exportFailed: '儲存失敗',
usingContext: '上下文模式',
turnOnContext: '啟用上下文模式,在此模式下,發送訊息會包含之前的聊天記錄。',
turnOffContext: '關閉上下文模式,在此模式下,發送訊息不會包含之前的聊天記錄。',
deleteMessage: '刪除訊息',
deleteMessageConfirm: '是否刪除此訊息?',
deleteHistoryConfirm: '確定刪除此紀錄?',
clearHistoryConfirm: '確定清除紀錄?',
preview: '預覽',
showRawText: '顯示原文',
},
setting: {
setting: '設定',
general: '總覽',
advanced: '高級',
config: '設定',
avatarLink: '頭貼連結',
name: '名稱',
description: '描述',
role: '角色設定',
resetUserInfo: '重設使用者資訊',
chatHistory: '紀錄',
theme: '主題',
language: '語言',
api: 'API',
reverseProxy: '反向代理',
timeout: '逾時',
socks: 'Socks',
httpsProxy: 'HTTPS Proxy',
balance: 'API余額',
},
store: {
local: '本機',
online: '線上',
title: '標題',
description: '描述',
clearStoreConfirm: '是否清除資料?',
importPlaceholder: '請將 JSON 資料貼在此處',
addRepeatTitleTips: '標題重複,請重新輸入',
addRepeatContentTips: '內容重複:{msg},請重新輸入',
editRepeatTitleTips: '標題衝突,請重新修改',
editRepeatContentTips: '內容衝突{msg} ,請重新修改',
importError: '鍵值不符合',
importRepeatTitle: '因標題重複跳過:{msg}',
importRepeatContent: '因內容重複跳過:{msg}',
onlineImportWarning: '注意:請檢查 JSON 檔案來源!',
downloadError: '請檢查網路狀態與 JSON 檔案有效性',
},
}

View File

@@ -0,0 +1,23 @@
import { createApp } from 'vue'
import App from './App.vue'
import { setupI18n } from './locales'
import { setupAssets, setupScrollbarStyle } from './plugins'
import { setupStore } from './store'
import { setupRouter } from './router'
async function bootstrap() {
const app = createApp(App)
setupAssets()
setupScrollbarStyle()
setupStore(app)
setupI18n(app)
await setupRouter(app)
app.mount('#app')
}
bootstrap()

View File

@@ -0,0 +1,18 @@
import 'katex/dist/katex.min.css'
import '@/styles/lib/tailwind.css'
import '@/styles/lib/highlight.less'
import '@/styles/lib/github-markdown.less'
import '@/styles/global.less'
/** Tailwind's Preflight Style Override */
function naiveStyleOverride() {
const meta = document.createElement('meta')
meta.name = 'naive-ui-style'
document.head.appendChild(meta)
}
function setupAssets() {
naiveStyleOverride()
}
export default setupAssets

View File

@@ -0,0 +1,4 @@
import setupAssets from './assets'
import setupScrollbarStyle from './scrollbarStyle'
export { setupAssets, setupScrollbarStyle }

View File

@@ -0,0 +1,28 @@
import { darkTheme, lightTheme } from 'naive-ui'
const setupScrollbarStyle = () => {
const style = document.createElement('style')
const styleContent = `
::-webkit-scrollbar {
background-color: transparent;
width: ${lightTheme.Scrollbar.common?.scrollbarWidth};
}
::-webkit-scrollbar-thumb {
background-color: ${lightTheme.Scrollbar.common?.scrollbarColor};
border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius};
}
html.dark ::-webkit-scrollbar {
background-color: transparent;
width: ${darkTheme.Scrollbar.common?.scrollbarWidth};
}
html.dark ::-webkit-scrollbar-thumb {
background-color: ${darkTheme.Scrollbar.common?.scrollbarColor};
border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius};
}
`
style.innerHTML = styleContent
document.head.appendChild(style)
}
export default setupScrollbarStyle

View File

@@ -0,0 +1,52 @@
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import { setupPageGuard } from './permission'
import { ChatLayout } from '@/views/chat/layout'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Root',
component: ChatLayout,
redirect: '/chat',
children: [
{
path: '/chat/:uuid?',
name: 'Chat',
component: () => import('@/views/chat/index.vue'),
},
],
},
{
path: '/404',
name: '404',
component: () => import('@/views/exception/404/index.vue'),
},
{
path: '/500',
name: '500',
component: () => import('@/views/exception/500/index.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
redirect: '/404',
},
]
export const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior: () => ({ left: 0, top: 0 }),
})
setupPageGuard(router)
export async function setupRouter(app: App) {
app.use(router)
await router.isReady()
}

View File

@@ -0,0 +1,30 @@
import type { Router } from 'vue-router'
import { useAuthStoreWithout } from '@/store/modules/auth'
export function setupPageGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStoreWithout()
if (!authStore.session) {
// try {
// const data = await authStore.getSession()
// if (String(data.auth) === 'false' && authStore.token)
// authStore.removeToken()
// if (to.path === '/500')
// next({ name: 'Root' })
// else
// next()
// }
// catch (error) {
// if (to.path !== '/500')
// next({ name: '500' })
// else
// next()
// }
next()
}
else {
next()
}
})
}

View File

@@ -0,0 +1,10 @@
import type { App } from 'vue'
import { createPinia } from 'pinia'
export const store = createPinia()
export function setupStore(app: App) {
app.use(store)
}
export * from './modules'

View File

@@ -0,0 +1,26 @@
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'appSetting'
export type Theme = 'light' | 'dark' | 'auto'
export type Language = 'zh-CN' | 'zh-TW' | 'en-US'
export interface AppState {
siderCollapsed: boolean
theme: Theme
language: Language
}
export function defaultSetting(): AppState {
return { siderCollapsed: false, theme: 'light', language: 'zh-CN' }
}
export function getLocalSetting(): AppState {
const localSetting: AppState | undefined = ss.get(LOCAL_NAME)
return { ...defaultSetting(), ...localSetting }
}
export function setLocalSetting(setting: AppState): void {
ss.set(LOCAL_NAME, setting)
}

View File

@@ -0,0 +1,34 @@
import { defineStore } from 'pinia'
import type { AppState, Language, Theme } from './helper'
import { getLocalSetting, setLocalSetting } from './helper'
import { store } from '@/store'
export const useAppStore = defineStore('app-store', {
state: (): AppState => getLocalSetting(),
actions: {
setSiderCollapsed(collapsed: boolean) {
this.siderCollapsed = collapsed
this.recordState()
},
setTheme(theme: Theme) {
this.theme = theme
this.recordState()
},
setLanguage(language: Language) {
if (this.language !== language) {
this.language = language
this.recordState()
}
},
recordState() {
setLocalSetting(this.$state)
},
},
})
export function useAppStoreWithOut() {
return useAppStore(store)
}

View File

@@ -0,0 +1,15 @@
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'SECRET_TOKEN'
export function getToken() {
return ss.get(LOCAL_NAME)
}
export function setToken(token: string) {
return ss.set(LOCAL_NAME, token)
}
export function removeToken() {
return ss.remove(LOCAL_NAME)
}

View File

@@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
import { getToken, removeToken, setToken } from './helper'
import { store } from '@/store'
import { fetchSession } from '@/api'
interface SessionResponse {
auth: boolean
model: 'ChatGPTAPI' | 'ChatGPTUnofficialProxyAPI'
}
export interface AuthState {
token: string | undefined
session: SessionResponse | null
}
export const useAuthStore = defineStore('auth-store', {
state: (): AuthState => ({
token: getToken(),
session: null,
}),
getters: {
isChatGPTAPI(state): boolean {
return state.session?.model === 'ChatGPTAPI'
},
},
actions: {
async getSession() {
try {
const { data } = await fetchSession<SessionResponse>()
this.session = { ...data }
return Promise.resolve(data)
}
catch (error) {
return Promise.reject(error)
}
},
setToken(token: string) {
this.token = token
setToken(token)
},
removeToken() {
this.token = undefined
removeToken()
},
},
})
export function useAuthStoreWithout() {
return useAuthStore(store)
}

View File

@@ -0,0 +1,22 @@
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'chatStorage'
export function defaultState(): Chat.ChatState {
const uuid = 1002
return {
active: uuid,
usingContext: true,
history: [{ uuid, title: 'New Chat', isEdit: false }],
chat: [{ uuid, data: [] }],
}
}
export function getLocalState(): Chat.ChatState {
const localState = ss.get(LOCAL_NAME)
return { ...defaultState(), ...localState }
}
export function setLocalState(state: Chat.ChatState) {
ss.set(LOCAL_NAME, state)
}

View File

@@ -0,0 +1,194 @@
import { defineStore } from 'pinia'
import { getLocalState, setLocalState } from './helper'
import { router } from '@/router'
export const useChatStore = defineStore('chat-store', {
state: (): Chat.ChatState => getLocalState(),
getters: {
getChatHistoryByCurrentActive(state: Chat.ChatState) {
const index = state.history.findIndex(item => item.uuid === state.active)
if (index !== -1)
return state.history[index]
return null
},
getChatByUuid(state: Chat.ChatState) {
return (uuid?: number) => {
if (uuid)
return state.chat.find(item => item.uuid === uuid)?.data ?? []
return state.chat.find(item => item.uuid === state.active)?.data ?? []
}
},
},
actions: {
setUsingContext(context: boolean) {
this.usingContext = context
this.recordState()
},
addHistory(history: Chat.History, chatData: Chat.Chat[] = []) {
this.history.unshift(history)
this.chat.unshift({ uuid: history.uuid, data: chatData })
this.active = history.uuid
this.reloadRoute(history.uuid)
},
updateHistory(uuid: number, edit: Partial<Chat.History>) {
const index = this.history.findIndex(item => item.uuid === uuid)
if (index !== -1) {
this.history[index] = { ...this.history[index], ...edit }
this.recordState()
}
},
async deleteHistory(index: number) {
this.history.splice(index, 1)
this.chat.splice(index, 1)
if (this.history.length === 0) {
this.active = null
this.reloadRoute()
return
}
if (index > 0 && index <= this.history.length) {
const uuid = this.history[index - 1].uuid
this.active = uuid
this.reloadRoute(uuid)
return
}
if (index === 0) {
if (this.history.length > 0) {
const uuid = this.history[0].uuid
this.active = uuid
this.reloadRoute(uuid)
}
}
if (index > this.history.length) {
const uuid = this.history[this.history.length - 1].uuid
this.active = uuid
this.reloadRoute(uuid)
}
},
async setActive(uuid: number) {
this.active = uuid
return await this.reloadRoute(uuid)
},
getChatByUuidAndIndex(uuid: number, index: number) {
if (!uuid || uuid === 0) {
if (this.chat.length)
return this.chat[0].data[index]
return null
}
const chatIndex = this.chat.findIndex(item => item.uuid === uuid)
if (chatIndex !== -1)
return this.chat[chatIndex].data[index]
return null
},
addChatByUuid(uuid: number, chat: Chat.Chat) {
if (!uuid || uuid === 0) {
if (this.history.length === 0) {
const uuid = Date.now()
this.history.push({ uuid, title: chat.text, isEdit: false })
this.chat.push({ uuid, data: [chat] })
this.active = uuid
this.recordState()
}
else {
this.chat[0].data.push(chat)
if (this.history[0].title === 'New Chat')
this.history[0].title = chat.text
this.recordState()
}
}
const index = this.chat.findIndex(item => item.uuid === uuid)
if (index !== -1) {
this.chat[index].data.push(chat)
if (this.history[index].title === 'New Chat')
this.history[index].title = chat.text
this.recordState()
}
},
updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) {
if (!uuid || uuid === 0) {
if (this.chat.length) {
this.chat[0].data[index] = chat
this.recordState()
}
return
}
const chatIndex = this.chat.findIndex(item => item.uuid === uuid)
if (chatIndex !== -1) {
this.chat[chatIndex].data[index] = chat
this.recordState()
}
},
updateChatSomeByUuid(uuid: number, index: number, chat: Partial<Chat.Chat>) {
if (!uuid || uuid === 0) {
if (this.chat.length) {
this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat }
this.recordState()
}
return
}
const chatIndex = this.chat.findIndex(item => item.uuid === uuid)
if (chatIndex !== -1) {
this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat }
this.recordState()
}
},
deleteChatByUuid(uuid: number, index: number) {
if (!uuid || uuid === 0) {
if (this.chat.length) {
this.chat[0].data.splice(index, 1)
this.recordState()
}
return
}
const chatIndex = this.chat.findIndex(item => item.uuid === uuid)
if (chatIndex !== -1) {
this.chat[chatIndex].data.splice(index, 1)
this.recordState()
}
},
clearChatByUuid(uuid: number) {
if (!uuid || uuid === 0) {
if (this.chat.length) {
this.chat[0].data = []
this.recordState()
}
return
}
const index = this.chat.findIndex(item => item.uuid === uuid)
if (index !== -1) {
this.chat[index].data = []
this.recordState()
}
},
async reloadRoute(uuid?: number) {
this.recordState()
await router.push({ name: 'Chat', params: { uuid } })
},
recordState() {
setLocalState(this.$state)
},
},
})

View File

@@ -0,0 +1,6 @@
export * from './app'
export * from './chat'
export * from './user'
export * from './prompt'
export * from './settings'
export * from './auth'

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