Showing preview only (201K chars total). Download the full file or copy to clipboard to get everything.
Repository: WenJing95/chatgpt-web
Branch: main
Commit: ea19057bd9a2
Files: 109
Total size: 168.0 KB
Directory structure:
gitextract_deg4t8zk/
├── .commitlintrc.json
├── .dockerignore
├── .editorconfig
├── .eslintrc.cjs
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── build_docker.yml
│ └── ci.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .npmrc
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── CONTRIBUTING.md
├── Dockerfile
├── README.md
├── config/
│ ├── index.ts
│ └── proxy.ts
├── docker-compose/
│ ├── docker-compose.yml
│ └── nginx/
│ ├── add_user.sh
│ ├── auth/
│ │ └── .htpasswd
│ ├── nginx.conf
│ └── remove_user.sh
├── index.html
├── license
├── package.json
├── postcss.config.js
├── service/
│ ├── Dockerfile
│ ├── api_model.py
│ ├── chatgpt_wapper.py
│ ├── entrypoint.sh
│ ├── errors.py
│ ├── main.py
│ ├── message_store.py
│ ├── requirements.txt
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── local-whisper/
│ │ │ ├── __init__.py
│ │ │ └── linux/
│ │ │ └── init.sh
│ │ └── openai_token_control.py
│ └── whisper_wapper.py
├── src/
│ ├── App.vue
│ ├── api/
│ │ └── index.ts
│ ├── components/
│ │ ├── common/
│ │ │ ├── HoverButton/
│ │ │ │ ├── Button.vue
│ │ │ │ └── index.vue
│ │ │ ├── NaiveProvider/
│ │ │ │ └── index.vue
│ │ │ ├── Setting/
│ │ │ │ ├── About.vue
│ │ │ │ ├── Advance.vue
│ │ │ │ ├── General.vue
│ │ │ │ └── index.vue
│ │ │ ├── SvgIcon/
│ │ │ │ └── index.vue
│ │ │ ├── UserAvatar/
│ │ │ │ └── index.vue
│ │ │ └── index.ts
│ │ └── custom/
│ │ ├── GithubSite.vue
│ │ └── index.ts
│ ├── hooks/
│ │ ├── useBasicLayout.ts
│ │ ├── useIconRender.ts
│ │ ├── useLanguage.ts
│ │ └── useTheme.ts
│ ├── locales/
│ │ ├── en-US.ts
│ │ ├── index.ts
│ │ ├── ja-JP.ts
│ │ └── zh-CN.ts
│ ├── main.ts
│ ├── plugins/
│ │ ├── assets.ts
│ │ ├── index.ts
│ │ └── scrollbarStyle.ts
│ ├── router/
│ │ └── index.ts
│ ├── store/
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── app/
│ │ │ ├── helper.ts
│ │ │ └── index.ts
│ │ ├── chat/
│ │ │ ├── helper.ts
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── user/
│ │ ├── helper.ts
│ │ └── index.ts
│ ├── styles/
│ │ ├── global.less
│ │ └── lib/
│ │ ├── github-markdown.less
│ │ ├── highlight.less
│ │ └── tailwind.css
│ ├── typings/
│ │ ├── chat.d.ts
│ │ ├── env.d.ts
│ │ └── global.d.ts
│ ├── utils/
│ │ ├── crypto/
│ │ │ └── index.ts
│ │ ├── format/
│ │ │ └── index.ts
│ │ ├── is/
│ │ │ └── index.ts
│ │ ├── request/
│ │ │ ├── axios.ts
│ │ │ └── index.ts
│ │ └── storage/
│ │ ├── index.ts
│ │ └── local.ts
│ └── views/
│ ├── chat/
│ │ ├── components/
│ │ │ ├── Message/
│ │ │ │ ├── Avatar.vue
│ │ │ │ ├── Text.vue
│ │ │ │ ├── index.vue
│ │ │ │ └── style.less
│ │ │ └── index.ts
│ │ ├── hooks/
│ │ │ ├── useChat.ts
│ │ │ ├── useCopyCode.ts
│ │ │ └── useScroll.ts
│ │ ├── index.vue
│ │ └── layout/
│ │ ├── Layout.vue
│ │ ├── header/
│ │ │ └── index.vue
│ │ ├── index.ts
│ │ └── sider/
│ │ ├── Footer.vue
│ │ ├── List.vue
│ │ └── index.vue
│ └── exception/
│ ├── 403/
│ │ └── index.vue
│ └── 404/
│ └── index.vue
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .commitlintrc.json
================================================
{
"extends": ["@commitlint/config-conventional"]
}
================================================
FILE: .dockerignore
================================================
node_modules
Dockerfile
.*
*/.*
================================================
FILE: .editorconfig
================================================
# 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
================================================
FILE: .eslintrc.cjs
================================================
module.exports = {
root: true,
extends: ['@antfu'],
}
================================================
FILE: .gitattributes
================================================
"*.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
================================================
FILE: .github/workflows/build_docker.yml
================================================
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
================================================
FILE: .github/workflows/ci.yml
================================================
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
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
service/log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
#backend
service/venv
service/message_store.json
docker-compose/nginx/html
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?
================================================
FILE: .husky/commit-msg
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit
================================================
FILE: .husky/pre-commit
================================================
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
================================================
FILE: .npmrc
================================================
strict-peer-dependencies=false
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"]
}
================================================
FILE: .vscode/settings.json
================================================
{
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"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"
}
================================================
FILE: CONTRIBUTING.md
================================================
# 贡献指南
感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。
## 语义化版本
该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。
每个重大更改都将记录在 `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)
================================================
FILE: Dockerfile
================================================
# build front-end
FROM node:lts-alpine AS builder
COPY ./ /app
WORKDIR /app
RUN apk add --no-cache git \
&& npm install pnpm -g \
&& pnpm install \
&& pnpm run build \
&& rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/*
================================================
FILE: README.md
================================================
# ChatGPT Web
### 使用界面



### 性格微调功能

### 文本审查功能(使用OpenAI官方接口)

- [ChatGPT Web](#chatgpt-web)
- [介绍](#介绍)
- [快速部署](#快速部署)
- [开发环境搭建](#开发环境搭建)
- [Node](#Node)
- [Python](#Python)
- [开发环境启动项目](#开发环境启动项目)
- [启动前端服务](#启动前端服务)
- [启动后端服务](#启动后端服务)
- [打包和部署](#打包和部署)
- [前端打包](#前端资源打包(需要安装node))
- [后端打包](#后端服务打包为docker容器(需要安装docker和docker-compose))
- [使用DockerCompose启动](#使用DockerCompose启动)
- [常见问题](#常见问题)
- [参与贡献](#参与贡献)
- [赞助原作者](#赞助)
- [License](#license)
## 介绍
这是一个可以私有化部署的`ChatGpt`网页,使用`OpenAI`的官方API接入`gpt-3.5`或`gpt-4`模型来实现接近`ChatGPT Plus`的对话效果。
源代码Fork和修改于[Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web/)
与OpenAI官方提供`ChatGPT Plus`对比,`ChatGPT Web`有以下优势:
1. **省钱(仅限`gpt-3.5`)**。
- 按照日常用量,你可以用1折左右的价格,体验与`ChatGPT Plus`几乎相同的对话服务。
- 语音识别可以在本地离线完成,完全免费。
2. **0门槛使用**。你可以将自建的`ChatGPT Web`
站点分享给家人和朋友,他们不再需要解决网络问题,就可以轻松享受到`ChatGPT Plus`带来的生产力提升。
3. **可以缓解网络封锁的影响**。`ChatGPT Web`只需要一个`OpenAI API Key`即可使用,如果你所在的地区无法访问`OpenAI`
,你可以将`ChatGPT Web`部署在海外服务器上,或在当地服务器上配置`socks_proxy`参数来转发请求给代理软件,即可正常使用。
和[Chanzhaoyu的原版](https://github.com/Chanzhaoyu/chatgpt-web/)的主要区别:
1. 专注于易用、易部署、不操心,我将尽我所能做到对小白用户友好,因此本项目也会舍弃一些专业功能,例如:
- 不支持accessToken这类非官方使用方式。我认为你只是希望有个稳定能用的AI助手,并不想折腾这些
- 不支持反向代理。第三方的反代地址安全性存疑、封号也超级快!
- 不做导入和管理Prompt模板的功能。使用者没有“这个链接干什么用的”、“什么是json文件”、“为什么要我自己审核json文件的安全性,怎么审核?”此类烦恼
2. 可以识别语音消息:通过OpenAI官方`whisper-1`接口,或免费的`whisper.cpp`实现。懒得打字的时候很好用
3. 可以调整`ChatGpt`的性格
4. 可以调整记住的上下文数量
其它区别:
1. 增加日语界面
2. 优化了移动端体验
## 快速部署
如果你不需要自己开发,只需要部署使用,可以直接跳到 [使用最新版本docker镜像启动](#使用最新版本docker镜像启动)
## 开发环境搭建
### Node
`node` 需要 `^16 || ^18` 版本(`node >= 14`
需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill)
),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本
```shell
node -v
```
如果你没有安装过 `pnpm`
```shell
npm install pnpm -g
```
### Python
`python` 需要 `3.10` 以上版本,进入文件夹 `/service` 运行以下命令
```shell
pip install --no-cache-dir -r requirements.txt
```
### whisper.cpp编译
这一步的目的是设置本地语音识别功能,这个模块来自[ggerganov/whisper](https://github.com/ggerganov/whisper.cpp)
如果你的系统是windows,可以跳过这一步,因为whisper的二进制文件,我已经为你下载好,放在项目里了;
如果你的系统是linux,则需要你自己编译whisper:
```shell
cd ./tools/local-whisper/linux/
chmod +x init.sh
./init.sh
```
`init.sh`脚本执行完成之后,你将看到`./tools/local-whisper/linux/whisper.cpp-master/`目录,目录中有个叫`main`的二进制文件,这就是你需要的`whisper`。
## 开发环境启动项目
### 启动前端服务
根目录下运行以下命令
```shell
# 前端网页的默认端口号是1002,对接的后端服务的默认端口号是3002,可以在 .env 和 .vite.config.ts 文件中修改
pnpm bootstrap
pnpm dev
```
### 启动后端服务
只有`--openai_api_key`是必填的启动参数,需要先去[OpenAI](https://platform.openai.com/)
注册账号,然后在[这里](https://platform.openai.com/account/api-keys)获取`OPENAI_API_KEY`。
```shell
# 进入文件夹 `/service` 运行以下命令
python main.py --openai_api_key="$OPENAI_API_KEY"
```
除了`openai_api_key`这个必填的参数之外,还有以下可选参数可用:
- `openai_timeout_ms` 访问OpenAI的超时时间(毫秒),默认值为 '100000'
- `api_model` 默认值为 gpt-3.5-turbo 也可以使用很贵的 gpt-4
- `socks_proxy` 代理,默认值为空字符串,格式示例: `http://127.0.0.1:10808`
- `use_local_whisper` 设置为`true`可以使用离线模型来完成语音识别,如果设置为`false`就会使用OpenAI的API进行语音识别,默认值为: `true`
- `host` HOST,默认值为 0.0.0.0
- `port` PORT,默认值为 3002
也就是说你也可以这样启动
```shell
python main.py --openai_api_key="$OPENAI_API_KEY" --openai_timeout_ms="$OPENAI_TIMEOUT_MS" --api_model="$API_MODEL" --socks_proxy="$SOCKS_PROXY" --use_local_whisper="$USE_LOCAL_WHISPER" --host="$HOST" --port="$PORT"
```
## 打包和部署
### 前端资源打包(需要安装node)
1. 根目录下运行以下命令
```shell
pnpm run build
```
2. 将打包好的文件夹`dist`文件夹复制到`/docker-compose/nginx`目录下,并改名为`html`
```shell
cp dist/ docker-compose/nginx/html -r
```
3. 配置访问权限
```shell
# 进入文件夹 `/docker-compose/nginx`
cd docker-compose/nginx
# 运行add_user.sh脚本,根据提示创建用户名和密码
# (密码文件将被保存在 /docker-compose/nginx/auth/.htpasswd)
bash add_user.sh
# 如果你想删除一个用户,可以使用remove_user.sh脚本
bash remove_user.sh
```
### 后端服务打包为docker容器(需要安装docker和docker-compose)
1. 进入文件夹 `/service` 运行以下命令
```shell
docker build -t chatgpt-web-backend .
```
### 使用DockerCompose启动
- 进入文件夹 `/docker-compose` 修改 `docker-compose.yml` 文件
```
version: '3'
services:
app:
image: chatgpt-web-backend # 这里填你打包的后端服务的镜像名字
ports:
- 3002:3002
environment:
OPENAI_API_KEY: your_openai_api_key
# 访问OpenAI的超时时间(毫秒),可选,默认值为 '100000'
OPENAI_TIMEOUT_MS: '100000'
# 可选,默认值为 gpt-3.5-turbo
API_MODEL: gpt-3.5-turbo
# Socks代理,可选,格式为 http://127.0.0.1:10808
SOCKS_PROXY: ''
# 可选,将USE_LOCAL_WHISPER设置为`true`可以使用离线模型来完成语音识别,如果设置为`false`就会使用OpenAI的API进行语音识别,默认值为: `true`
# 使用离线模型就不需要向OpenAI付费,但会额外消耗一些服务器的cpu和内存资源,这里使用的是tiny模型,工作时占用的内存大概是125MB左右
# 具体性能消耗参考whisper.cpp的官方文档: https://github.com/ggerganov/whisper.cpp#memory-usage
USE_LOCAL_WHISPER: 'true'
# HOST,可选,默认值为 0.0.0.0
HOST: 0.0.0.0
# PORT,可选,默认值为 3002
PORT: 3002
nginx:
depends_on:
- app
image: nginx:alpine
ports:
- '80:80'
expose:
- '80'
volumes:
- ./nginx/html/:/etc/nginx/html/
- ./nginx/auth/:/etc/nginx/auth/
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
links:
- app
```
- 进入文件夹 `/docker-compose` 运行以下命令
```shell
# 前台运行
docker-compose up
# 或后台运行
docker-compose up -d
```
**建议先在前台运行试用一下,看看有没有报错,如果启动和使用都没有问题,再改成后台运行。**
启动成功之后,访问`http://你的机器ip`即可看到网页内容。
## 使用最新版本docker镜像启动
- 如果你只是想自己使用docker部署,可以直接使用我已经打包好的镜像和前端资源
- 首先将前端资源的压缩包解压
前端资源的压缩包在`/docker-compose/nginx/html.zip`,使用`unzip html.zip`将其解压缩。你应该可以看到`/docker-compose/nginx/html`下面有`index.html`和其他前端资源。
- 然后给你的网站添加用户名和密码
```shell
# 进入文件夹 `/docker-compose/nginx`
cd docker-compose/nginx
# 运行add_user.sh脚本,根据提示创建用户名和密码
# (密码文件将被保存在 /docker-compose/nginx/auth/.htpasswd)
bash add_user.sh
# 如果你想删除一个用户,可以使用remove_user.sh脚本
bash remove_user.sh
```
- 最后修改`docker-compose/docker-compose.yml`文件。
除了填写你自己的`OPENAI_API_KEY`之外,还要根据自己的系统环境修改`image`的标签,如果你部署用的是x86架构的机器,就填写`wenjing95/chatgpt-web-backend:x86_64`,如果你用的是arm架构的机器,就填写`wenjing95/chatgpt-web-backend:aarch64`。
```
version: '3'
services:
app:
# 根据自己的系统选择x86_64还是aarch64
image: wenjing95/chatgpt-web-backend:x86_64
# image: wenjing95/chatgpt-web-backend:aarch64
ports:
- 3002:3002
environment:
# 记得填写你的OPENAI_API_KEY
OPENAI_API_KEY: your_openai_api_key
# 访问OpenAI的超时时间(毫秒),可选,默认值为 '100000'
OPENAI_TIMEOUT_MS: '100000'
# 可选,默认值为 gpt-3.5-turbo
API_MODEL: gpt-3.5-turbo
# Socks代理,可选,格式为 http://127.0.0.1:10808
SOCKS_PROXY: ''
# 可选,将USE_LOCAL_WHISPER设置为`true`可以使用离线模型来完成语音识别,如果设置为`false`就会使用OpenAI的API进行语音识别,默认值为: `true`
# 使用离线模型就不需要向OpenAI付费,但会额外消耗一些服务器的cpu和内存资源,这里使用的是tiny模型,工作时占用的内存大概是125MB左右
# 具体性能消耗参考whisper.cpp的官方文档: https://github.com/ggerganov/whisper.cpp#memory-usage
USE_LOCAL_WHISPER: 'true'
# HOST,可选,默认值为 0.0.0.0
HOST: 0.0.0.0
# PORT,可选,默认值为 3002
PORT: 3002
nginx:
depends_on:
- app
image: nginx:alpine
ports:
- '80:80'
expose:
- '80'
volumes:
- ./nginx/html/:/etc/nginx/html/
- ./nginx/auth/:/etc/nginx/auth/
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
links:
- app
```
- 最后进入文件夹 `/docker-compose` 运行以下命令
```shell
# 前台运行
docker-compose up
# 或后台运行
docker-compose up -d
```
**建议先在前台运行试用一下,看看有没有报错,如果启动和使用都没有问题,再改成后台运行。**
启动成功之后,访问`http://你的机器ip`即可看到网页内容。
## 常见问题
Q: 为什么 `Git` 提交总是报错?
A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md)
Q: 如果只使用前端页面,在哪里改请求接口?
A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。
Q: 文件保存时全部爆红?
A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。
Q: 前端没有打字机效果?
A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx
会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web
server 配置同理。
Q: 为什么录音功能不能用?
A: 录音需要https环境,推荐使用cloudflare的免费https证书。
Q: build docker容器的时候,显示`exec entrypoint.sh: no such file or directory`?
A: 因为`entrypoint.sh`文件的换行符是`LF`,而不是`CRLF`,如果你用`CRLF`的IDE操作过这个文件,可能就会出错。可以使用`dos2unix`工具将`LF`换成`CRLF`。
## 参与贡献
贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md)
感谢原作者[Chanzhaoyu](https://github.com/Chanzhaoyu/chatgpt-web/)和所有做过贡献的人,还有生产力工具`ChatGpt`
和`Github Copilot`!
<a href="https://github.com/WenJing95/chatgpt-web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=WenJing95/chatgpt-web" />
</a>
## 赞助
如果你觉得这个项目对你有帮助,请给我点个Star。
如果情况允许,请支持原作者[Chanzhaoyu](https://github.com/Chanzhaoyu/chatgpt-web/)
## License
MIT © [WenJing95](./license)
================================================
FILE: config/index.ts
================================================
export * from './proxy'
================================================
FILE: config/proxy.ts
================================================
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
}
================================================
FILE: docker-compose/docker-compose.yml
================================================
version: '3'
services:
app:
# 根据自己的系统选择x86_64还是aarch64
image: wenjing95/chatgpt-web-backend:x86_64
# image: wenjing95/chatgpt-web-backend:aarch64
ports:
- 3002:3002
environment:
OPENAI_API_KEY: ''
# 访问OpenAI的超时时间,可选,默认值为 '100000'
OPENAI_TIMEOUT_MS: '100000'
# 可选,默认值为 gpt-3.5-turbo
API_MODEL: gpt-3.5-turbo
# Socks代理,可选,格式为 http://127.0.0.1:10808
SOCKS_PROXY: ''
# 可选,将USE_LOCAL_WHISPER设置为`true`可以使用离线模型来完成语音识别,如果设置为`false`就会使用OpenAI的API进行语音识别,默认值为: `true`
# 使用离线模型就不需要向OpenAI付费,但会额外消耗一些服务器的cpu和内存资源,这里使用的是tiny模型,工作时占用的内存大概是125MB左右
# 具体性能消耗参考whisper.cpp的官方文档: https://github.com/ggerganov/whisper.cpp#memory-usage
USE_LOCAL_WHISPER: 'true'
# HOST,可选,默认值为 0.0.0.0
HOST: 0.0.0.0
# PORT,可选,默认值为 3002
PORT: 3002
nginx:
depends_on:
- app
image: nginx:alpine
ports:
- '80:80'
expose:
- '80'
volumes:
- ./nginx/html/:/etc/nginx/html/
- ./nginx/auth/:/etc/nginx/auth/
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
links:
- app
================================================
FILE: docker-compose/nginx/add_user.sh
================================================
#!/bin/bash
read -p "Enter your desired username: " username
read -s -p "Enter your desired password: " password
htpasswd_file="./auth/.htpasswd"
if [ ! -f "$htpasswd_file" ]; then
sudo touch "$htpasswd_file"
fi
sudo sh -c "echo -n '${username}:' >> $htpasswd_file"
sudo sh -c "openssl passwd -apr1 ${password} >> $htpasswd_file"
echo "Username: ${username}"
echo "Password: ${password}"
echo ".htpasswd file location: ${htpasswd_file}"
================================================
FILE: docker-compose/nginx/auth/.htpasswd
================================================
================================================
FILE: docker-compose/nginx/nginx.conf
================================================
server {
listen 80;
server_name localhost;
charset utf-8;
error_page 500 502 503 504 /50x.html;
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
location / {
root /etc/nginx/html/;
try_files $uri /index.html;
}
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://app:3002/;
proxy_buffering off;
client_max_body_size 3M;
}
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;
}
================================================
FILE: docker-compose/nginx/remove_user.sh
================================================
#!/bin/bash
htpasswd_file="./auth/.htpasswd"
if [ ! -f "$htpasswd_file" ]; then
echo "No .htpasswd file found"
exit 1
fi
read -p "Enter the username you want to remove: " username
# Check if the user already exists in the file
if ! grep -q "${username}:" "$htpasswd_file"; then
echo "User not found in .htpasswd file"
exit 1
fi
# Delete the user from the file
sudo sed -i "/${username}:/d" "$htpasswd_file"
echo "User ${username} has been removed from the .htpasswd file."
================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<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, user-scalable=0, viewport-fit=cover" />
<meta content="telephone=no,email=no" name="format-detection" />
<meta name="apple-mobile-web-app-title" content="ChatGPT Web"/>
<title>ChatGPT Web</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>
</html>
================================================
FILE: license
================================================
MIT License
Copyright (c) 2023 WenJing
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.
================================================
FILE: package.json
================================================
{
"name": "chatgpt-web",
"version": "2.10.9",
"private": false,
"description": "ChatGPT Web",
"author": "WenJing",
"keywords": [
"chatgpt-web",
"chatgpt",
"chatbot",
"vue"
],
"scripts": {
"dev": "vite",
"build": "run-p type-check build-only",
"preview": "vite preview",
"build-only": "vite build",
"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",
"katex": "^0.16.4",
"markdown-it": "^13.0.1",
"naive-ui": "^2.34.3",
"pinia": "^2.0.32",
"recorder-core": "^1.2.23020100",
"vite-plugin-pwa": "^0.14.4",
"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.1.4",
"vite-plugin-pwa": "^0.14.4",
"vue-tsc": "^1.2.0"
},
"lint-staged": {
"*.{ts,tsx,vue}": [
"pnpm lint:fix"
]
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: service/Dockerfile
================================================
# 使用 Python 3.10 作为基础镜像
FROM python:3.10
# 设置工作目录
WORKDIR /app
# 复制项目文件到容器中
COPY . .
# ==rust是tiktoken依赖的环境==
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin:$PATH"
# 编译whisper
WORKDIR ./tools/local-whisper/linux/
RUN chmod +x init.sh
RUN ./init.sh
WORKDIR /app
# 下载whisper使用的模型
#wget -P ./tools/local-whisper/model https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin
# 安装依赖
RUN pip install --upgrade pip && pip install --upgrade --no-cache-dir -r requirements.txt
# 设置环境变量
ENV OPENAI_API_KEY=""
ENV OPENAI_TIMEOUT_MS="100000"
ENV API_MODEL="gpt-3.5-turbo"
ENV SOCKS_PROXY=""
ENV USE_LOCAL_WHISPER: 'true'
ENV HOST="127.0.0.1"
ENV PORT=3002
# 复制 entrypoint.sh 脚本到容器中
COPY entrypoint.sh .
# 修改 entrypoint.sh 的权限
RUN chmod +x entrypoint.sh
# 启动命令
ENTRYPOINT ["./entrypoint.sh"]
================================================
FILE: service/api_model.py
================================================
class ApiModel:
KNOWN_API_MODEL_NAMES = ["gpt-3.5-turbo",
"gpt-4",
"gpt-3.5-turbo-16k",
# "gpt-4-32k"
]
KNOWN_API_MODEL_MAX_TOKENS = {"gpt-3.5-turbo": 4000,
"gpt-4": 8000,
"gpt-3.5-turbo-16k": 16000,
# "gpt-4-32k": 32000
}
@classmethod
def get_api_model_name(cls, api_model_name: str, default_api_model_name: str) -> str:
return api_model_name if api_model_name in cls.KNOWN_API_MODEL_NAMES else default_api_model_name
@classmethod
def get_max_token(cls, api_model_name: str, default_max_token: int) -> int:
return cls.KNOWN_API_MODEL_MAX_TOKENS.get(api_model_name, default_max_token)
================================================
FILE: service/chatgpt_wapper.py
================================================
import random
import time
import openai # for OpenAI API calls
import traceback
import json
import asyncio
from loguru import logger
from backoff import on_exception, expo
from tools.openai_token_control import discard_overlimit_messages
import concurrent.futures
from errors import Errors
base = {"role": "system", "content": "You are a helpful assistant."}
async def process(prompt, options, memory_count, top_p, message_store, timeout, max_token, model="gpt-3.5-turbo"):
"""
发文字消息
"""
# 不能是空消息
if not prompt:
logger.error("Prompt is empty.")
yield Errors.PROMPT_IS_EMPTY.value
return
# 内容审查
moderation_params = dict(
model='text-moderation-stable',
input=prompt,
)
moderation_res = await _moderation_create_async(moderation_params)
if moderation_res is None:
yield Errors.SOMETHING_WRONG_IN_OPENAI_MODERATION_API.value
return
try:
if moderation_res.results[0].flagged:
warning_text = "[This content does not comply with OpenAI's usage policy. : {} ]".format(
prompt
)
logger.warning(warning_text)
yield Errors.NOT_COMPLY_POLICY.value
return
chat = {"role": "user", "content": prompt}
# 组合历史消息
if options:
parent_message_id = options.get("parentMessageId")
messages = message_store.get_from_key(parent_message_id)
if messages:
messages.append(chat)
else:
messages = [base, chat]
else:
parent_message_id = None
messages = [base, chat]
# 记忆
messages = messages[-memory_count:]
# 消息不能超过token限制
# todo 压缩过去消息
messages = discard_overlimit_messages(messages, model, max_token)
# send to OpenAI
params = dict(
stream=True, messages=messages, model=model, top_p=top_p, timeout=timeout
)
if parent_message_id:
params["request_id"] = parent_message_id
res = await _chat_completions_create_async(params)
if res is None:
yield Errors.SOMETHING_WRONG_IN_OPENAI_GPT_API.value
return
# 处理结果
text = ""
role = ""
prev_message_dict = {}
for openai_object in res:
openai_object_dict = openai_object.to_dict_recursive()
prev_message_dict = openai_object_dict
if not role:
role = openai_object_dict["choices"][0]["delta"].get("role", "")
text_delta = openai_object_dict["choices"][0]["delta"].get("content", "")
text += text_delta
message = json.dumps(dict(
role=role,
id=openai_object_dict["id"],
parentMessageId=parent_message_id,
text=text,
# delta=text_delta,
# detail=dict(
# id=openai_object_dict["id"],
# object=openai_object_dict["object"],
# # created=openai_object_dict["created"],
# # model=openai_object_dict["model"],
# # choices=openai_object_dict["choices"]
# )
))
yield "data: " + message
except:
err = traceback.format_exc()
logger.error(err)
yield Errors.SOMETHING_WRONG.value
return
try:
# save to cache
chat = {"role": role, "content": text}
messages.append(chat)
parent_message_id = prev_message_dict["id"]
message_store.set(parent_message_id, messages)
except:
err = traceback.format_exc()
logger.error(err)
@on_exception(expo, openai.error.RateLimitError, max_tries=5)
def _moderation_create(params):
return openai.Moderation.create(**params)
async def _moderation_create_async(params):
with concurrent.futures.ThreadPoolExecutor() as executor:
try:
result = await asyncio.get_event_loop().run_in_executor(
executor, _moderation_create, params
)
except:
err = traceback.format_exc()
logger.error(err)
# 这里处理 openai.error.RateLimitError 之外的错误
return None
return result
@on_exception(expo, openai.error.RateLimitError, max_tries=5)
def _chat_completions_create(params):
return openai.ChatCompletion.create(**params)
async def _chat_completions_create_async(params):
with concurrent.futures.ThreadPoolExecutor() as executor:
try:
result = await asyncio.get_event_loop().run_in_executor(
executor, _chat_completions_create, params
)
except:
err = traceback.format_exc()
logger.error(err)
# 这里处理 openai.error.RateLimitError 之外的错误
return None
return result
================================================
FILE: service/entrypoint.sh
================================================
#!/bin/bash
# 将环境变量传递给 Python 脚本
export OPENAI_API_KEY="$OPENAI_API_KEY"
export OPENAI_TIMEOUT_MS="$OPENAI_TIMEOUT_MS"
export API_MODEL="$API_MODEL"
export SOCKS_PROXY="$SOCKS_PROXY"
export USE_LOCAL_WHISPER="$USE_LOCAL_WHISPER"
export HOST="$HOST"
export PORT="$PORT"
# 启动 Python 脚本
python main.py --openai_api_key="$OPENAI_API_KEY" --openai_timeout_ms="$OPENAI_TIMEOUT_MS" --api_model="$API_MODEL" --socks_proxy="$SOCKS_PROXY" --use_local_whisper="$USE_LOCAL_WHISPER" --host="$HOST" --port="$PORT"
================================================
FILE: service/errors.py
================================================
from enum import Enum
class Errors(Enum):
SOMETHING_WRONG = "ChatGptWebServerError:SomethingWrong"
SOMETHING_WRONG_IN_OPENAI_GPT_API = "ChatGptWebServerError:SomethingWrongInOpenaiGptApi"
SOMETHING_WRONG_IN_OPENAI_MODERATION_API = "ChatGptWebServerError:SomethingWrongInOpenaiModerationApi"
SOMETHING_WRONG_IN_OPENAI_WHISPER_API = "ChatGptWebServerError:SomethingWrongInOpenaiWhisperApi"
SOMETHING_WRONG_IN_LOCAL_WHISPER = "ChatGptWebServerError:SomethingWrongInLocalWhisper"
UNKNOWN_WHISPER_SETTING = "ChatGptWebServerError:UnknownWhisperSetting"
NOT_COMPLY_POLICY = "ChatGptWebServerError:NotComplyPolicy"
PROMPT_IS_EMPTY = "ChatGptWebServerError:PromptIsEmpty"
================================================
FILE: service/main.py
================================================
import openai
import os
from os.path import abspath, dirname
from loguru import logger
from chatgpt_wapper import process
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import JSONResponse, StreamingResponse
import uvicorn
from message_store import MessageStore
from whisper_wapper import process_audio_local, process_audio_api
import argparse
from api_model import ApiModel
import platform
from pathlib import Path
log_folder = os.path.join(abspath(dirname(__file__)), "log")
logger.add(os.path.join(log_folder, "{time}.log"), level="INFO")
DEFAULT_TIMEOUT_MS_STRING = "100000"
MIN_TIMEOUT_MS = 15000
DEFAULT_DB_SIZE = 100000
DEFAULT_API_MODEL = "gpt-3.5-turbo"
DEFAULT_MAX_TOKEN = 4000
CURRENT_OS = platform.system()
USE_LOCAL_WHISPER = True
current_file_path = Path(__file__).resolve()
base_directory = current_file_path.parent
AUDIO_TMP_PATH = base_directory / "audio_tmp"
LOCAL_WHISPER_MODEL_PATH = base_directory / "tools" / "local-whisper" / "model" / "ggml-tiny.bin"
LOCAL_WHISPER_BIN_PATH_LINUX = base_directory / "tools" / "local-whisper" / "linux" / "whisper.cpp-master" / "main"
LOCAL_WHISPER_BIN_PATH_WINDOWS = base_directory / "tools" / "local-whisper" / "windows" / "main.exe"
massage_store = MessageStore(db_path="message_store.json", table_name="chatgpt", max_size=DEFAULT_DB_SIZE)
app = FastAPI()
stream_response_headers = {
"Content-Type": "application/octet-stream",
"Cache-Control": "no-cache",
}
@app.post("/config")
async def config():
return JSONResponse(content=dict(
message=None,
status="Success",
data=dict(
apiModel=API_MODEL,
socksProxy=SOCKS_PROXY,
timeoutMs=OPENAI_TIMEOUT * 1000,
)
))
@app.post("/chat-process")
async def chat_process(request_data: dict):
prompt = request_data["prompt"]
options = request_data["options"]
if 1 == request_data["memory"]:
memory_count = 5
elif 50 == request_data["memory"]:
memory_count = 20
else:
memory_count = 999
if 1 == request_data["top_p"]:
top_p = 0.2
elif 50 == request_data["top_p"]:
top_p = 0.6
else:
top_p = 1
answer_text = process(prompt, options, memory_count, top_p, MASSAGE_STORE, OPENAI_TIMEOUT, MAX_TOKEN,
model=API_MODEL)
return StreamingResponse(content=answer_text, headers=stream_response_headers, media_type="text/event-stream")
@app.post("/audio-chat-process")
async def audio_chat_process(audio: UploadFile = File(...)):
if USE_LOCAL_WHISPER:
local_whisper_bin_path = LOCAL_WHISPER_BIN_PATH_LINUX if "Linux" == CURRENT_OS else LOCAL_WHISPER_BIN_PATH_WINDOWS
prompt = process_audio_local(audio,
audio_tmp_path=AUDIO_TMP_PATH,
model_path=LOCAL_WHISPER_MODEL_PATH,
local_whisper_bin_path=local_whisper_bin_path)
else:
prompt = process_audio_api(audio,
timeout=OPENAI_TIMEOUT)
return StreamingResponse(content=prompt, headers=stream_response_headers, media_type="text/event-stream")
def init_config():
# 读取配置
parser = argparse.ArgumentParser(description='')
parser.add_argument('--openai_api_key', type=str, help='API key for OpenAI')
parser.add_argument('--api_model', type=str, default="gpt-3.5-turbo",
help='OpenAI API model, default is gpt-3.5-turbo')
parser.add_argument('--socks_proxy', type=str, default="",
help='Socks proxy, default is "", e.g. http://127.0.0.1:10808')
parser.add_argument('--timeout_ms', type=str, default=DEFAULT_TIMEOUT_MS_STRING,
help="(Deprecate) Timeout for OpenAI API, default is '100000'")
parser.add_argument('--openai_timeout_ms', type=str, default=DEFAULT_TIMEOUT_MS_STRING,
help="Timeout for OpenAI API, default is '100000'")
parser.add_argument('--use_local_whisper', type=str, default='True',
help="Use local whisper or api whisper. local whisper is free, api whisper is best,"
" default is 'True'")
parser.add_argument('--host', type=str, default="0.0.0.0", help='Host for server, default is 0.0.0.0')
parser.add_argument('--port', type=str, default="3002", help="Port for server, default is '3002'")
args = parser.parse_args()
if not args.openai_api_key:
err = "OpenAI API key is not found. use --openai_api_key to set it."
logger.error(err)
raise TypeError(err)
openai_api_key = args.openai_api_key
openai.api_key = args.openai_api_key
input_api_model = args.api_model
if not input_api_model:
err = "API model is not found."
logger.error(err)
raise TypeError(err)
if input_api_model not in ApiModel.KNOWN_API_MODEL_NAMES:
warning = "Unknown Api model '{}'. Legal settings are {}, The system has automatically set it to {}".format(
input_api_model,
ApiModel.KNOWN_API_MODEL_NAMES,
DEFAULT_API_MODEL
)
logger.warning(warning)
api_model = ApiModel.get_api_model_name(input_api_model, DEFAULT_API_MODEL)
max_token = ApiModel.get_max_token(api_model, DEFAULT_MAX_TOKEN)
socks_proxy = args.socks_proxy
if socks_proxy:
logger.info("Socks proxy is enabled.")
logger.info("Socks proxy is {}.".format(socks_proxy))
openai.proxy = socks_proxy
else:
logger.info("Socks proxy is disabled.")
if DEFAULT_TIMEOUT_MS_STRING != args.timeout_ms:
logger.warning("The parameter '--timeout_ms' is deprecated, please use '--openai_timeout_ms' instead.")
args.openai_timeout_ms = args.timeout_ms
openai_timeout_ms = args.openai_timeout_ms or DEFAULT_TIMEOUT_MS_STRING
if isinstance(openai_timeout_ms, str):
try:
openai_timeout_ms = int(openai_timeout_ms)
except:
openai_timeout_ms = DEFAULT_TIMEOUT_MS_STRING
if openai_timeout_ms < MIN_TIMEOUT_MS:
openai_timeout_ms = MIN_TIMEOUT_MS
logger.warning(
"OpenAI timeout is too short, the system has automatically set it to {openai_timeout_ms}(ms).".format(
openai_timeout_ms=MIN_TIMEOUT_MS))
openai_timeout = openai_timeout_ms / 1000
# service_timeout_ms = args.service_timeout_ms or 100000
# if isinstance(service_timeout_ms, str):
# try:
# service_timeout_ms = int(service_timeout_ms)
# except:
# service_timeout_ms = 100000
# if service_timeout_ms < 15000:
# service_timeout_ms = 15000
# logger.warning("Service timeout is too short, the system has automatically set it to 15000(ms).")
# service_timeout = service_timeout_ms / 1000
# if openai_timeout_ms > service_timeout_ms:
# openai_timeout = service_timeout
# logger.warning(
# "OpenAI timeout is longer than service timeout, the system has automatically set it to the same as service timeout.")
host = args.host or "0.0.0.0"
port = args.port or 3002
if isinstance(port, str):
try:
port = int(port)
except:
err = "Port must be a number."
logger.error(err)
raise TypeError(err)
use_local_whisper = True if args.use_local_whisper in ['True', 'true'] else False
AUDIO_TMP_PATH.mkdir(parents=True, exist_ok=True)
return massage_store, openai_api_key, host, port, api_model, \
max_token, socks_proxy, openai_timeout, use_local_whisper
if __name__ == "__main__":
MASSAGE_STORE, OPENAI_API_KEY, HOST, PORT, API_MODEL, MAX_TOKEN, SOCKS_PROXY, OPENAI_TIMEOUT, USE_LOCAL_WHISPER = init_config()
logger.info("OPENAI_API_KEY:{}".format(OPENAI_API_KEY))
logger.info("HOST:{}".format(HOST))
logger.info("PORT:{}".format(PORT))
logger.info("API_MODEL:{}".format(API_MODEL))
logger.info("MAX_TOKEN:{}".format(MAX_TOKEN))
logger.info("SOCKS_PROXY:{}".format(SOCKS_PROXY))
logger.info("OPENAI_TIMEOUT_MS:{}".format(OPENAI_TIMEOUT * 1000))
logger.info("USE_LOCAL_WHISPER:{}".format(USE_LOCAL_WHISPER))
logger.info("AUDIO_TMP_PATH:{}".format(AUDIO_TMP_PATH))
uvicorn.run(app, host=HOST, port=PORT)
================================================
FILE: service/message_store.py
================================================
import time
from tinydb import TinyDB, Query
class MessageStore:
def __init__(self, db_path, table_name, max_size=100000):
self.db = TinyDB(db_path)
self.table = self.db.table(table_name)
self.max_size = max_size
def set(self, key, value):
if len(self.table) >= self.max_size:
self._delete_oldest()
self.table.insert({'key': key, 'value': value, 'timestamp': time.time()})
def get_from_key(self, key):
query = Query()
result = self.table.get(query.key == key)
if result is None:
return None
return result['value']
def _delete_oldest(self):
records = self.table.all()
if len(records) >= self.max_size:
oldest_record = sorted(records, key=lambda r: r['timestamp'])[0]
self.table.remove(doc_ids=[oldest_record.doc_id])
================================================
FILE: service/tools/__init__.py
================================================
================================================
FILE: service/tools/local-whisper/__init__.py
================================================
================================================
FILE: service/tools/local-whisper/linux/init.sh
================================================
sudo apt update
sudo apt install build-essential -y
wget https://github.com/ggerganov/whisper.cpp/archive/refs/heads/master.zip
unzip master.zip
rm master.zip
cd whisper.cpp-master || exit
make
================================================
FILE: service/tools/openai_token_control.py
================================================
import tiktoken
MAX_RANGES = 10
def discard_overlimit_messages(messages: list, model: str, max_token: int) -> list:
"""
Discards messages that exceed the maximum number of tokens allowed by OpenAI.
"""
range_count = 0
while True:
# 如果消息太少,就不处理
if len(messages) <= 2:
return messages
# 防止意外(例如num_tokens_from_messages的实现没有及时跟随openai更新)出现死循环
if range_count > MAX_RANGES:
return messages
token_count = num_tokens_from_messages(messages, model=model)
range_count += 1
if token_count <= max_token:
return messages
else:
# 去掉过去的一半消息,给回答留下足够空间
# 通常来说问题比较短,回复比较长,如果只去掉最远的1、2条消息,可能会导致问题占了大部分token,比方说4090个
# 在最大token只能有4096个的情况下,回复只能有6个token,这样就会导致回复被截断
messages = messages[int(len(messages) / 2):]
continue
def num_tokens_from_string(string: str, encoding_name: str) -> int:
"""Returns the number of tokens in a text string."""
encoding = tiktoken.get_encoding(encoding_name)
num_tokens = len(encoding.encode(string))
return num_tokens
def num_tokens_from_messages(messages, model="gpt-3.5-turbo"):
"""Returns the number of tokens used by a list of messages."""
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
print("Warning: model not found. Using cl100k_base encoding.")
encoding = tiktoken.get_encoding("cl100k_base")
if model.startswith("gpt-3.5"):
tokens_per_message = 4
tokens_per_name = -1
elif model.startswith("gpt-4"):
tokens_per_message = 3
tokens_per_name = 1
else:
tokens_per_message = 4
tokens_per_name = -1
#raise NotImplementedError(
# f"""num_tokens_from_messages() is not implemented for model {model}. See https://github.com/openai/openai-python/blob/main/chatml.md for information on how messages are converted to tokens.""")
num_tokens = 0
for message in messages:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(encoding.encode(value))
if key == "name":
num_tokens += tokens_per_name
num_tokens += 3 # every reply is primed with <|start|>assistant<|message|>
return num_tokens
if __name__ == '__main__':
print(num_tokens_from_string("test", "cl100k_base"))
messages = [{"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "test"}]
print(num_tokens_from_messages(messages, "gpt-3.5-turbo-0301"))
print(num_tokens_from_messages(messages, "gpt-4"))
print(num_tokens_from_messages(messages, "gpt-3.5-turbo"))
print(num_tokens_from_messages(messages, "gpt-3.5-turbo-16k"))
================================================
FILE: service/whisper_wapper.py
================================================
import openai
import subprocess
from backoff import on_exception, expo
from io import BytesIO
from typing import Optional
from fastapi import UploadFile
import concurrent.futures
import asyncio
import traceback
from loguru import logger
from errors import Errors
import shutil
from pathlib import Path
import uuid
def upload_file_to_file_obj(upload_file: UploadFile, file_obj: Optional[BytesIO] = None):
if file_obj is None:
file_obj = BytesIO()
file_obj.write(upload_file.file.read())
file_obj.seek(0)
file_obj.name = upload_file.filename
return file_obj
async def process_audio_api(audio, timeout, model="whisper-1"):
try:
file = upload_file_to_file_obj(audio)
params = dict(
model=model,
file=file,
request_timeout=timeout,
)
func = _create
transcript = await _create_async(params, func)
if transcript is None:
yield Errors.SOMETHING_WRONG_IN_OPENAI_WHISPER_API.value
return
prompt = transcript["text"]
logger.debug("audio prompt: {}".format(prompt))
del audio
del file
except:
err = traceback.format_exc()
logger.error(err)
yield Errors.SOMETHING_WRONG.value
return
if not prompt:
yield Errors.PROMPT_IS_EMPTY.value
return
yield "data: " + prompt
@on_exception(expo, openai.error.RateLimitError, max_tries=5)
def _create(params):
return openai.Audio.transcribe(**params)
async def process_audio_local(audio, audio_tmp_path, model_path, local_whisper_bin_path):
async def save_audio_file_to_disk(audio_file: UploadFile, path: Path):
loop = asyncio.get_event_loop()
with path.open("wb") as buffer:
await loop.run_in_executor(None, shutil.copyfileobj, audio_file.file, buffer)
audio_file.file.seek(0)
def delete_file_if_exists(file_path: Path):
if file_path.exists():
file_path.unlink()
filename = "{}.wav".format(str(uuid.uuid4()))
file_path = audio_tmp_path / filename
try:
await save_audio_file_to_disk(audio, file_path)
params = dict(
file_path=file_path.as_posix(),
model_path=model_path.as_posix(),
local_whisper_bin_path=local_whisper_bin_path.as_posix(),
)
func = _local_create
transcript = await _create_async(params, func)
if transcript is None:
yield Errors.SOMETHING_WRONG_IN_LOCAL_WHISPER.value
return
prompt = transcript["text"]
logger.debug("audio prompt: {}".format(prompt))
# remove leading newlines and spaces
prompt = prompt.lstrip("\n ")
if "[BLANK_AUDIO]" in prompt:
prompt = ""
except:
err = traceback.format_exc()
logger.error(err)
yield Errors.SOMETHING_WRONG.value
return
finally:
del audio
delete_file_if_exists(file_path)
if not prompt:
yield Errors.PROMPT_IS_EMPTY.value
return
yield "data: " + prompt
def _local_create(params):
args = [
'--language', 'auto',
'--model', params["model_path"],
'-f', params["file_path"],
'-nt',
]
command = [params["local_whisper_bin_path"]] + args
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode == 0:
logger.debug("Subprocess executed successfully.")
logger.debug("Output:", result.stdout)
else:
logger.debug("Subprocess execution failed.")
logger.debug("Error:", result.stderr)
return {"text": result.stdout}
async def _create_async(params, func):
with concurrent.futures.ThreadPoolExecutor() as executor:
try:
result = await asyncio.get_event_loop().run_in_executor(
executor, func, params
)
except:
err = traceback.format_exc()
logger.error(err)
return None
return result
================================================
FILE: src/App.vue
================================================
<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>
================================================
FILE: src/api/index.ts
================================================
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { post } from '@/utils/request'
export function fetchChatConfig<T = any>() {
return post<T>({
url: '/config',
})
}
export function fetchChatAPIProcess<T = any>(
params: {
prompt: string
memory: number
top_p: number
options?: { conversationId?: string; parentMessageId?: string }
signal?: GenericAbortSignal
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
},
) {
return post<T>({
url: '/chat-process',
data: {
prompt: params.prompt,
options: params.options,
memory: params.memory,
top_p: params.top_p,
},
signal: params.signal,
onDownloadProgress: params.onDownloadProgress,
})
}
export function fetchAudioChatAPIProcess<T = any>(
params: {
formData: FormData
options?: { conversationId?: string; parentMessageId?: string }
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
},
) {
return post<T>({
url: '/audio-chat-process',
data: params.formData,
onDownloadProgress: params.onDownloadProgress,
})
}
================================================
FILE: src/components/common/HoverButton/Button.vue
================================================
<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>
================================================
FILE: src/components/common/HoverButton/index.vue
================================================
<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>
================================================
FILE: src/components/common/NaiveProvider/index.vue
================================================
<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>
================================================
FILE: src/components/common/Setting/About.vue
================================================
<script setup lang='ts'>
import { onMounted, ref } from 'vue'
import { NSpin } from 'naive-ui'
import { fetchChatConfig } from '@/api'
import pkg from '@/../package.json'
import { SvgIcon } from '@/components/common'
interface ConfigState {
timeoutMs?: number
apiModel?: string
socksProxy?: string
}
const loading = ref(false)
const config = ref<ConfigState>()
async function fetchConfig() {
try {
loading.value = true
const { data } = await fetchChatConfig<ConfigState>()
config.value = data
}
catch (e) {
}
finally {
loading.value = false
}
}
onMounted(() => {
fetchConfig()
})
</script>
<template>
<NSpin :show="loading">
<div class="p-4 space-y-4">
<h2 class="text-xl font-bold">
ChatGpt Web - {{ pkg.version }}
</h2>
<a
href="https://github.com/WenJing95/chatgpt-web"
target="_blank"
class="text-[#4b9e5f] relative flex items-center"
>
View Source Code
<SvgIcon class="text-lg text-[#4b9e5f] ml-1" icon="carbon:logo-github" />
</a>
<div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
<p v-text="$t("common.about_head")" />
<p v-text="$t("common.about_body")" />
</div>
<p>{{ $t("setting.api") }}:{{ config?.apiModel ?? '-' }}</p>
<p>{{ $t("setting.timeout") }}:{{ config?.timeoutMs ?? '-' }}</p>
<p>{{ $t("setting.socks") }}:{{ config?.socksProxy ?? '-' }}</p>
</div>
</NSpin>
</template>
================================================
FILE: src/components/common/Setting/Advance.vue
================================================
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { NRadioButton, NRadioGroup, NSlider, useMessage } from 'naive-ui'
import { useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper'
import { t } from '@/locales'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const chatgpt_top_p = ref(userInfo.value.chatgpt_top_p ?? 100)
const chatgpt_memory = ref(userInfo.value.chatgpt_memory ?? 100)
const ms = useMessage()
function updateChatgptParams(options: Partial<UserInfo>) {
userStore.updateUserInfo(options)
ms.success(t('common.success'))
}
</script>
<template>
<div class="p-4 space-y-5 min-h-[200px]">
<div class="space-y-6">
<div class="flex flex-wrap items-center gap-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatgpt_memory_title') }}</span>
<div class="w-[300px]">
<NSlider
v-model:value="chatgpt_memory"
:marks="{
1: $t('setting.chatgpt_memory_choice_1'),
50: $t('setting.chatgpt_memory_choice_2'),
100: $t('setting.chatgpt_memory_choice_3'),
}"
step="mark"
@update:value="updateChatgptParams({ chatgpt_memory })"
/>
</div>
</div>
<div class="flex flex-wrap items-center gap-4">
<span class="flex-shrink-0 w-[100px]" />
<div class="w-[300px] text-gray-500">
{{ $t('setting.chatgpt_memory_memo') }}
</div>
</div>
<div class="flex flex-wrap items-center gap-4">
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatgpt_top_p_title') }}</span>
<div class="w-[400px] text-gray-500">
<NRadioGroup
v-model:value="chatgpt_top_p"
name="radiobuttongroup2"
size="medium"
@update:value="updateChatgptParams({ chatgpt_top_p })"
>
<NRadioButton
:key="0"
:value="0"
>
{{ $t('setting.chatgpt_top_p_choice_1') }}
</NRadioButton>
<NRadioButton
:key="50"
:value="50"
>
{{ $t('setting.chatgpt_top_p_choice_2') }}
</NRadioButton>
<NRadioButton
:key="100"
:value="100"
>
{{ $t('setting.chatgpt_top_p_choice_3') }}
</NRadioButton>
</NRadioGroup>
</div>
</div>
<div class="flex flex-wrap items-center gap-4">
<span class="flex-shrink-0 w-[100px]" />
<div class="w-[400px] text-gray-500">
<span v-if="0 === chatgpt_top_p">
{{ $t('setting.chatgpt_top_p_1_memo') }}
</span>
<span v-else-if="50 === chatgpt_top_p">
{{ $t('setting.chatgpt_top_p_2_memo') }}
</span>
<span v-else>
{{ $t('setting.chatgpt_top_p_3_memo') }}
</span>
</div>
</div>
</div>
</div>
</template>
================================================
FILE: src/components/common/Setting/General.vue
================================================
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { NButton, NImage, NInput, useMessage } from 'naive-ui'
import type { Language, Theme } from '@/store/modules/app/helper'
import { HoverButton, SvgIcon } from '@/components/common'
import { useAppStore, useUserStore } from '@/store'
import type { UserInfo } from '@/store/modules/user/helper'
import { t } from '@/locales'
interface Emit {
(event: 'update'): void
}
const emit = defineEmits<Emit>()
const appStore = useAppStore()
const userStore = useUserStore()
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: 'English', key: 'en-US', value: 'en-US' },
{ label: '日本語', key: 'ja-JP', value: 'ja-JP' },
]
function updateUserInfo(options: Partial<UserInfo>) {
userStore.updateUserInfo(options)
ms.success(t('common.success'))
}
function randomAvatar() {
const avatar = `https://api.multiavatar.com/${Math.random()}.svg`
userStore.updateUserInfo({ avatar })
ms.success(t('common.success'))
}
function handleReset() {
userStore.resetUserInfo()
ms.success(t('common.success'))
emit('update')
}
</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-[60px]">{{ $t('setting.avatarLink') }}</span>
<div class="w-[200px]">
<NInput v-model:value="avatar" placeholder="" />
</div>
<NButton size="small" text type="primary" @click="updateUserInfo({ avatar })">
{{ $t('common.save') }}
</NButton>
<HoverButton :tooltip="$t('setting.randomAvatar')" @click="randomAvatar()">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon class="text-lg" icon="mdi:dice-5-outline" />
</span>
</HoverButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[60px]" />
<div class="w-[200px]">
<NImage
v-model:src="userInfo.avatar"
width="100"
class="rounded-full"
/>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[60px]">{{ $t('setting.name') }}</span>
<div class="w-[200px]">
<NInput v-model:value="name" placeholder="" />
</div>
<NButton size="small" 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-[60px]">{{ $t('setting.description') }}</span>
<div class="flex-1">
<NInput v-model:value="description" placeholder="" />
</div>
<NButton size="small" text type="primary" @click="updateUserInfo({ description })">
{{ $t('common.save') }}
</NButton>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[60px]">{{ $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-[60px]">{{ $t('setting.language') }}</span>
<div class="flex flex-wrap items-center gap-4">
<template v-for="item of languageOptions" :key="item.key">
<NButton
size="small"
:type="item.key === language ? 'primary' : undefined"
@click="appStore.setLanguage(item.key)"
>
{{ item.label }}
</NButton>
</template>
</div>
</div>
<div class="flex items-center space-x-4">
<span class="flex-shrink-0 w-[60px]">{{ $t('setting.resetUserInfo') }}</span>
<NButton size="small" text type="primary" @click="handleReset">
{{ $t('common.reset') }}
</NButton>
</div>
</div>
</div>
</template>
================================================
FILE: src/components/common/Setting/index.vue
================================================
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NModal, NTabPane, NTabs } from 'naive-ui'
import General from './General.vue'
import Advance from './Advance.vue'
import About from './About.vue'
import { SvgIcon } from '@/components/common'
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
interface Props {
visible: boolean
}
interface Emit {
(e: 'update:visible', visible: boolean): void
}
const active = ref('Advance')
const reload = ref(false)
const show = computed({
get() {
return props.visible
},
set(visible: boolean) {
emit('update:visible', visible)
},
})
function handleReload() {
reload.value = true
setTimeout(() => {
reload.value = false
}, 0)
}
</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="Advance" tab="Advance">
<template #tab>
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
<span class="ml-2">{{ $t('setting.advance') }}</span>
</template>
<Advance />
</NTabPane>
<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 v-if="!reload" @update="handleReload" />
</div>
</NTabPane>
<NTabPane name="Config" tab="Config">
<template #tab>
<SvgIcon class="text-lg" icon="mdi:about-circle-outline" />
<span class="ml-2">{{ $t('setting.about') }}</span>
</template>
<About />
</NTabPane>
</NTabs>
</div>
</NModal>
</template>
================================================
FILE: src/components/common/SvgIcon/index.vue
================================================
<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>
================================================
FILE: src/components/common/UserAvatar/index.vue
================================================
<script setup lang='ts'>
import { computed } from 'vue'
import { NAvatar } from 'naive-ui'
import { useUserStore } from '@/store'
import { isString } from '@/utils/is'
import { SvgIcon } from '@/components/common'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
</script>
<template>
<div class="flex items-center">
<div class="w-10 h-10 overflow-hidden rounded-full">
<template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
<NAvatar
size="large"
round
:src="userInfo.avatar"
/>
</template>
<template v-else>
<NAvatar size="large" round />
</template>
</div>
<div class="ml-2">
<h2 class="font-bold text-md flex ">
ChatGPT Web
</h2>
<p class="text-xs text-gray-500">
<span
v-if="isString(userInfo.description) && userInfo.description !== ''"
v-text="userInfo.description"
/>
<span>
<a
href="https://github.com/WenJing95/chatgpt-web"
target="_blank"
class="text-[#4b9e5f] relative flex items-center"
>
View Source Code
<SvgIcon class="text-lg text-[#4b9e5f] ml-1" icon="carbon:logo-github" />
</a>
</span>
</p>
</div>
</div>
</template>
================================================
FILE: src/components/common/index.ts
================================================
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'
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting }
================================================
FILE: src/components/custom/GithubSite.vue
================================================
<template>
<div class="text-neutral-400">
<span>Forked and modified from </span>
<a href="https://github.com/Chanzhaoyu/chatgpt-bot" target="_blank" class="text-blue-500">
Chanzhaoyu/chatgpt-web
</a>
<span>by</span>
<a href="https://github.com/WenJing95" target="_blank" class="text-blue-500">
WenJing
</a>
</div>
</template>
================================================
FILE: src/components/custom/index.ts
================================================
import GithubSite from './GithubSite.vue'
export { GithubSite }
================================================
FILE: src/hooks/useBasicLayout.ts
================================================
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
export function useBasicLayout() {
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('sm')
return { isMobile }
}
================================================
FILE: src/hooks/useIconRender.ts
================================================
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,
}
}
================================================
FILE: src/hooks/useLanguage.ts
================================================
import { computed } from 'vue'
import { enUS, jaJP, zhCN } from 'naive-ui'
import { useAppStore } from '@/store'
import { setLocale } from '@/locales'
export function useLanguage() {
const appStore = useAppStore()
const language = computed(() => {
switch (appStore.language) {
case 'zh-CN':
setLocale('zh-CN')
return zhCN
case 'en-US':
setLocale('en-US')
return enUS
case 'ja-JP':
setLocale('ja-JP')
return jaJP
default:
setLocale('zh-CN')
return zhCN
}
})
return { language }
}
================================================
FILE: src/hooks/useTheme.ts
================================================
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(() => {
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: src/locales/en-US.ts
================================================
export default {
common: {
delete: 'Delete',
save: 'Save',
reset: 'Reset',
yes: 'Yes',
no: 'No',
noData: 'No Data',
wrong: 'Something went wrong, please try again later.',
success: 'Success',
failed: 'Failed',
about_head: 'This project was created by Chanzhaoyu, and has been forked and modified by WenJing. Its released under the MIT License.',
about_body: 'If you find this helpful, please give me a star on GitHub. If you would like to make a donate, please donate to the original author Chanzhaoyu. Thank you!',
},
chat: {
newChat: 'New 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?',
deleteMessage: 'Delete Message',
deleteMessageConfirm: 'Are you sure to delete this message?',
deleteHistoryConfirm: 'Are you sure to clear this history?',
clickToTalk: 'Click to talk',
clickToSend: 'Click to send',
recordingInProgress: '[Recording in progress...]',
openMicrophoneFailedTitle: 'Microphone Opening Failed',
openMicrophoneFailedText: 'HTTPS environment and permission are required',
stopResponding: 'Stop Responding',
preview: 'Preview',
showRawText: 'Show as raw text',
},
setting: {
setting: 'Setting',
randomAvatar: 'Generate a random avatar',
general: 'General',
advance: 'Advance',
about: 'About',
avatarLink: 'Avatar Link',
name: 'Name',
description: 'Description',
resetUserInfo: 'Reset UserInfo',
theme: 'Theme',
language: 'Language',
chatgpt_memory_title: 'ChatGpt\'s memory capacity',
chatgpt_memory_memo: 'The stronger the memory, the more context ChatGPT can remember during conversations, but it may consume more costs.',
chatgpt_memory_choice_1: 'Normal Memory (5 logs)',
chatgpt_memory_choice_2: 'Medium Memory (20 logs)',
chatgpt_memory_choice_3: 'Strong Memory (all logs)',
chatgpt_top_p_title: 'The personality of ChatGpt',
chatgpt_top_p_1_memo: 'Tends to precise analysis, reducing the possibility of ChatGPT\'s nonsense.',
chatgpt_top_p_2_memo: 'Balancing accuracy and creativity in responses.',
chatgpt_top_p_3_memo: 'Brainstorming mode, tends to provide richer information.',
chatgpt_top_p_choice_1: 'Accurate',
chatgpt_top_p_choice_2: 'Balanced personality',
chatgpt_top_p_choice_3: 'Exploratory',
api: 'API',
timeout: 'Timeout',
socks: 'Socks',
},
server: {
PromptIsEmpty: 'Hello! How can I assist you today?',
NotComplyPolicy: 'Sorry, the content you have sent does not comply with our usage policy. Please note that our platform prohibits the publication of content that involves harassment, discrimination, violence, pornography, and other violations of laws, regulations, and social ethics. If you have any questions, please contact the developer for further assistance. Thank you.',
SomethingWrong: 'Oops, something went wrong. Please try again later.',
SomethingWrongInOpenaiGptApi: 'Oops, something went wrong in OpenAI GPT API. Please try again later.',
SomethingWrongInOpenaiModerationApi: 'Oops, something went wrong in OpenAI Moderation API. Please try again later.',
SomethingWrongInOpenaiWhisperApi: 'Oops, something went wrong in OpenAI Whisper API. Please try again later.',
SomethingWrongInLocalWhisper: 'Oops, something went wrong in local Whisper API. Please contact the developer for further assistance.',
},
}
================================================
FILE: src/locales/index.ts
================================================
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import en from './en-US'
import cn from './zh-CN'
import jp from './ja-JP'
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': en,
'zh-CN': cn,
'ja-JP': jp,
},
})
export function t(key: string) {
return i18n.global.t(key)
}
export function setLocale(locale: Language) {
i18n.global.locale = locale
}
export function setupI18n(app: App) {
app.use(i18n)
}
export default i18n
================================================
FILE: src/locales/ja-JP.ts
================================================
export default {
common: {
delete: '削除',
save: '保存する',
reset: 'リセット',
yes: 'はい',
no: 'いいえ',
noData: 'データなし',
wrong: '問題が発生しました。後でもう一度試してください。',
success: '成功しました',
failed: '失敗しました',
about_head: '作成者はChanzhaoyuで、編集者はWenJingです。ライセンスはMITです。',
about_body: 'もしプロジェクトが役に立った場合は、Githubでスターをつけていただくか、元の作者に寄付をご検討いただけると幸いです。',
},
chat: {
newChat: '新しい会話',
placeholder: '何でも聞いてください...(Shift + Enter = 改行)',
placeholderMobile: '何でも聞いてください...',
copy: 'コピー',
copied: 'コピー済み',
copyCode: 'コードをコピー',
clearChat: 'チャットをクリア',
clearChatConfirm: 'このチャットをクリアしてもよろしいですか?',
deleteMessage: 'メッセージを削除',
deleteMessageConfirm: 'このメッセージを削除してもよろしいですか?',
deleteHistoryConfirm: 'この履歴をクリアしてもよろしいですか?',
clickToTalk: 'クリックして録音開始',
clickToSend: '送信',
recordingInProgress: '[録音中...]',
openMicrophoneFailedTitle: 'マイクのオープンに失敗しました',
openMicrophoneFailedText: 'HTTPS環境下で、設定でマイクの使用が許可されていることを確認してください',
stopResponding: '応答を停止する',
preview: 'プレビュー',
showRawText: '生のテキストを表示する',
},
setting: {
setting: '設定',
randomAvatar: 'アバターをランダムに生成する',
general: '一般',
advance: '高度な設定',
about: 'このアプリについて',
avatarLink: 'アバターリンク',
name: '名前',
description: '説明',
resetUserInfo: 'ユーザー情報をリセット',
theme: 'テーマ',
language: '言語',
chatgpt_memory_title: '記憶力',
chatgpt_memory_memo: '記憶力が強いほど、ChatGptは会話中に覚えている文脈が多くなりますが、より多くのコストがかかる可能性があります。',
chatgpt_memory_choice_1: '記憶力が弱い(5件)',
chatgpt_memory_choice_2: '記憶力が普通(20件)',
chatgpt_memory_choice_3: '記憶力が強い(すべて)',
chatgpt_top_p_title: '性格',
chatgpt_top_p_1_memo: '正確な分析に傾くことで、ChatGptの無意味な発言の可能性を減らします。',
chatgpt_top_p_2_memo: '回答の正確さと創造性のバランスを兼ね備える。',
chatgpt_top_p_3_memo: 'ブレインストーミングモードで、より豊富な情報を提供する傾向があります。',
chatgpt_top_p_choice_1: '正確性重視',
chatgpt_top_p_choice_2: '一石二鳥',
chatgpt_top_p_choice_3: 'アイデア出し重視',
api: 'API',
timeout: 'タイムアウト',
socks: 'ソックス',
},
server: {
PromptIsEmpty: 'こんにちは!今日は何かお手伝いできますか?',
NotComplyPolicy: '申し訳ありませんが、送信されたコンテンツが私たちの使用ポリシーに準拠していません。当社のプラットフォームでは、ハラスメント、差別、暴力、ポルノグラフィ、その他の法律、規制、社会倫理に違反するコンテンツの投稿を禁止しています。ご質問がある場合は、開発者にお問い合わせいただくか、サポートをご利用ください。ありがとうございます。',
SomethingWrong: '申し訳ありませんが、問題が発生しました。後でもう一度お試しください。',
SomethingWrongInOpenaiGptApi: '申し訳ありませんが、OpenAI GPT APIへのアクセス中に問題が発生しました。後でもう一度お試しください。',
SomethingWrongInOpenaiModerationApi: '申し訳ありませんが、OpenAI Moderation APIへのアクセス中に問題が発生しました。後でもう一度お試しください。',
SomethingWrongInOpenaiWhisperApi: '申し訳ありませんが、OpenAI Whisper APIへのアクセス中に問題が発生しました。後でもう一度お試しください。',
SomethingWrongInLocalWhisper: '申し訳ありませんが、Local Whisper APIへのアクセス中に問題が発生しました。開発者に連絡してさらなるサポートを求めてください。',
},
}
================================================
FILE: src/locales/zh-CN.ts
================================================
export default {
common: {
delete: '删除',
save: '保存',
reset: '重置',
yes: '是',
no: '否',
noData: '暂无数据',
wrong: '出错了,请稍后再试',
success: '操作成功',
failed: '操作失败',
about_head: '本项目原作者为Chanzhaoyu, 经WenJing分叉和修改,基于 MIT 协议开源。',
about_body: '如果你觉得此项目对你有帮助,请在Github给我点个Star;如果你希望捐助,请捐助原作者,谢谢你!',
},
chat: {
newChat: '新的对话',
placeholder: '有问题尽管问我...(Shift + Enter = 换行)',
placeholderMobile: '有问题尽管问我...',
copy: '复制',
copied: '复制成功',
copyCode: '复制代码',
clearChat: '清空会话',
clearChatConfirm: '是否清空会话?',
deleteMessage: '删除消息',
deleteMessageConfirm: '是否删除此消息?',
deleteHistoryConfirm: '确定删除此记录?',
clickToTalk: '点我开始录音',
clickToSend: '正在录音,点击发送',
recordingInProgress: '[正在录音...]',
openMicrophoneFailedTitle: '打开麦克风失败',
openMicrophoneFailedText: '需要https环境并且在设置中开启权限',
stopResponding: '停止回复',
preview: '预览',
showRawText: '显示原文',
},
setting: {
setting: '设置',
randomAvatar: '随机生成一个头像',
general: '一般',
advance: '高级',
about: '关于',
avatarLink: '头像链接',
name: '名称',
description: '描述',
resetUserInfo: '重置用户信息',
theme: '主题',
language: '语言',
chatgpt_memory_title: '记忆力',
chatgpt_memory_memo: '记忆力越强,ChatGPT 在对话过程中能记住的上下文越多,但可能会消耗更多的费用',
chatgpt_memory_choice_1: '普通记忆(5条)',
chatgpt_memory_choice_2: '中等记忆(20条)',
chatgpt_memory_choice_3: '最强记忆(全部)',
chatgpt_top_p_title: '性格',
chatgpt_top_p_1_memo: '倾向于提供精确的分析,减少ChatGpt胡说八道的可能性',
chatgpt_top_p_2_memo: '兼顾回答的准确性和想象力',
chatgpt_top_p_3_memo: '倾向于提供更丰富的信息',
chatgpt_top_p_choice_1: '准确可信',
chatgpt_top_p_choice_2: '平衡性格',
chatgpt_top_p_choice_3: '发散思维',
api: 'API',
timeout: '超时',
socks: 'Socks',
},
server: {
PromptIsEmpty: '你好!今天我能为您提供什么帮助?',
NotComplyPolicy: '对不起,您发送的内容不符合我们的使用政策。请注意,我们的平台禁止发布涉及骚扰、歧视、暴力、色情等违反法律法规和社会道德的内容。如有疑问,请联系开发者获取更多帮助。谢谢。',
SomethingWrong: '出错了,请稍后再试',
SomethingWrongInOpenaiGptApi: '访问OpenAI GPT API出错,请稍后再试',
SomethingWrongInOpenaiModerationApi: '访问OpenAI Moderation API出错,请稍后再试',
SomethingWrongInOpenaiWhisperApi: '访问OpenAI Whisper API出错,请稍后再试',
SomethingWrongInLocalWhisper: '访问本地Whisper API出错,请联系开发者获取更多帮助',
},
}
================================================
FILE: src/main.ts
================================================
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()
================================================
FILE: src/plugins/assets.ts
================================================
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
================================================
FILE: src/plugins/index.ts
================================================
import setupAssets from './assets'
import setupScrollbarStyle from './scrollbarStyle'
export { setupAssets, setupScrollbarStyle }
================================================
FILE: src/plugins/scrollbarStyle.ts
================================================
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
================================================
FILE: src/router/index.ts
================================================
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
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: '/403',
name: '403',
component: () => import('@/views/exception/403/index.vue'),
},
{
path: '/404',
name: '404',
component: () => import('@/views/exception/404/index.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
redirect: '/404',
},
]
export const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior: () => ({ left: 0, top: 0 }),
})
export async function setupRouter(app: App) {
app.use(router)
await router.isReady()
}
================================================
FILE: src/store/index.ts
================================================
import type { App } from 'vue'
import { createPinia } from 'pinia'
export const store = createPinia()
export function setupStore(app: App) {
app.use(store)
}
export * from './modules'
================================================
FILE: src/store/modules/app/helper.ts
================================================
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'appSetting'
const ZH_CN = 'zh-CN'
const EN_US = 'en-US'
const JA_JP = 'ja-JP'
export type Theme = 'light' | 'dark' | 'auto'
export type Language = typeof ZH_CN | typeof EN_US | typeof JA_JP
export type focusTextarea = true
export interface AppState {
siderCollapsed: boolean
theme: Theme
language: Language
focusTextarea: focusTextarea
}
export function defaultSetting(): AppState {
const browserLanguage = window.navigator.language || ZH_CN
let defaultLanguage: string
if (browserLanguage.includes('zh'))
defaultLanguage = ZH_CN
else if (browserLanguage.includes('ja'))
defaultLanguage = JA_JP
else if (browserLanguage.includes('en'))
defaultLanguage = EN_US
else
defaultLanguage = ZH_CN
return { siderCollapsed: false, theme: 'dark', language: defaultLanguage as Language, focusTextarea: true }
}
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)
}
================================================
FILE: src/store/modules/app/index.ts
================================================
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()
}
},
setFocusTextarea() {
this.focusTextarea = true
this.recordState()
},
recordState() {
setLocalSetting(this.$state)
},
},
})
export function useAppStoreWithOut() {
return useAppStore(store)
}
================================================
FILE: src/store/modules/chat/helper.ts
================================================
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'chatStorage'
export function defaultState(): Chat.ChatState {
const uuid = 1002
return { active: uuid, history: [{ uuid, title: 'New Chat', isEdit: false }], chat: [{ uuid, data: [] }] }
}
export function getLocalState(): Chat.ChatState {
const localState = ss.get(LOCAL_NAME)
return localState ?? defaultState()
}
export function setLocalState(state: Chat.ChatState) {
ss.set(LOCAL_NAME, state)
}
================================================
FILE: src/store/modules/chat/index.ts
================================================
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: {
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, newChatText: string) {
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 === newChatText)
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 === newChatText)
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, chat_title: string) {
if (!uuid || uuid === 0) {
if (this.chat.length) {
this.chat[0].data = []
this.history[0].title = chat_title
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)
},
},
})
================================================
FILE: src/store/modules/index.ts
================================================
export * from './app'
export * from './chat'
export * from './user'
================================================
FILE: src/store/modules/user/helper.ts
================================================
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'userStorage'
export interface UserInfo {
avatar: string
name: string
description: string
chatgpt_top_p: number
chatgpt_memory: number
}
export interface UserState {
userInfo: UserInfo
}
export function defaultSetting(): UserState {
return {
userInfo: {
avatar: 'https://api.multiavatar.com/0.8481955987976837.svg',
name: 'ChatGPT Web',
description: '',
chatgpt_top_p: 100,
chatgpt_memory: 50,
},
}
}
export function getLocalState(): UserState {
const localSetting: UserState | undefined = ss.get(LOCAL_NAME)
return { ...defaultSetting(), ...localSetting }
}
export function setLocalState(setting: UserState): void {
ss.set(LOCAL_NAME, setting)
}
================================================
FILE: src/store/modules/user/index.ts
================================================
import { defineStore } from 'pinia'
import type { UserInfo, UserState } from './helper'
import { defaultSetting, getLocalState, setLocalState } from './helper'
export const useUserStore = defineStore('user-store', {
state: (): UserState => getLocalState(),
actions: {
updateUserInfo(userInfo: Partial<UserInfo>) {
this.userInfo = { ...this.userInfo, ...userInfo }
this.recordState()
},
resetUserInfo() {
this.userInfo = { ...defaultSetting().userInfo }
this.recordState()
},
recordState() {
setLocalState(this.$state)
},
},
})
================================================
FILE: src/styles/global.less
================================================
html,
body,
#app {
height: 100%;
}
body {
padding-bottom: env(safe-area-inset-bottom);
}
.clickable-element {
cursor: pointer;
}
================================================
FILE: src/styles/lib/github-markdown.less
================================================
html.dark {
.markdown-body {
color-scheme: dark;
--color-prettylights-syntax-comment: #8b949e;
--color-prettylights-syntax-constant: #79c0ff;
--color-prettylights-syntax-entity: #d2a8ff;
--color-prettylights-syntax-storage-modifier-import: #c9d1d9;
--color-prettylights-syntax-entity-tag: #7ee787;
--color-prettylights-syntax-keyword: #ff7b72;
--color-prettylights-syntax-string: #a5d6ff;
--color-prettylights-syntax-variable: #ffa657;
--color-prettylights-syntax-brackethighlighter-unmatched: #f85149;
--color-prettylights-syntax-invalid-illegal-text: #f0f6fc;
--color-prettylights-syntax-invalid-illegal-bg: #8e1519;
--color-prettylights-syntax-carriage-return-text: #f0f6fc;
--color-prettylights-syntax-carriage-return-bg: #b62324;
--color-prettylights-syntax-string-regexp: #7ee787;
--color-prettylights-syntax-markup-list: #f2cc60;
--color-prettylights-syntax-markup-heading: #1f6feb;
--color-prettylights-syntax-markup-italic: #c9d1d9;
--color-prettylights-syntax-markup-bold: #c9d1d9;
--color-prettylights-syntax-markup-deleted-text: #ffdcd7;
--color-prettylights-syntax-markup-deleted-bg: #67060c;
--color-prettylights-syntax-markup-inserted-text: #aff5b4;
--color-prettylights-syntax-markup-inserted-bg: #033a16;
--color-prettylights-syntax-markup-changed-text: #ffdfb6;
--color-prettylights-syntax-markup-changed-bg: #5a1e02;
--color-prettylights-syntax-markup-ignored-text: #c9d1d9;
--color-prettylights-syntax-markup-ignored-bg: #1158c7;
--color-prettylights-syntax-meta-diff-range: #d2a8ff;
--color-prettylights-syntax-brackethighlighter-angle: #8b949e;
--color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;
--color-prettylights-syntax-constant-other-reference-link: #a5d6ff;
--color-fg-default: #c9d1d9;
--color-fg-muted: #8b949e;
--color-fg-subtle: #6e7681;
--color-canvas-default: #0d1117;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
--color-neutral-muted: rgba(110,118,129,0.4);
--color-accent-fg: #58a6ff;
--color-accent-emphasis: #1f6feb;
--color-attention-subtle: rgba(187,128,9,0.15);
--color-danger-fg: #f85149;
}
}
html {
.markdown-body {
color-scheme: light;
--color-prettylights-syntax-comment: #6e7781;
--color-prettylights-syntax-constant: #0550ae;
--color-prettylights-syntax-entity: #8250df;
--color-prettylights-syntax-storage-modifier-import: #24292f;
--color-prettylights-syntax-entity-tag: #116329;
--color-prettylights-syntax-keyword: #cf222e;
--color-prettylights-syntax-string: #0a3069;
--color-prettylights-syntax-variable: #953800;
--color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
--color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
--color-prettylights-syntax-invalid-illegal-bg: #82071e;
--color-prettylights-syntax-carriage-return-text: #f6f8fa;
--color-prettylights-syntax-carriage-return-bg: #cf222e;
--color-prettylights-syntax-string-regexp: #116329;
--color-prettylights-syntax-markup-list: #3b2300;
--color-prettylights-syntax-markup-heading: #0550ae;
--color-prettylights-syntax-markup-italic: #24292f;
--color-prettylights-syntax-markup-bold: #24292f;
--color-prettylights-syntax-markup-deleted-text: #82071e;
--color-prettylights-syntax-markup-deleted-bg: #ffebe9;
--color-prettylights-syntax-markup-inserted-text: #116329;
--color-prettylights-syntax-markup-inserted-bg: #dafbe1;
--color-prettylights-syntax-markup-changed-text: #953800;
--color-prettylights-syntax-markup-changed-bg: #ffd8b5;
--color-prettylights-syntax-markup-ignored-text: #eaeef2;
--color-prettylights-syntax-markup-ignored-bg: #0550ae;
--color-prettylights-syntax-meta-diff-range: #8250df;
--color-prettylights-syntax-brackethighlighter-angle: #57606a;
--color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
--color-prettylights-syntax-constant-other-reference-link: #0a3069;
--color-fg-default: #24292f;
--color-fg-muted: #57606a;
--color-fg-subtle: #6e7781;
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210,18%,87%,1);
--color-neutral-muted: rgba(175,184,193,0.2);
--color-accent-fg: #0969da;
--color-accent-emphasis: #0969da;
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
}
}
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: var(--color-fg-default);
background-color: var(--color-canvas-default);
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: ' ';
display: inline-block;
background-color: currentColor;
-webkit-mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
mask-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>");
}
.markdown-body details,
.markdown-body figcaption,
.markdown-body figure {
display: block;
}
.markdown-body summary {
display: list-item;
}
.markdown-body [hidden] {
display: none !important;
}
.markdown-body a {
background-color: transparent;
color: var(--color-accent-fg);
text-decoration: none;
}
.markdown-body abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
}
.markdown-body b,
.markdown-body strong {
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body dfn {
font-style: italic;
}
.markdown-body h1 {
margin: .67em 0;
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: .3em;
font-size: 2em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body mark {
background-color: var(--color-attention-subtle);
color: var(--color-fg-default);
}
.markdown-body small {
font-size: 90%;
}
.markdown-body sub,
.markdown-body sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-body sub {
bottom: -0.25em;
}
.markdown-body sup {
top: -0.5em;
}
.markdown-body img {
border-style: none;
max-width: 100%;
box-sizing: content-box;
background-color: var(--color-canvas-default);
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre,
.markdown-body samp {
font-family: monospace;
font-size: 1em;
}
.markdown-body figure {
margin: 1em 40px;
}
.markdown-body hr {
box-sizing: content-box;
overflow: hidden;
background: transparent;
border-bottom: 1px solid var(--color-border-muted);
height: .25em;
padding: 0;
margin: 24px 0;
background-color: var(--color-border-default);
border: 0;
}
.markdown-body input {
font: inherit;
margin: 0;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body [type=button],
.markdown-body [type=reset],
.markdown-body [type=submit] {
-webkit-appearance: button;
}
.markdown-body [type=checkbox],
.markdown-body [type=radio] {
box-sizing: border-box;
padding: 0;
}
.markdown-body [type=number]::-webkit-inner-spin-button,
.markdown-body [type=number]::-webkit-outer-spin-button {
height: auto;
}
.markdown-body [type=search]::-webkit-search-cancel-button,
.markdown-body [type=search]::-webkit-search-decoration {
-webkit-appearance: none;
}
.markdown-body ::-webkit-input-placeholder {
color: inherit;
opacity: .54;
}
.markdown-body ::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body ::placeholder {
color: var(--color-fg-subtle);
opacity: 1;
}
.markdown-body hr::before {
display: table;
content: "";
}
.markdown-body hr::after {
display: table;
clear: both;
content: "";
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
overflow: auto;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body details summary {
cursor: pointer;
}
.markdown-body details:not([open])>*:not(summary) {
display: none !important;
}
.markdown-body a:focus,
.markdown-body [role=button]:focus,
.markdown-body input[type=radio]:focus,
.markdown-body input[type=checkbox]:focus {
outline: 2px solid var(--color-accent-fg);
outline-offset: -2px;
box-shadow: none;
}
.markdown-body a:focus:not(:focus-visible),
.markdown-body [role=button]:focus:not(:focus-visible),
.markdown-body input[type=radio]:focus:not(:focus-visible),
.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
outline: solid 1px transparent;
}
.markdown-body a:focus-visible,
.markdown-body [role=button]:focus-visible,
.markdown-body input[type=radio]:focus-visible,
.markdown-body input[type=checkbox]:focus-visible {
outline: 2px solid var(--color-accent-fg);
outline-offset: -2px;
box-shadow: none;
}
.markdown-body a:not([class]):focus,
.markdown-body a:not([class]):focus-visible,
.markdown-body input[type=radio]:focus,
.markdown-body input[type=radio]:focus-visible,
.markdown-body input[type=checkbox]:focus,
.markdown-body input[type=checkbox]:focus-visible {
outline-offset: 0;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
line-height: 10px;
color: var(--color-fg-default);
vertical-align: middle;
background-color: var(--color-canvas-subtle);
border: solid 1px var(--color-neutral-muted);
border-bottom-color: var(--color-neutral-muted);
border-radius: 6px;
box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: var(--base-text-weight-semibold, 600);
line-height: 1.25;
}
.markdown-body h2 {
font-weight: var(--base-text-weight-semibold, 600);
padding-bottom: .3em;
font-size: 1.5em;
border-bottom: 1px solid var(--color-border-muted);
}
.markdown-body h3 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1.25em;
}
.markdown-body h4 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: 1em;
}
.markdown-body h5 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: .875em;
}
.markdown-body h6 {
font-weight: var(--base-text-weight-semibold, 600);
font-size: .85em;
color: var(--color-fg-muted);
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
padding: 0 1em;
color: var(--color-fg-muted);
border-left: .25em solid var(--color-border-default);
}
.markdown-body ul,
.markdown-body ol {
margin-top: 0;
margin-bottom: 0;
padding-left: 2em;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body tt,
.markdown-body code,
.markdown-body samp {
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;
font-size: 12px;
word-wrap: normal;
}
.markdown-body .octicon {
display: inline-block;
overflow: visible !important;
vertical-align: text-bottom;
fill: currentColor;
}
.markdown-body input::-webkit-outer-spin-button,
.markdown-body input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.markdown-body::before {
display: table;
content: "";
}
.markdown-body::after {
display: table;
clear: both;
content: "";
}
.markdown-body>*:first-child {
margin-top: 0 !important;
}
.markdown-body>*:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .absent {
color: var(--color-danger-fg);
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre,
.markdown-body details {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: var(--color-fg-default);
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 tt,
.markdown-body h1 code,
.markdown-body h2 tt,
.markdown-body h2 code,
.markdown-body h3 tt,
.markdown-body h3 code,
.markdown-body h4 tt,
.markdown-body h4 code,
.markdown-body h5 tt,
.markdown-body h5 code,
.markdown-body h6 tt,
.markdown-body h6 code {
padding: 0 .2em;
font-size: inherit;
}
.markdown-body summary h1,
.markdown-body summary h2,
.markdown-body summary h3,
.markdown-body summary h4,
.markdown-body summary h5,
.markdown-body summary h6 {
display: inline-block;
}
.markdown-body summary h1 .anchor,
.markdown-body summary h2 .anchor,
.markdown-body summary h3 .anchor,
.markdown-body summary h4 .anchor,
.markdown-body summary h5 .anchor,
.markdown-body summary h6 .anchor {
margin-left: -40px;
}
.markdown-body summary h1,
.markdown-body summary h2 {
padding-bottom: 0;
border-bottom: 0;
}
.markdown-body ul.no-list,
.markdown-body ol.no-list {
padding: 0;
list-style-type: none;
}
.markdown-body ol[type=a] {
list-style-type: lower-alpha;
}
.markdown-body ol[type=A] {
list-style-type: upper-alpha;
}
.markdown-body ol[type=i] {
list-style-type: lower-roman;
}
.markdown-body ol[type=I] {
list-style-type: upper-roman;
}
.markdown-body ol[type="1"] {
list-style-type: decimal;
}
.markdown-body div>ol:not([type]) {
list-style-type: decimal;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body li+li {
margin-top: .25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table th {
font-weight: var(--base-text-weight-semibold, 600);
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
.markdown-body table tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
.markdown-body table tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
.markdown-body table img {
background-color: transparent;
}
.markdown-body img[align=right] {
padding-left: 20px;
}
.markdown-body img[align=left] {
padding-right: 20px;
}
.markdown-body .emoji {
max-width: none;
vertical-align: text-top;
background-color: transparent;
}
.markdown-body span.frame {
display: block;
overflow: hidden;
}
.markdown-body span.frame>span {
display: block;
float: left;
width: auto;
padding: 7px;
margin: 13px 0 0;
overflow: hidden;
border: 1px solid var(--color-border-default);
}
.markdown-body span.frame span img {
display: block;
float: left;
}
.markdown-body span.frame span span {
display: block;
padding: 5px 0 0;
clear: both;
color: var(--color-fg-default);
}
.markdown-body span.align-center {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-center>span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: center;
}
.markdown-body span.align-center span img {
margin: 0 auto;
text-align: center;
}
.markdown-body span.align-right {
display: block;
overflow: hidden;
clear: both;
}
.markdown-body span.align-right>span {
display: block;
margin: 13px 0 0;
overflow: hidden;
text-align: right;
}
.markdown-body span.align-right span img {
margin: 0;
text-align: right;
}
.markdown-body span.float-left {
display: block;
float: left;
margin-right: 13px;
overflow: hidden;
}
.markdown-body span.float-left span {
margin: 13px 0 0;
}
.markdown-body span.float-right {
display: block;
float: right;
margin-left: 13px;
overflow: hidden;
}
.markdown-body span.float-right>span {
display: block;
margin: 13px auto 0;
overflow: hidden;
text-align: right;
}
.markdown-body code,
.markdown-body tt {
padding: .2em .4em;
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: var(--color-neutral-muted);
border-radius: 6px;
}
.markdown-body code br,
.markdown-body tt br {
display: none;
}
.markdown-body del code {
text-decoration: inherit;
}
.markdown-body samp {
font-size: 85%;
}
.markdown-body pre code {
font-size: 100%;
}
.markdown-body pre>code {
padding: 0;
margin: 0;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--color-canvas-subtle);
border-radius: 6px;
}
.markdown-body pre code,
.markdown-body pre tt {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body .csv-data td,
.markdown-body .csv-data th {
padding: 5px;
overflow: hidden;
font-size: 12px;
line-height: 1;
text-align: left;
white-space: nowrap;
}
.markdown-body .csv-data .blob-num {
padding: 10px 8px 9px;
text-align: right;
background: var(--color-canvas-default);
border: 0;
}
.markdown-body .csv-data tr {
border-top: 0;
}
.markdown-body .csv-data th {
font-weight: var(--base-text-weight-semibold, 600);
background: var(--color-canvas-subtle);
border-top: 0;
}
.markdown-body [data-footnote-ref]::before {
content: "[";
}
.markdown-body [data-footnote-ref]::after {
content: "]";
}
.markdown-body .footnotes {
font-size: 12px;
color: var(--color-fg-muted);
border-top: 1px solid var(--color-border-default);
}
.markdown-body .footnotes ol {
padding-left: 16px;
}
.markdown-body .footnotes ol ul {
display: inline-block;
padding-left: 16px;
margin-top: 16px;
}
.markdown-body .footnotes li {
position: relative;
}
.markdown-body .footnotes li:target::before {
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -24px;
pointer-events: none;
content: "";
border: 2px solid var(--color-accent-emphasis);
border-radius: 6px;
}
.markdown-body .footnotes li:target {
color: var(--color-fg-default);
}
.markdown-body .footnotes .data-footnote-backref g-emoji {
font-family: monospace;
}
.markdown-body .pl-c {
color: var(--color-prettylights-syntax-comment);
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: var(--color-prettylights-syntax-constant);
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: var(--color-prettylights-syntax-entity);
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: var(--color-prettylights-syntax-storage-modifier-import);
}
.markdown-body .pl-ent {
color: var(--color-prettylights-syntax-entity-tag);
}
.markdown-body .pl-k {
color: var(--color-prettylights-syntax-keyword);
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: var(--color-prettylights-syntax-string);
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: var(--color-prettylights-syntax-variable);
}
.markdown-body .pl-bu {
color: var(--color-prettylights-syntax-brackethighlighter-unmatched);
}
.markdown-body .pl-ii {
color: var(--color-prettylights-syntax-invalid-illegal-text);
background-color: var(--color-prettylights-syntax-invalid-illegal-bg);
}
.markdown-body .pl-c2 {
color: var(--color-prettylights-syntax-carriage-return-text);
background-color: var(--color-prettylights-syntax-carriage-return-bg);
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: var(--color-prettylights-syntax-string-regexp);
}
.markdown-body .pl-ml {
color: var(--color-prettylights-syntax-markup-list);
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-heading);
}
.markdown-body .pl-mi {
font-style: italic;
color: var(--color-prettylights-syntax-markup-italic);
}
.markdown-body .pl-mb {
font-weight: bold;
color: var(--color-prettylights-syntax-markup-bold);
}
.markdown-body .pl-md {
color: var(--color-prettylights-syntax-markup-deleted-text);
background-color: var(--color-prettylights-syntax-markup-deleted-bg);
}
.markdown-body .pl-mi1 {
color: var(--color-prettylights-syntax-markup-inserted-text);
background-color: var(--color-prettylights-syntax-markup-inserted-bg);
}
.markdown-body .pl-mc {
color: var(--color-prettylights-syntax-markup-changed-text);
background-color: var(--color-prettylights-syntax-markup-changed-bg);
}
.markdown-body .pl-mi2 {
color: var(--color-prettylights-syntax-markup-ignored-text);
background-color: var(--color-prettylights-syntax-markup-ignored-bg);
}
.markdown-body .pl-mdr {
font-weight: bold;
color: var(--color-prettylights-syntax-meta-diff-range);
}
.markdown-body .pl-ba {
color: var(--color-prettylights-syntax-brackethighlighter-angle);
}
.markdown-body .pl-sg {
color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);
}
.markdown-body .pl-corl {
text-decoration: underline;
color: var(--color-prettylights-syntax-constant-other-reference-link);
}
.markdown-body g-emoji {
display: inline-block;
min-width: 1ch;
font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
font-size: 1em;
font-style: normal !important;
font-weight: var(--base-text-weight-normal, 400);
line-height: 1;
vertical-align: -0.075em;
}
.markdown-body g-emoji img {
width: 1em;
height: 1em;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item label {
font-weight: var(--base-text-weight-normal, 400);
}
.markdown-body .task-list-item.enabled label {
cursor: pointer;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 4px;
}
.markdown-body .task-list-item .handle {
display: none;
}
.markdown-body .task-list-item-checkbox {
margin: 0 .2em .25em -1.4em;
vertical-align: middle;
}
.markdown-body .contains-task-list:dir(rtl) .task-list-item-checkbox {
margin: 0 -1.6em .25em .2em;
}
.markdown-body .contains-task-list {
position: relative;
}
.markdown-body .contains-task-list:hover .task-list-item-convert-container,
.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
display: block;
width: auto;
height: 24px;
overflow: visible;
clip: auto;
}
.markdown-body ::-webkit-calendar-picker-indicator {
filter: invert(50%);
}
================================================
FILE: src/styles/lib/highlight.less
================================================
html.dark {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
.hljs {
color: #abb2bf;
background: #282c34
}
.hljs-keyword,
.hljs-operator,
.hljs-pattern-match {
color: #f92672
}
.hljs-function,
.hljs-pattern-match .hljs-constructor {
color: #61aeee
}
.hljs-function .hljs-params {
color: #a6e22e
}
.hljs-function .hljs-params .hljs-typing {
color: #fd971f
}
.hljs-module-access .hljs-module {
color: #7e57c2
}
.hljs-constructor {
color: #e2b93d
}
.hljs-constructor .hljs-string {
color: #9ccc65
}
.hljs-comment,
.hljs-quote {
color: #b18eb1;
font-style: italic
}
.hljs-doctag,
.hljs-formula {
color: #c678dd
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e06c75
}
.hljs-literal {
color: #56b6c2
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #98c379
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #e6c07b
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #d19a66
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #61aeee
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}
html {
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px;
&::-webkit-scrollbar {
height: 4px;
}
}
.hljs {
color: #383a42;
background: #fafafa
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic
}
.hljs-doctag,
.hljs-formula,
.hljs-keyword {
color: #a626a4
}
.hljs-deletion,
.hljs-name,
.hljs-section,
.hljs-selector-tag,
.hljs-subst {
color: #e45649
}
.hljs-literal {
color: #0184bb
}
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string,
.hljs-regexp,
.hljs-string {
color: #50a14f
}
.hljs-attr,
.hljs-number,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-pseudo,
.hljs-template-variable,
.hljs-type,
.hljs-variable {
color: #986801
}
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-symbol,
.hljs-title {
color: #4078f2
}
.hljs-built_in,
.hljs-class .hljs-title,
.hljs-title.class_ {
color: #c18401
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}
.hljs-link {
text-decoration: underline
}
}
================================================
FILE: src/styles/lib/tailwind.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
================================================
FILE: src/typings/chat.d.ts
================================================
declare namespace Chat {
interface Chat {
dateTime: string
text: string
inversion?: boolean
error?: boolean
loading?: boolean
conversationOptions?: ConversationRequest | null
requestOptions: { prompt: string; options?: ConversationRequest | null }
}
interface History {
title: string
isEdit: boolean
uuid: number
}
interface ChatState {
active: number | null
history: History[]
chat: { uuid: number; data: Chat[] }[]
}
interface ConversationRequest {
conversationId?: string
parentMessageId?: string
}
interface ConversationResponse {
conversationId: string
detail: {
choices: { finish_reason: string; index: number; logprobs: any; text: string }[]
created: number
id: string
model: string
object: string
usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number }
}
id: string
parentMessageId: string
role: string
text: string
}
}
================================================
FILE: src/typings/env.d.ts
================================================
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_GLOB_API_URL: string;
readonly VITE_GLOB_API_TIMEOUT: string;
readonly VITE_APP_API_BASE_URL: string;
readonly VITE_GLOB_OPEN_LONG_REPLY: string;
readonly VITE_GLOB_APP_PWA: string;
}
================================================
FILE: src/typings/global.d.ts
================================================
interface Window {
$loadingBar?: import('naive-ui').LoadingBarProviderInst;
$dialog?: import('naive-ui').DialogProviderInst;
$message?: import('naive-ui').MessageProviderInst;
$notification?: import('naive-ui').NotificationProviderInst;
}
================================================
FILE: src/utils/crypto/index.ts
================================================
import CryptoJS from 'crypto-js'
const CryptoSecret = '__CRYPTO_SECRET__'
export function enCrypto(data: any) {
const str = JSON.stringify(data)
return CryptoJS.AES.encrypt(str, CryptoSecret).toString()
}
export function deCrypto(data: string) {
const bytes = CryptoJS.AES.decrypt(data, CryptoSecret)
const str = bytes.toString(CryptoJS.enc.Utf8)
if (str)
return JSON.parse(str)
return null
}
================================================
FILE: src/utils/format/index.ts
================================================
/**
* 转义 HTML 字符
* @param source
*/
export function encodeHTML(source: string) {
return source
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
/**
* 判断是否为代码块
* @param text
*/
export function includeCode(text: string | null | undefined) {
const regexp = /^(?:\s{4}|\t).+/gm
return !!(text?.includes(' = ') || text?.match(regexp))
}
/**
* 复制文本
* @param options
*/
export function copyText(options: { text: string; origin?: boolean }) {
const props = { origin: true, ...options }
let input: HTMLInputElement | HTMLTextAreaElement
if (props.origin)
input = document.createElement('textarea')
else
input = document.createElement('input')
input.setAttribute('readonly', 'readonly')
input.value = props.text
document.body.appendChild(input)
input.select()
if (document.execCommand('copy'))
document.execCommand('copy')
document.body.removeChild(input)
}
================================================
FILE: src/utils/is/index.ts
================================================
export function isNumber<T extends number>(value: T | unknown): value is number {
return Object.prototype.toString.call(value) === '[object Number]'
}
export function isString<T extends string>(value: T | unknown): value is string {
return Object.prototype.toString.call(value) === '[object String]'
}
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
return Object.prototype.toString.call(value) === '[object Boolean]'
}
export function isNull<T extends null>(value: T | unknown): value is null {
return Object.prototype.toString.call(value) === '[object Null]'
}
export function isUndefined<T extends undefined>(value: T | unknown): value is undefined {
return Object.prototype.toString.call(value) === '[object Undefined]'
}
export function isObject<T extends object>(value: T | unknown): value is object {
return Object.prototype.toString.call(value) === '[object Object]'
}
export function isArray<T extends any[]>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Array]'
}
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Function]'
}
export function isDate<T extends Date>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Date]'
}
export function isRegExp<T extends RegExp>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object RegExp]'
}
export function isPromise<T extends Promise<any>>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Promise]'
}
export function isSet<T extends Set<any>>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Set]'
}
export function isMap<T extends Map<any, any>>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object Map]'
}
export function isFile<T extends File>(value: T | unknown): value is T {
return Object.prototype.toString.call(value) === '[object File]'
}
================================================
FILE: src/utils/request/axios.ts
================================================
import axios, { type AxiosResponse } from 'axios'
const service = axios.create({
baseURL: import.meta.env.VITE_GLOB_API_URL,
})
service.interceptors.request.use(
(config) => {
return config
},
(error) => {
return Promise.reject(error.response)
},
)
service.interceptors.response.use(
(response: AxiosResponse): AxiosResponse => {
if (response.status === 200)
return response
throw new Error(response.status.toString())
},
(error) => {
return Promise.reject(error)
},
)
export default service
================================================
FILE: src/utils/request/index.ts
================================================
import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'
import request from './axios'
export interface HttpOption {
url: string
data?: any
method?: string
headers?: any
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void
signal?: GenericAbortSignal
beforeRequest?: () => void
afterRequest?: () => void
}
export interface Response<T = any> {
data: T
message: string | null
status: string
}
function http<T = any>(
{ url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
) {
const successHandler = (res: AxiosResponse<Response<T>>) => {
if (res.data.status === 'Success' || typeof res.data === 'string')
return res.data
return Promise.reject(res.data)
}
const failHandler = (error: Response<Error>) => {
afterRequest?.()
throw new Error(error?.message || 'Error')
}
beforeRequest?.()
method = method || 'GET'
const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
return method === 'GET'
? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler)
: request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler)
}
export function get<T = any>(
{ url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
): Promise<Response<T>> {
return http<T>({
url,
method,
data,
onDownloadProgress,
signal,
beforeRequest,
afterRequest,
})
}
export function post<T = any>(
{ url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
): Promise<Response<T>> {
return http<T>({
url,
method,
data,
headers,
onDownloadProgress,
signal,
beforeRequest,
afterRequest,
})
}
export default post
================================================
FILE: src/utils/storage/index.ts
================================================
export * from './local'
================================================
FILE: src/utils/storage/local.ts
================================================
import { deCrypto, enCrypto } from '../crypto'
interface StorageData<T = any> {
data: T
expire: number | null
}
export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) {
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
const { expire, crypto } = Object.assign(
{
expire: DEFAULT_CACHE_TIME,
crypto: true,
},
options,
)
function set<T = any>(key: string, data: T) {
const storageData: StorageData<T> = {
data,
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
}
const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData)
window.localStorage.setItem(key, json)
}
function get(key: string) {
const json = window.localStorage.getItem(key)
if (json) {
let storageData: StorageData | null = null
try {
storageData = crypto ? deCrypto(json) : JSON.parse(json)
}
catch {
// Prevent failure
}
if (storageData) {
const { data, expire } = storageData
if (expire === null || expire >= Date.now())
return data
}
remove(key)
return null
}
}
function remove(key: string) {
window.localStorage.removeItem(key)
}
function clear() {
window.localStorage.clear()
}
return {
set,
get,
remove,
clear,
}
}
export const ls = createLocalStorage()
export const ss = createLocalStorage({ expire: null, crypto: false })
================================================
FILE: src/views/chat/components/Message/Avatar.vue
================================================
<script lang="ts" setup>
import { computed } from 'vue'
import { NAvatar } from 'naive-ui'
import { useUserStore } from '@/store'
import { isString } from '@/utils/is'
interface Props {
image?: boolean
}
defineProps<Props>()
const userStore = useUserStore()
const avatar = computed(() => userStore.userInfo.avatar)
</script>
<template>
<template v-if="image">
<NAvatar v-if="isString(avatar) && avatar.length > 0" :src="avatar" />
<NAvatar v-else round />
</template>
<span v-else class="text-[28px] dark:text-white">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em">
<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" fill="currentColor" />
</svg>
</span>
</template>
================================================
FILE: src/views/chat/components/Message/Text.vue
================================================
<script lang="ts" setup>
import { computed, ref } from 'vue'
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { t } from '@/locales'
interface Props {
inversion?: boolean
error?: boolean
text?: string
loading?: boolean
asRawText?: boolean
}
const props = defineProps<Props>()
const { isMobile } = useBasicLayout()
const textRef = ref<HTMLElement>()
const mdi = new MarkdownIt({
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
}
return highlightBlock(hljs.highlightAuto(code).value, '')
},
})
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
const wrapClass = computed(() => {
return [
'text-wrap',
'min-w-[20px]',
'rounded-md',
isMobile.value ? 'p-2' : 'px-3 py-2',
props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',
props.inversion ? 'dark:bg-[#a1dc95]' : 'dark:bg-[#1e1e20]',
props.inversion ? 'message-request' : 'message-reply',
{ 'text-red-500': props.error },
]
})
const text = computed(() => {
const value = props.text ?? ''
if (!props.asRawText)
return mdi.render(value)
return value
})
function highlightBlock(str: string, lang?: string) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
}
defineExpose({ textRef })
</script>
<template>
<div class="text-black" :class="wrapClass">
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion" class="flex items-end">
<div v-if="!asRawText" class="w-full markdown-body" v-html="text" />
<div v-else class="w-full whitespace-pre-wrap" v-text="text" />
<span v-if="loading" class="dark:text-white w-[4px] h-[20px] block animate-blink" />
</div>
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
</div>
</template>
<style lang="less">
@import url(./style.less);
</style>
================================================
FILE: src/views/chat/components/Message/index.vue
================================================
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NDropdown } from 'naive-ui'
import AvatarComponent from './Avatar.vue'
import TextComponent from './Text.vue'
import { SvgIcon } from '@/components/common'
import { copyText } from '@/utils/format'
import { useIconRender } from '@/hooks/useIconRender'
import { t } from '@/locales'
import { useBasicLayout } from '@/hooks/useBasicLayout'
interface Props {
dateTime?: string
text?: string
inversion?: boolean
error?: boolean
loading?: boolean
}
interface Emit {
(ev: 'regenerate'): void
(ev: 'delete'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const { isMobile } = useBasicLayout()
const { iconRender } = useIconRender()
const textRef = ref<HTMLElement>()
const asRawText = ref(props.inversion)
const messageRef = ref<HTMLElement>()
const options = computed(() => {
const common = [
{
label: t('chat.copy'),
key: 'copyText',
icon: iconRender({ icon: 'ri:file-copy-2-line' }),
},
{
label: t('common.delete'),
key: 'delete',
icon: iconRender({ icon: 'ri:delete-bin-line' }),
},
]
if (!props.inversion) {
common.unshift({
label: asRawText.value ? t('chat.preview') : t('chat.showRawText'),
key: 'toggleRenderType',
icon: iconRender({ icon: asRawText.value ? 'ic:outline-code-off' : 'ic:outline-code' }),
})
}
return common
})
function handleSelect(key: 'copyText' | 'delete' | 'toggleRenderType') {
switch (key) {
case 'copyText':
copyText({ text: props.text ?? '' })
return
case 'toggleRenderType':
asRawText.value = !asRawText.value
return
case 'delete':
emit('delete')
}
}
function handleRegenerate() {
messageRef.value?.scrollIntoView()
emit('regenerate')
}
</script>
<template>
<div
ref="messageRef"
class="flex w-full mb-6 overflow-hidden"
:class="[{ 'flex-row-reverse': inversion }]"
>
<div
class="flex items-center justify-center flex-shrink-0 h-8 overflow-hidden rounded-full basis-8"
:class="[inversion ? 'ml-2' : 'mr-2']"
>
<AvatarComponent :image="inversion" />
</div>
<div class="overflow-hidden text-md " :class="[inversion ? 'items-end' : 'items-start']">
<p class="text-sm text-[#b4bbc4]" :class="[inversion ? 'text-right' : 'text-left']">
{{ dateTime }}
</p>
<div
class="flex items-end gap-1 mt-2"
:class="[inversion ? 'flex-row-reverse' : 'flex-row']"
>
<TextComponent
ref="textRef"
:inversion="inversion"
:error="error"
:text="text"
:loading="loading"
:as-raw-text="asRawText"
/>
<div class="flex flex-col">
<button
v-if="!inversion"
class="mb-2 transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-300"
@click="handleRegenerate"
>
<SvgIcon icon="ri:restart-line" />
</button>
<NDropdown
:trigger="isMobile ? 'click' : 'hover'"
:placement="!inversion ? 'right' : 'left'"
:options="options"
@select="handleSelect"
>
<button class="transition text-neutral-300 hover:text-neutral-800 dark:hover:text-neutral-200">
<SvgIcon icon="ri:more-2-fill" />
</button>
</NDropdown>
</div>
</div>
</div>
</div>
</template>
================================================
FILE: src/views/chat/components/Message/style.less
================================================
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #fff;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy{
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
}
html.dark {
.message-reply {
.raw-text {
white-space: pre-wrap;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
================================================
FILE: src/views/chat/components/index.ts
================================================
import Message from './Message/index.vue'
export { Message }
================================================
FILE: src/views/chat/hooks/useChat.ts
================================================
import { useChatStore } from '@/store'
export function useChat() {
const chatStore = useChatStore()
const getChatByUuidAndIndex = (uuid: number, index: number) => {
return chatStore.getChatByUuidAndIndex(uuid, index)
}
const addChat = (uuid: number, chat: Chat.Chat, newChatText: string) => {
chatStore.addChatByUuid(uuid, chat, newChatText)
}
const updateChat = (uuid: number, index: number, chat: Chat.Chat) => {
chatStore.updateChatByUuid(uuid, index, chat)
}
const updateChatSome = (uuid: number, index: number, chat: Partial<Chat.Chat>) => {
chatStore.updateChatSomeByUuid(uuid, index, chat)
}
return {
addChat,
updateChat,
updateChatSome,
getChatByUuidAndIndex,
}
}
================================================
FILE: src/views/chat/hooks/useCopyCode.ts
================================================
import { onMounted, onUpdated } from 'vue'
import { copyText } from '@/utils/format'
export function useCopyCode() {
function copyCodeBlock() {
const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper')
codeBlockWrapper.forEach((wrapper) => {
const copyBtn = wrapper.querySelector('.code-block-header__copy')
const codeBlock = wrapper.querySelector('.code-block-body')
if (copyBtn && codeBlock) {
copyBtn.addEventListener('click', () => {
if (navigator.clipboard?.writeText)
navigator.clipboard.writeText(codeBlock.textContent ?? '')
else
copyText({ text: codeBlock.textContent ?? '', origin: true })
})
}
})
}
onMounted(() => copyCodeBlock())
onUpdated(() => copyCodeBlock())
}
================================================
FILE: src/views/chat/hooks/useScroll.ts
================================================
import type { Ref } from 'vue'
import { nextTick, ref } from 'vue'
type ScrollElement = HTMLDivElement | null
interface ScrollReturn {
scrollRef: Ref<ScrollElement>
scrollToBottom: () => Promise<void>
scrollToTop: () => Promise<void>
scrollToBottomIfAtBottom: () => Promise<void>
}
export function useScroll(): ScrollReturn {
const scrollRef = ref<ScrollElement>(null)
const scrollToBottom = async () => {
await nextTick()
if (scrollRef.value)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
const scrollToTop = async () => {
await nextTick()
if (scrollRef.value)
scrollRef.value.scrollTop = 0
}
const scrollToBottomIfAtBottom = async () => {
await nextTick()
if (scrollRef.value) {
const threshold = 50 // 阈值,表示滚动条到底部的距离阈值
const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight
if (distanceToBottom <= threshold)
scrollRef.value.scrollTop = scrollRef.value.scrollHeight
}
}
return {
scrollRef,
scrollToBottom,
scrollToTop,
scrollToBottomIfAtBottom,
}
}
================================================
FILE: src/views/chat/index.vue
================================================
<script setup lang='ts'>
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { NButton, NInput, useDialog } from 'naive-ui'
import Recorder from 'recorder-core/recorder.wav.min'
import { Message } from './components'
import { useScroll } from './hooks/useScroll'
import { useChat } from './hooks/useChat'
import { useCopyCode } from './hooks/useCopyCode'
import { HoverButton, SvgIcon } from '@/components/common'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useAppStore, useChatStore, useUserStore } from '@/store'
import { fetchAudioChatAPIProcess, fetchChatAPIProcess } from '@/api'
import { t } from '@/locales'
let controller = new AbortController()
const route = useRoute()
const dialog = useDialog()
const chatStore = useChatStore()
const userStore = useUserStore()
const appStore = useAppStore()
const userInfo = computed(() => userStore.userInfo)
const focusTextarea = computed(() => appStore.focusTextarea)
useCopyCode()
const { isMobile } = useBasicLayout()
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
const { scrollRef, scrollToBottom } = useScroll()
const { uuid } = route.params as { uuid: string }
const dataSources = computed(() => chatStore.getChatByUuid(+uuid))
const conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !!item.conversationOptions)))
const prompt = ref<string>('')
const isAudioPrompt = ref<boolean>(false)
const loading = ref<boolean>(false)
const recProtectionPeriod = ref<boolean>(false)
const sendingRecord = ref<boolean>(false)
const inputRef = ref<Ref | null>(null)
const recording = ref<boolean>(false)
const audioMode = ref<boolean>(false)
const actionVisible = ref<boolean>(true)
// 未知原因刷新页面,loading 状态不会重置,手动重置
dataSources.value.forEach((item, index) => {
if (item.loading)
updateChatSome(+uuid, index, { loading: false })
})
function isServerError(responseText: string) {
return responseText.startsWith('ChatGptWebServerError:')
}
function getServerErrorType(responseText: string) {
if (responseText.startsWith('ChatGptWebServerError:'))
return responseText.split(':')[1]
return ''
}
function handleSubmit() {
onConversation()
}
let rec = new Recorder()
const recOpen = function (success: Function) {
rec = Recorder({
type: 'wav',
sampleRate: 16000,
bitRate: 16,
onProcess() {
},
})
rec.open(() => {
success && success()
// },function(msg:string,isUserNotAllow:boolean){
}, () => {
dialog.warning({
title: t('chat.openMicrophoneFailedTitle'),
content: t('chat.openMicrophoneFailedText'),
})
})
}
function _recStart() { // 打开了录音后才能进行start、stop调用
rec.start()
recording.value = true
isAudioPrompt.value = true
// temporarily disable rec button
recProtectionPeriod.value = true
setTimeout(() => {
recProtectionPeriod.value = false
}, 2000)
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: t('chat.recordingInProgress'),
loading: true,
inversion: true,
error: false,
conversationOptions: null,
requestOptions: { prompt: '', options: null },
},
t('chat.newChat'),
)
scrollToBottom()
}
async function recStart() {
if (recording.value || loading.value)
return
recOpen(() => {
_recStart()
})
}
async function recStop() {
if (!rec) {
recording.value = false
return
}
sendingRecord.value = true
// rec.stop(async function(blob:Blob,duration:any){
rec.stop(async (blob: Blob) => {
rec.close()
rec = null
recording.value = false
if (!blob)
return
controller = new AbortController()
let options: Chat.ConversationRequest = {}
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions
if (lastContext)
options = { ...lastContext }
const formData = new FormData()
formData.append('audio', blob, 'recording.wav')
try {
await fetchAudioChatAPIProcess<Chat.ConversationResponse>({
formData,
options,
onDownloadProgress: ({ event }) => {
const xhr = event.target
let { responseText } = xhr
responseText = removeDataPrefix(responseText)
prompt.value = responseText
isAudioPrompt.value = true
// 如果解析音频消息出错,就显示something wrong
const isError = isServerError(responseText)
if (isError) {
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: t(`server.${getServerErrorType(responseText)}`),
inversion: false,
error: true,
loading: false,
requestOptions: { prompt: '', ...options },
},
)
scrollToBottom()
loading.value = false
sendingRecord.value = false
return
}
onConversation()
},
})
}
catch (error: any) {
const errorMessage = error?.message ?? t('common.wrong')
if (error.message === 'canceled') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
loading: false,
},
)
scrollToBottom()
return
}
const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)
if (currentChat?.text && currentChat.text !== '') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
text: `${currentChat.text}\n[${errorMessage}]`,
error: false,
loading: false,
},
)
return
}
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: '', options: { ...options } },
},
)
scrollToBottom()
}
finally {
recording.value = false
}
// },function(msg:string){
}, () => {
// console.log("rec error:"+msg);
rec.close()
rec = null
})
}
function removeDataPrefix(str: string) {
if (str && str.startsWith('data: '))
return str.slice(6)
return str
}
async function onConversation() {
const message = prompt.value
if (loading.value)
return
if (!message || message.trim() === '')
return
controller = new AbortController()
if (isAudioPrompt.value) {
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: message ?? '',
inversion: true,
error: false,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
},
)
}
else {
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: message,
inversion: true,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: null },
},
t('chat.newChat'),
)
}
scrollToBottom()
isAudioPrompt.value = false
loading.value = true
prompt.value = ''
let options: Chat.ConversationRequest = {}
const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions
if (lastContext)
options = { ...lastContext }
addChat(
+uuid,
{
dateTime: new Date().toLocaleString(),
text: '',
loading: true,
inversion: false,
error: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
t('chat.newChat'),
)
scrollToBottom()
try {
await fetchChatAPIProcess<Chat.ConversationResponse>({
prompt: message,
memory: userInfo.value.chatgpt_memory,
top_p: userInfo.value.chatgpt_top_p,
options,
signal: controller.signal,
onDownloadProgress: ({ event }) => {
const xhr = event.target
let { responseText } = xhr
responseText = removeDataPrefix(responseText)
const isError = isServerError(responseText)
if (isError) {
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: t(`server.${getServerErrorType(responseText)}`),
inversion: false,
error: true,
loading: false,
requestOptions: { prompt: message, ...options },
},
)
scrollToBottom()
return
}
// SSE response format "data: xxx"
const lastIndex = responseText.lastIndexOf('data: ')
let chunk = responseText
if (lastIndex !== -1)
chunk = responseText.substring(lastIndex)
chunk = removeDataPrefix(chunk)
try {
const data = JSON.parse(chunk)
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: data.text ?? '',
inversion: false,
error: false,
loading: false,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottom()
}
catch (error) {
//
}
},
})
}
catch (error: any) {
const errorMessage = error?.message ?? t('common.wrong')
if (error.message === 'canceled') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
loading: false,
},
)
scrollToBottom()
return
}
const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)
if (currentChat?.text && currentChat.text !== '') {
updateChatSome(
+uuid,
dataSources.value.length - 1,
{
text: `${currentChat.text}\n[${errorMessage}]`,
error: false,
loading: false,
},
)
return
}
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottom()
}
finally {
loading.value = false
sendingRecord.value = false
}
}
async function onRegenerate(index: number) {
if (loading.value)
return
controller = new AbortController()
const { requestOptions } = dataSources.value[index]
const message = requestOptions?.prompt ?? ''
let options: Chat.ConversationRequest = {}
if (requestOptions.options)
options = { ...requestOptions.options }
loading.value = true
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: '',
inversion: false,
error: false,
loading: true,
conversationOptions: null,
requestOptions: { prompt: message, ...options },
},
)
try {
await fetchChatAPIProcess<Chat.ConversationResponse>({
prompt: message,
memory: userInfo.value.chatgpt_memory,
top_p: userInfo.value.chatgpt_top_p,
options,
signal: controller.signal,
onDownloadProgress: ({ event }) => {
const xhr = event.target
let { responseText } = xhr
responseText = removeDataPrefix(responseText)
const isError = isServerError(responseText)
if (isError) {
updateChat(
+uuid,
dataSources.value.length - 1,
{
dateTime: new Date().toLocaleString(),
text: t(`server.${getServerErrorType(responseText)}`),
inversion: false,
error: true,
loading: false,
requestOptions: { prompt: message, ...options },
},
)
scrollToBottom()
return
}
// SSE response format "data: xxx"
const lastIndex = responseText.lastIndexOf('data: ')
let chunk = responseText
if (lastIndex !== -1)
chunk = responseText.substring(lastIndex)
chunk = removeDataPrefix(chunk)
try {
const data = JSON.parse(chunk)
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: data.text ?? '',
inversion: false,
error: false,
loading: false,
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
requestOptions: { prompt: message, ...options },
},
)
}
catch (error) {
//
}
},
})
}
catch (error: any) {
if (error.message === 'canceled') {
updateChatSome(
+uuid,
index,
{
loading: false,
},
)
return
}
const errorMessage = error?.message ?? t('common.wrong')
updateChat(
+uuid,
index,
{
dateTime: new Date().toLocaleString(),
text: errorMessage,
inversion: false,
error: true,
loading: false,
conversationOptions: null,
requestOptions: { prompt: message, ...options },
},
)
}
finally {
loading.value = false
}
}
watch(
focusTextarea,
() => {
setTimeout(() => {
const textarea = document.documentElement.querySelector('.n-input__textarea-el') as HTMLInputElement
if (textarea)
textarea.focus()
}, 0)
},
)
function handleDelete(index: number) {
if (loading.value)
return
dialog.warning({
title: t('chat.deleteMessage'),
content: t('chat.deleteMessageConfirm'),
positiveText: t('common.yes'),
negativeText: t('common.no'),
onPositiveClick: () => {
chatStore.deleteChatByUuid(+uuid, index)
},
})
}
// function handleClear() {
// if (loading.value)
// return
//
// dialog.warning({
// title: t('chat.clearChat'),
// content: t('chat.clearChatConfirm'),
// positiveText: t('common.yes'),
// negativeText: t('common.no'),
// onPositiveClick: () => {
// chatStore.clearChatByUuid(+uuid, t('chat.newChat'))
// },
// })
// }
function handleEnter(event: KeyboardEvent) {
if (!isMobile.value) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
else {
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault()
handleSubmit()
}
}
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function handleRec() {
if (recording.value === true)
recStop()
else
recStart()
}
function onInputFocus() {
if (isMobile.value)
actionVisible.value = false
}
function onInputBlur() {
if (isMobile.value)
actionVisible.value = true
}
const placeholder = computed(() => {
if (isMobile.value)
return t('chat.placeholderMobile')
return t('chat.placeholder')
})
const buttonDisabled = computed(() => {
return loading.value || !prompt.value || prompt.value.trim() === ''
})
const recButtonDisabled = computed(() => {
return recProtectionPeriod.value || sendingRecord.value
})
const wrapClass = computed(() => {
if (isMobile.value)
return ['pt-14']
return []
})
const footerClass = computed(() => {
let classes = ['p-4']
if (isMobile.value)
classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'overflow-hidden']
return classes
})
onMounted(() => {
scrollToBottom()
if (inputRef.value && !isMobile.value)
inputRef.value?.focus()
})
onUnmounted(() => {
if (loading.value)
controller.abort()
})
</script>
<template>
<div class="flex flex-col w-full h-full" :class="wrapClass">
<main class="flex-1 overflow-hidden">
<div
id="scrollRef"
ref="scrollRef"
class="h-full overflow-hidden overflow-y-auto"
:class="[isMobile ? 'p-2' : 'p-4']"
>
<div class="w-full max-w-screen-xl m-auto">
<template v-if="!dataSources.length">
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
<span>ChatGpt Web</span>
</div>
</template>
<template v-else>
<div>
<Message
v-for="(item, index) of dataSources"
:key="index"
:date-time="item.dateTime"
:text="item.text"
:inversion="item.inversion"
:error="item.error"
:loading="item.loading"
@regenerate="onRegenerate(index)"
@delete="handleDelete(index)"
/>
<div class="sticky bottom-0 left-0 flex justify-center">
<NButton v-if="loading" type="warning" @click="handleStop">
<template #icon>
<SvgIcon icon="ri:stop-circle-line" />
</template>
{{ $t('chat.stopResponding') }}
</NButton>
</div>
</div>
</template>
</div>
</div>
</main>
<footer :class="footerClass">
<div class="w-full max-w-screen-xl m-auto">
<div class="flex items-center justify-between space-x-2">
<div v-if="actionVisible" class="flex items-center space-x-2">
<!-- <HoverButton @click="handleClear"> -->
<!-- <span class="text-xl text-[#4f555e] dark:text-white"> -->
<!-- <SvgIcon icon="ri:delete-bin-line" /> -->
<!-- </span> -->
<!-- </HoverButton> -->
<HoverButton
v-if="!audioMode"
@click="audioMode = !audioMode"
>
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ph:microphone-bold" />
</span>
</HoverButton>
<HoverButton
v-else
@click="audioMode = !audioMode"
>
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="material-symbols:keyboard" />
</span>
</HoverButton>
</div>
<NInput
v-if="!audioMode"
ref="inputRef"
v-model:value="prompt"
autofocus
type="textarea"
:autosize="{ minRows: 1.4, maxRows: 10 }"
:placeholder="placeholder"
@keypress="handleEnter"
@focus="onInputFocus"
@blur="onInputBlur"
/>
<NButton
v-if="audioMode"
:disabled="recButtonDisabled"
style="flex-grow: 2;"
type="default"
@click="handleRec"
>
<span v-if="recording">{{ $t('chat.clickToSend') }}</span>
<span v-if="!recording">{{ $t('chat.clickToTalk') }}</span>
</NButton>
<NButton
v-if="!audioMode"
type="primary"
:disabled="buttonDisabled" @click="handleSubmit"
>
<template #icon>
<span class="dark:text-black">
<SvgIcon icon="ri:send-plane-fill" />
</span>
</template>
</NButton>
</div>
</div>
</footer>
</div>
</template>
================================================
FILE: src/views/chat/layout/Layout.vue
================================================
<script setup lang='ts'>
import { computed } from 'vue'
import { NLayout, NLayoutContent } from 'naive-ui'
import { useRouter } from 'vue-router'
import Sider from './sider/index.vue'
import Header from './header/index.vue'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useAppStore, useChatStore } from '@/store'
const router = useRouter()
const appStore = useAppStore()
const chatStore = useChatStore()
router.replace({ name: 'Chat', params: { uuid: chatStore.active } })
const { isMobile } = useBasicLayout()
const collapsed = computed(() => appStore.siderCollapsed)
const getMobileClass = computed(() => {
if (isMobile.value)
return ['rounded-none', 'shadow-none']
return ['border', 'rounded-md', 'shadow-md', 'dark:border-neutral-800']
})
const getContainerClass = computed(() => {
return [
'h-full',
{ 'pl-[260px]': !isMobile.value && !collapsed.value },
]
})
</script>
<template>
<div class="h-full dark:bg-[#24272e] transition-all" :class="[isMobile ? 'p-0' : 'p-4']">
<div class="h-full overflow-hidden" :class="getMobileClass">
<NLayout class="z-40 transition" :class="getContainerClass" has-sider>
<Sider />
<Header v-if="isMobile" />
<NLayoutContent class="h-full">
<RouterView v-slot="{ Component, route }">
<component :is="Component" :key="route.fullPath" />
</RouterView>
</NLayoutContent>
</NLayout>
</div>
</div>
</template>
================================================
FILE: src/views/chat/layout/header/index.vue
================================================
<script lang="ts" setup>
import { computed, defineAsyncComponent, nextTick, ref } from 'vue'
import { SvgIcon } from '@/components/common'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
const appStore = useAppStore()
const chatStore = useChatStore()
const collapsed = computed(() => appStore.siderCollapsed)
const currentChatHistory = computed(() => chatStore.getChatHistoryByCurrentActive)
const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
const showSetting = ref(false)
function handleUpdateCollapsed() {
appStore.setSiderCollapsed(!collapsed.value)
}
function onScrollToTop() {
const scrollRef = document.querySelector('#scrollRef')
if (scrollRef)
nextTick(() => scrollRef.scrollTop = 0)
}
function onScrollToBottom() {
const scrollRef = document.querySelector('#scrollRef')
if (scrollRef)
nextTick(() => scrollRef.scrollTop = scrollRef.scrollHeight)
}
const { isMobile } = useBasicLayout()
</script>
<template>
<header
class="fixed top-0 left-0 right-0 z-30 border-b dark:border-neutral-800 bg-white/80 dark:bg-black/20 backdrop-blur"
>
<div class="relative flex items-center justify-between h-14">
<button
class="flex items-center justify-center w-11 h-11"
@click="handleUpdateCollapsed"
>
<SvgIcon v-if="collapsed" class="text-2xl" icon="ri:align-justify" />
<SvgIcon v-else class="text-2xl" icon="ri:align-right" />
</button>
<h1
class="flex-1 px-4 overflow-hidden text-center cursor-pointer select-none text-ellipsis whitespace-nowrap"
@dblclick="onScrollToTop"
>
{{ currentChatHistory?.title ?? '' }}
</h1>
<button
v-if="!isMobile"
class="flex items-center justify-center w-11 h-11"
@click="onScrollToBottom"
>
<SvgIcon class="text-2xl" icon="ri:arrow-down-s-line" />
</button>
<HoverButton class="flex items-center justify-center w-11 h-11" :tooltip="$t('setting.setting')" @click="showSetting = true">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:settings-4-line" />
</span>
</HoverButton>
<Setting v-if="isMobile" v-model:visible="showSetting" />
</div>
</header>
</template>
================================================
FILE: src/views/chat/layout/index.ts
================================================
import ChatLayout from './Layout.vue'
export { ChatLayout }
================================================
FILE: src/views/chat/layout/sider/Footer.vue
================================================
<script setup lang='ts'>
import { defineAsyncComponent, ref } from 'vue'
import { HoverButton, SvgIcon, UserAvatar } from '@/components/common'
const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
const show = ref(false)
</script>
<template>
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800">
<div class="flex-1 flex-shrink-0 overflow-hidden">
<UserAvatar />
</div>
<HoverButton :tooltip="$t('setting.setting')" @click="show = true">
<span class="text-xl text-[#4f555e] dark:text-white">
<SvgIcon icon="ri:settings-4-line" />
</span>
</HoverButton>
<Setting v-if="show" v-model:visible="show" />
</footer>
</template>
================================================
FILE: src/views/chat/layout/sider/List.vue
================================================
<script setup lang='ts'>
import { computed } from 'vue'
import { NInput, NPopconfirm, NScrollbar } from 'naive-ui'
import { SvgIcon } from '@/components/common'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
const { isMobile } = useBasicLayout()
const appStore = useAppStore()
const chatStore = useChatStore()
const dataSources = computed(() => chatStore.history)
async function handleSelect({ uuid }: Chat.History) {
if (isActive(uuid))
return
if (chatStore.active)
chatStore.updateHistory(chatStore.active, { isEdit: false })
await chatStore.setActive(uuid)
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
function handleEdit({ uuid }: Chat.History, isEdit: boolean, event?: MouseEvent) {
event?.stopPropagation()
chatStore.updateHistory(uuid, { isEdit })
}
function handleDelete(index: number, event?: MouseEvent | TouchEvent) {
event?.stopPropagation()
chatStore.deleteHistory(index)
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
function handleEnter({ uuid }: Chat.History, isEdit: boolean, event: KeyboardEvent) {
event?.stopPropagation()
if (event.key === 'Enter')
chatStore.updateHistory(uuid, { isEdit })
}
function isActive(uuid: number) {
return chatStore.active === uuid
}
</script>
<template>
<NScrollbar class="px-4">
<div class="flex flex-col gap-2 text-sm">
<template v-if="!dataSources.length">
<div class="flex flex-col items-center mt-4 text-center text-neutral-300">
<SvgIcon icon="ri:inbox-line" class="mb-2 text-3xl" />
<span>{{ $t('common.noData') }}</span>
</div>
</template>
<template v-else>
<div v-for="(item, index) of dataSources" :key="index">
<a
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer hover:bg-neutral-100 group dark:border-neutral-800 dark:hover:bg-[#24272e]"
:class="isActive(item.uuid) && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]', 'dark:bg-[#24272e]', 'dark:border-[#4b9e5f]', 'pr-14']"
@click="handleSelect(item)"
>
<span>
<SvgIcon icon="ri:message-3-line" />
</span>
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
<NInput
v-if="item.isEdit"
v-model:value="item.title"
size="tiny"
@keypress="handleEnter(item, false, $event)"
/>
<span v-else>{{ item.title }}</span>
</div>
<div v-if="isActive(item.uuid)" class="absolute z-10 flex visible right-1">
<template v-if="item.isEdit">
<button class="p-1" @click="handleEdit(item, false, $event)">
<SvgIcon icon="ri:save-line" />
</button>
</template>
<template v-else>
<button class="p-1">
<SvgIcon icon="ri:edit-line" @click="handleEdit(item, true, $event)" />
</button>
<NPopconfirm placement="bottom" @positive-click="handleDelete(index, $event)">
<template #trigger>
<button class="p-1">
<SvgIcon icon="ri:delete-bin-line" />
</button>
</template>
{{ $t('chat.deleteHistoryConfirm') }}
</NPopconfirm>
</template>
</div>
</a>
</div>
</template>
</div>
</NScrollbar>
</template>
================================================
FILE: src/views/chat/layout/sider/index.vue
================================================
<script setup lang='ts'>
import type { CSSProperties } from 'vue'
import { computed, watch } from 'vue'
import { NButton, NLayoutSider } from 'naive-ui'
import List from './List.vue'
import Footer from './Footer.vue'
import { useAppStore, useChatStore } from '@/store'
import { useBasicLayout } from '@/hooks/useBasicLayout'
const appStore = useAppStore()
const chatStore = useChatStore()
const { isMobile } = useBasicLayout()
const collapsed = computed(() => appStore.siderCollapsed)
function handleAdd(newChatText: string) {
chatStore.addHistory({ title: newChatText, uuid: Date.now(), isEdit: false })
appStore.setFocusTextarea()
if (isMobile.value)
appStore.setSiderCollapsed(true)
}
function handleUpdateCollapsed() {
appStore.setSiderCollapsed(!collapsed.value)
}
const getMobileClass = computed<CSSProperties>(() => {
if (isMobile.value) {
return {
position: 'fixed',
zIndex: 50,
}
}
return {}
})
const mobileSafeArea = computed(() => {
if (isMobile.value) {
return {
paddingBottom: 'env(safe-area-inset-bottom)',
}
}
return {}
})
watch(
isMobile,
(val) => {
appStore.setSiderCollapsed(val)
},
{
immediate: true,
flush: 'post',
},
)
</script>
<template>
<NLayoutSider
:collapsed="collapsed"
:collapsed-width="0"
:width="260"
:show-trigger="isMobile ? false : 'arrow-circle'"
collapse-mode="transform"
position="absolute"
bordered
:style="getMobileClass"
@update-collapsed="handleUpdateCollapsed"
>
<div class="flex flex-col h-full" :style="mobileSafeArea">
<main class="flex flex-col flex-1 min-h-0">
<div class="p-4">
<NButton dashed block @click="handleAdd($t('chat.newChat'))">
{{ $t("chat.newChat") }}
</NButton>
</div>
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
<List />
</div>
</main>
<Footer />
</div>
</NLayoutSider>
<template v-if="isMobile">
<div v-show="!collapsed" class="fixed inset-0 z-40 bg-black/40" @click="handleUpdateCollapsed" />
</template>
</template>
================================================
FILE: src/views/exception/403/index.vue
================================================
<script lang="ts" setup>
import { NButton } from 'naive-ui'
import { useRouter } from 'vue-router'
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<template>
<div class="flex h-full">
<div class="px-4 m-auto space-y-4 text-center max-[400px]">
<h1 class="text-4xl text-slate-800 dark:text-neutral-200">
No permission
</h1>
<p class="text-base text-slate-500 dark:text-neutral-400">
The page you're trying access has restricted access.
Please refer to your system administrator
</p>
<div class="flex items-center justify-center text-center">
<div class="w-[300px]">
<div class="w-[300px]">
<img src="../../../icons/403.svg" alt="404">
</div>
</div>
</div>
<NButton type="primary" @click="goHome">
Go to Home
</NButton>
</div>
</div>
</template>
================================================
FILE: src/views/exception/404/index.vue
================================================
<script lang="ts" setup>
import { NButton } from 'naive-ui'
import { useRouter } from 'vue-router'
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<template>
<div class="flex h-full">
<div class="px-4 m-auto space-y-4 text-center max-[400px]">
<h1 class="text-4xl text-slate-800 dark:text-neutral-200">
Sorry, page not found!
</h1>
<p class="text-base text-slate-500 dark:text-neutral-400">
Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.
</p>
<div class="flex items-center justify-center text-center">
<div class="w-[300px]">
<img src="../../../icons/404.svg" alt="404">
</div>
</div>
<NButton type="primary" @click="goHome">
Go to Home
</NButton>
</div>
</div>
</template>
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
],
theme: {
extend: {
animation: {
blink: 'blink 1.2s infinite steps(1, start)',
},
keyframes: {
blink: {
'0%, 100%': { 'background-color': 'currentColor' },
'50%': { 'background-color': 'transparent' },
},
},
},
},
plugins: [],
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
},
"types": ["vite/client", "node", "naive-ui/volar"]
},
"exclude": ["node_modules", "dist", "service"]
}
================================================
FILE: vite.config.ts
================================================
import path from 'path'
import type { PluginOption } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
function setupPlugins(env: ImportMetaEnv): PluginOption[] {
return [
vue(),
env.VITE_GLOB_APP_PWA === 'true' && VitePWA({
injectRegister: 'auto',
manifest: {
name: 'chatGPT',
short_name: 'chatGPT',
icons: [
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
],
},
}),
]
}
export default defineConfig((env) => {
const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv
return {
resolve: {
alias: {
'@': path.resolve(process.cwd(), 'src'),
},
},
plugins: setupPlugins(viteEnv),
server: {
host: '0.0.0.0',
port: 1002,
open: false,
proxy: {
'/api': {
target: viteEnv.VITE_APP_API_BASE_URL,
changeOrigin: true, // 允许跨域
rewrite: path => path.replace('/api/', '/'),
},
},
},
build: {
reportCompressedSize: false,
sourcemap: false,
commonjsOptions: {
ignoreTryCatch: false,
},
},
}
})
gitextract_deg4t8zk/ ├── .commitlintrc.json ├── .dockerignore ├── .editorconfig ├── .eslintrc.cjs ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── build_docker.yml │ └── ci.yml ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .npmrc ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── README.md ├── config/ │ ├── index.ts │ └── proxy.ts ├── docker-compose/ │ ├── docker-compose.yml │ └── nginx/ │ ├── add_user.sh │ ├── auth/ │ │ └── .htpasswd │ ├── nginx.conf │ └── remove_user.sh ├── index.html ├── license ├── package.json ├── postcss.config.js ├── service/ │ ├── Dockerfile │ ├── api_model.py │ ├── chatgpt_wapper.py │ ├── entrypoint.sh │ ├── errors.py │ ├── main.py │ ├── message_store.py │ ├── requirements.txt │ ├── tools/ │ │ ├── __init__.py │ │ ├── local-whisper/ │ │ │ ├── __init__.py │ │ │ └── linux/ │ │ │ └── init.sh │ │ └── openai_token_control.py │ └── whisper_wapper.py ├── src/ │ ├── App.vue │ ├── api/ │ │ └── index.ts │ ├── components/ │ │ ├── common/ │ │ │ ├── HoverButton/ │ │ │ │ ├── Button.vue │ │ │ │ └── index.vue │ │ │ ├── NaiveProvider/ │ │ │ │ └── index.vue │ │ │ ├── Setting/ │ │ │ │ ├── About.vue │ │ │ │ ├── Advance.vue │ │ │ │ ├── General.vue │ │ │ │ └── index.vue │ │ │ ├── SvgIcon/ │ │ │ │ └── index.vue │ │ │ ├── UserAvatar/ │ │ │ │ └── index.vue │ │ │ └── index.ts │ │ └── custom/ │ │ ├── GithubSite.vue │ │ └── index.ts │ ├── hooks/ │ │ ├── useBasicLayout.ts │ │ ├── useIconRender.ts │ │ ├── useLanguage.ts │ │ └── useTheme.ts │ ├── locales/ │ │ ├── en-US.ts │ │ ├── index.ts │ │ ├── ja-JP.ts │ │ └── zh-CN.ts │ ├── main.ts │ ├── plugins/ │ │ ├── assets.ts │ │ ├── index.ts │ │ └── scrollbarStyle.ts │ ├── router/ │ │ └── index.ts │ ├── store/ │ │ ├── index.ts │ │ └── modules/ │ │ ├── app/ │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── chat/ │ │ │ ├── helper.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── user/ │ │ ├── helper.ts │ │ └── index.ts │ ├── styles/ │ │ ├── global.less │ │ └── lib/ │ │ ├── github-markdown.less │ │ ├── highlight.less │ │ └── tailwind.css │ ├── typings/ │ │ ├── chat.d.ts │ │ ├── env.d.ts │ │ └── global.d.ts │ ├── utils/ │ │ ├── crypto/ │ │ │ └── index.ts │ │ ├── format/ │ │ │ └── index.ts │ │ ├── is/ │ │ │ └── index.ts │ │ ├── request/ │ │ │ ├── axios.ts │ │ │ └── index.ts │ │ └── storage/ │ │ ├── index.ts │ │ └── local.ts │ └── views/ │ ├── chat/ │ │ ├── components/ │ │ │ ├── Message/ │ │ │ │ ├── Avatar.vue │ │ │ │ ├── Text.vue │ │ │ │ ├── index.vue │ │ │ │ └── style.less │ │ │ └── index.ts │ │ ├── hooks/ │ │ │ ├── useChat.ts │ │ │ ├── useCopyCode.ts │ │ │ └── useScroll.ts │ │ ├── index.vue │ │ └── layout/ │ │ ├── Layout.vue │ │ ├── header/ │ │ │ └── index.vue │ │ ├── index.ts │ │ └── sider/ │ │ ├── Footer.vue │ │ ├── List.vue │ │ └── index.vue │ └── exception/ │ ├── 403/ │ │ └── index.vue │ └── 404/ │ └── index.vue ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts
SYMBOL INDEX (127 symbols across 36 files)
FILE: config/proxy.ts
function createViteProxy (line 3) | function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) {
FILE: service/api_model.py
class ApiModel (line 1) | class ApiModel:
method get_api_model_name (line 14) | def get_api_model_name(cls, api_model_name: str, default_api_model_nam...
method get_max_token (line 18) | def get_max_token(cls, api_model_name: str, default_max_token: int) ->...
FILE: service/chatgpt_wapper.py
function process (line 17) | async def process(prompt, options, memory_count, top_p, message_store, t...
function _moderation_create (line 129) | def _moderation_create(params):
function _moderation_create_async (line 133) | async def _moderation_create_async(params):
function _chat_completions_create (line 148) | def _chat_completions_create(params):
function _chat_completions_create_async (line 152) | async def _chat_completions_create_async(params):
FILE: service/errors.py
class Errors (line 4) | class Errors(Enum):
FILE: service/main.py
function config (line 46) | async def config():
function chat_process (line 59) | async def chat_process(request_data: dict):
function audio_chat_process (line 83) | async def audio_chat_process(audio: UploadFile = File(...)):
function init_config (line 98) | def init_config():
FILE: service/message_store.py
class MessageStore (line 4) | class MessageStore:
method __init__ (line 5) | def __init__(self, db_path, table_name, max_size=100000):
method set (line 10) | def set(self, key, value):
method get_from_key (line 15) | def get_from_key(self, key):
method _delete_oldest (line 22) | def _delete_oldest(self):
FILE: service/tools/openai_token_control.py
function discard_overlimit_messages (line 6) | def discard_overlimit_messages(messages: list, model: str, max_token: in...
function num_tokens_from_string (line 34) | def num_tokens_from_string(string: str, encoding_name: str) -> int:
function num_tokens_from_messages (line 41) | def num_tokens_from_messages(messages, model="gpt-3.5-turbo"):
FILE: service/whisper_wapper.py
function upload_file_to_file_obj (line 16) | def upload_file_to_file_obj(upload_file: UploadFile, file_obj: Optional[...
function process_audio_api (line 25) | async def process_audio_api(audio, timeout, model="whisper-1"):
function _create (line 58) | def _create(params):
function process_audio_local (line 62) | async def process_audio_local(audio, audio_tmp_path, model_path, local_w...
function _local_create (line 115) | def _local_create(params):
function _create_async (line 137) | async def _create_async(params, func):
FILE: src/api/index.ts
function fetchChatConfig (line 4) | function fetchChatConfig<T = any>() {
function fetchChatAPIProcess (line 10) | function fetchChatAPIProcess<T = any>(
function fetchAudioChatAPIProcess (line 33) | function fetchAudioChatAPIProcess<T = any>(
FILE: src/hooks/useBasicLayout.ts
function useBasicLayout (line 3) | function useBasicLayout() {
FILE: src/hooks/useIconRender.ts
type IconConfig (line 5) | interface IconConfig {
type IconStyle (line 11) | interface IconStyle {
FILE: src/hooks/useLanguage.ts
function useLanguage (line 6) | function useLanguage() {
FILE: src/hooks/useTheme.ts
function useTheme (line 6) | function useTheme() {
FILE: src/locales/index.ts
function t (line 24) | function t(key: string) {
function setLocale (line 28) | function setLocale(locale: Language) {
function setupI18n (line 32) | function setupI18n(app: App) {
FILE: src/main.ts
function bootstrap (line 8) | async function bootstrap() {
FILE: src/plugins/assets.ts
function naiveStyleOverride (line 8) | function naiveStyleOverride() {
function setupAssets (line 14) | function setupAssets() {
FILE: src/router/index.ts
function setupRouter (line 46) | async function setupRouter(app: App) {
FILE: src/store/index.ts
function setupStore (line 6) | function setupStore(app: App) {
FILE: src/store/modules/app/helper.ts
constant LOCAL_NAME (line 3) | const LOCAL_NAME = 'appSetting'
constant ZH_CN (line 4) | const ZH_CN = 'zh-CN'
constant EN_US (line 5) | const EN_US = 'en-US'
constant JA_JP (line 6) | const JA_JP = 'ja-JP'
type Theme (line 8) | type Theme = 'light' | 'dark' | 'auto'
type Language (line 10) | type Language = typeof ZH_CN | typeof EN_US | typeof JA_JP
type focusTextarea (line 12) | type focusTextarea = true
type AppState (line 14) | interface AppState {
function defaultSetting (line 21) | function defaultSetting(): AppState {
function getLocalSetting (line 39) | function getLocalSetting(): AppState {
function setLocalSetting (line 44) | function setLocalSetting(setting: AppState): void {
FILE: src/store/modules/app/index.ts
method setSiderCollapsed (line 9) | setSiderCollapsed(collapsed: boolean) {
method setTheme (line 14) | setTheme(theme: Theme) {
method setLanguage (line 19) | setLanguage(language: Language) {
method setFocusTextarea (line 26) | setFocusTextarea() {
method recordState (line 31) | recordState() {
function useAppStoreWithOut (line 37) | function useAppStoreWithOut() {
FILE: src/store/modules/chat/helper.ts
constant LOCAL_NAME (line 3) | const LOCAL_NAME = 'chatStorage'
function defaultState (line 5) | function defaultState(): Chat.ChatState {
function getLocalState (line 10) | function getLocalState(): Chat.ChatState {
function setLocalState (line 15) | function setLocalState(state: Chat.ChatState) {
FILE: src/store/modules/chat/index.ts
method getChatHistoryByCurrentActive (line 9) | getChatHistoryByCurrentActive(state: Chat.ChatState) {
method getChatByUuid (line 16) | getChatByUuid(state: Chat.ChatState) {
method addHistory (line 26) | addHistory(history: Chat.History, chatData: Chat.Chat[] = []) {
method updateHistory (line 33) | updateHistory(uuid: number, edit: Partial<Chat.History>) {
method deleteHistory (line 41) | async deleteHistory(index: number) {
method setActive (line 73) | async setActive(uuid: number) {
method getChatByUuidAndIndex (line 78) | getChatByUuidAndIndex(uuid: number, index: number) {
method addChatByUuid (line 90) | addChatByUuid(uuid: number, chat: Chat.Chat, newChatText: string) {
method updateChatByUuid (line 116) | updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) {
method updateChatSomeByUuid (line 132) | updateChatSomeByUuid(uuid: number, index: number, chat: Partial<Chat.Cha...
method deleteChatByUuid (line 148) | deleteChatByUuid(uuid: number, index: number) {
method clearChatByUuid (line 164) | clearChatByUuid(uuid: number, chat_title: string) {
method reloadRoute (line 181) | async reloadRoute(uuid?: number) {
method recordState (line 186) | recordState() {
FILE: src/store/modules/user/helper.ts
constant LOCAL_NAME (line 3) | const LOCAL_NAME = 'userStorage'
type UserInfo (line 5) | interface UserInfo {
type UserState (line 13) | interface UserState {
function defaultSetting (line 17) | function defaultSetting(): UserState {
function getLocalState (line 30) | function getLocalState(): UserState {
function setLocalState (line 35) | function setLocalState(setting: UserState): void {
FILE: src/store/modules/user/index.ts
method updateUserInfo (line 8) | updateUserInfo(userInfo: Partial<UserInfo>) {
method resetUserInfo (line 13) | resetUserInfo() {
method recordState (line 18) | recordState() {
FILE: src/typings/chat.d.ts
type Chat (line 3) | interface Chat {
type History (line 13) | interface History {
type ChatState (line 19) | interface ChatState {
type ConversationRequest (line 25) | interface ConversationRequest {
type ConversationResponse (line 30) | interface ConversationResponse {
FILE: src/typings/env.d.ts
type ImportMetaEnv (line 3) | interface ImportMetaEnv {
FILE: src/typings/global.d.ts
type Window (line 1) | interface Window {
FILE: src/utils/crypto/index.ts
function enCrypto (line 5) | function enCrypto(data: any) {
function deCrypto (line 10) | function deCrypto(data: string) {
FILE: src/utils/format/index.ts
function encodeHTML (line 5) | function encodeHTML(source: string) {
function includeCode (line 18) | function includeCode(text: string | null | undefined) {
function copyText (line 27) | function copyText(options: { text: string; origin?: boolean }) {
FILE: src/utils/is/index.ts
function isNumber (line 1) | function isNumber<T extends number>(value: T | unknown): value is number {
function isString (line 5) | function isString<T extends string>(value: T | unknown): value is string {
function isBoolean (line 9) | function isBoolean<T extends boolean>(value: T | unknown): value is bool...
function isNull (line 13) | function isNull<T extends null>(value: T | unknown): value is null {
function isUndefined (line 17) | function isUndefined<T extends undefined>(value: T | unknown): value is ...
function isObject (line 21) | function isObject<T extends object>(value: T | unknown): value is object {
function isArray (line 25) | function isArray<T extends any[]>(value: T | unknown): value is T {
function isFunction (line 29) | function isFunction<T extends (...args: any[]) => any | void | never>(va...
function isDate (line 33) | function isDate<T extends Date>(value: T | unknown): value is T {
function isRegExp (line 37) | function isRegExp<T extends RegExp>(value: T | unknown): value is T {
function isPromise (line 41) | function isPromise<T extends Promise<any>>(value: T | unknown): value is...
function isSet (line 45) | function isSet<T extends Set<any>>(value: T | unknown): value is T {
function isMap (line 49) | function isMap<T extends Map<any, any>>(value: T | unknown): value is T {
function isFile (line 53) | function isFile<T extends File>(value: T | unknown): value is T {
FILE: src/utils/request/index.ts
type HttpOption (line 4) | interface HttpOption {
type Response (line 15) | interface Response<T = any> {
function http (line 21) | function http<T = any>(
function get (line 47) | function get<T = any>(
function post (line 61) | function post<T = any>(
FILE: src/utils/storage/local.ts
type StorageData (line 3) | interface StorageData<T = any> {
function createLocalStorage (line 8) | function createLocalStorage(options?: { expire?: number | null; crypto?:...
FILE: src/views/chat/hooks/useChat.ts
function useChat (line 3) | function useChat() {
FILE: src/views/chat/hooks/useCopyCode.ts
function useCopyCode (line 4) | function useCopyCode() {
FILE: src/views/chat/hooks/useScroll.ts
type ScrollElement (line 4) | type ScrollElement = HTMLDivElement | null
type ScrollReturn (line 6) | interface ScrollReturn {
function useScroll (line 13) | function useScroll(): ScrollReturn {
FILE: vite.config.ts
function setupPlugins (line 7) | function setupPlugins(env: ImportMetaEnv): PluginOption[] {
Condensed preview — 109 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (197K chars).
[
{
"path": ".commitlintrc.json",
"chars": 53,
"preview": "{\n \"extends\": [\"@commitlint/config-conventional\"]\n}\n"
},
{
"path": ".dockerignore",
"chars": 32,
"preview": "node_modules\nDockerfile\n.*\n*/.*\n"
},
{
"path": ".editorconfig",
"chars": 198,
"preview": "# Editor configuration, see http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = tab\nindent_size = 2"
},
{
"path": ".eslintrc.cjs",
"chars": 58,
"preview": "module.exports = {\n root: true,\n extends: ['@antfu'],\n}\n"
},
{
"path": ".gitattributes",
"chars": 306,
"preview": "\"*.vue\" eol=lf\n\"*.js\" eol=lf\n\"*.ts\" eol=lf\n\"*.jsx\" eol=lf\n\"*.tsx\" eol=lf\n\"*.cjs\" eol=lf\n\"*.cts\" e"
},
{
"path": ".github/workflows/build_docker.yml",
"chars": 1139,
"preview": "name: build_docker\n\non:\n push:\n branches: [main]\n release:\n types: [created] # 表示在创建新的 Release 时触发\n\njobs:\n buil"
},
{
"path": ".github/workflows/ci.yml",
"chars": 753,
"preview": "name: CI\n\non:\n push:\n branches:\n - main\n\n pull_request:\n branches:\n - main\n\njobs:\n lint:\n runs-on:"
},
{
"path": ".gitignore",
"chars": 414,
"preview": "# Logs\nlogs\n*.log\nservice/log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\n#backend\n"
},
{
"path": ".husky/commit-msg",
"chars": 84,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx --no -- commitlint --edit \n"
},
{
"path": ".husky/pre-commit",
"chars": 69,
"preview": "#!/usr/bin/env sh\n. \"$(dirname -- \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
},
{
"path": ".npmrc",
"chars": 31,
"preview": "strict-peer-dependencies=false\n"
},
{
"path": ".vscode/extensions.json",
"chars": 65,
"preview": "{\n \"recommendations\": [\"Vue.volar\", \"dbaeumer.vscode-eslint\"]\n}\n"
},
{
"path": ".vscode/settings.json",
"chars": 1052,
"preview": "{\n \"prettier.enable\": false,\n \"editor.formatOnSave\": false,\n \"editor.codeActionsOnSave\": {\n \"source.fixAll.eslint\""
},
{
"path": "CONTRIBUTING.md",
"chars": 1198,
"preview": "# 贡献指南\n感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。\n\n## 语义化版本\n该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的"
},
{
"path": "Dockerfile",
"chars": 257,
"preview": "# build front-end\nFROM node:lts-alpine AS builder\n\nCOPY ./ /app\nWORKDIR /app\n\nRUN apk add --no-cache git \\\n && npm in"
},
{
"path": "README.md",
"chars": 8578,
"preview": "# ChatGPT Web\n\n### 使用界面\n\n\n\n\n### 性格微调功能\n "
},
{
"path": "docker-compose/docker-compose.yml",
"chars": 1112,
"preview": "version: '3'\n\nservices:\n app:\n # 根据自己的系统选择x86_64还是aarch64\n image: wenjing95/chatgpt-web-backend:x86_64\n # imag"
},
{
"path": "docker-compose/nginx/add_user.sh",
"chars": 445,
"preview": "#!/bin/bash\n\nread -p \"Enter your desired username: \" username\nread -s -p \"Enter your desired password: \" password\nhtpass"
},
{
"path": "docker-compose/nginx/auth/.htpasswd",
"chars": 0,
"preview": ""
},
{
"path": "docker-compose/nginx/nginx.conf",
"chars": 638,
"preview": "\n\tserver {\n\t\t\tlisten 80;\n\t\t\tserver_name localhost;\n\t\t\tcharset utf-8;\n\t\t\terror_page 500 502 503 504 /50x.html;\n\n\t\t\taut"
},
{
"path": "docker-compose/nginx/remove_user.sh",
"chars": 495,
"preview": "#!/bin/bash\n\nhtpasswd_file=\"./auth/.htpasswd\"\n\nif [ ! -f \"$htpasswd_file\" ]; then\n echo \"No .htpasswd file found\"\n "
},
{
"path": "index.html",
"chars": 1780,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-cmn-Hans\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<link rel=\"icon\" type=\"image/svg+xml\" href=\"/f"
},
{
"path": "license",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2023 WenJing\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "package.json",
"chars": 1923,
"preview": "{\n \"name\": \"chatgpt-web\",\n \"version\": \"2.10.9\",\n \"private\": false,\n \"description\": \"ChatGPT Web\",\n \"author\": \"WenJi"
},
{
"path": "postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "service/Dockerfile",
"chars": 834,
"preview": "# 使用 Python 3.10 作为基础镜像\nFROM python:3.10\n\n# 设置工作目录\nWORKDIR /app\n\n# 复制项目文件到容器中\nCOPY . .\n\n# ==rust是tiktoken依赖的环境==\nRUN cur"
},
{
"path": "service/api_model.py",
"chars": 879,
"preview": "class ApiModel:\n KNOWN_API_MODEL_NAMES = [\"gpt-3.5-turbo\",\n \"gpt-4\",\n "
},
{
"path": "service/chatgpt_wapper.py",
"chars": 4936,
"preview": "import random\nimport time\n\nimport openai # for OpenAI API calls\nimport traceback\nimport json\nimport asyncio\nfrom loguru"
},
{
"path": "service/entrypoint.sh",
"chars": 502,
"preview": "#!/bin/bash\n\n# 将环境变量传递给 Python 脚本\nexport OPENAI_API_KEY=\"$OPENAI_API_KEY\"\nexport OPENAI_TIMEOUT_MS=\"$OPENAI_TIMEOUT_MS\"\n"
},
{
"path": "service/errors.py",
"chars": 698,
"preview": "from enum import Enum\n\n\nclass Errors(Enum):\n SOMETHING_WRONG = \"ChatGptWebServerError:SomethingWrong\"\n SOMETHING_W"
},
{
"path": "service/main.py",
"chars": 8374,
"preview": "import openai\nimport os\nfrom os.path import abspath, dirname\nfrom loguru import logger\nfrom chatgpt_wapper import proces"
},
{
"path": "service/message_store.py",
"chars": 872,
"preview": "import time\nfrom tinydb import TinyDB, Query\n\nclass MessageStore:\n def __init__(self, db_path, table_name, max_size=1"
},
{
"path": "service/tools/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "service/tools/local-whisper/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "service/tools/local-whisper/linux/init.sh",
"chars": 197,
"preview": "sudo apt update\nsudo apt install build-essential -y\n\nwget https://github.com/ggerganov/whisper.cpp/archive/refs/heads/ma"
},
{
"path": "service/tools/openai_token_control.py",
"chars": 2801,
"preview": "import tiktoken\n\nMAX_RANGES = 10\n\n\ndef discard_overlimit_messages(messages: list, model: str, max_token: int) -> list:\n "
},
{
"path": "service/whisper_wapper.py",
"chars": 4049,
"preview": "import openai\nimport subprocess\nfrom backoff import on_exception, expo\nfrom io import BytesIO\nfrom typing import Optiona"
},
{
"path": "src/App.vue",
"chars": 534,
"preview": "<script setup lang=\"ts\">\nimport { NConfigProvider } from 'naive-ui'\nimport { NaiveProvider } from '@/components/common'\n"
},
{
"path": "src/api/index.ts",
"chars": 1126,
"preview": "import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'\nimport { post } from '@/utils/request'\n\nexport funct"
},
{
"path": "src/components/common/HoverButton/Button.vue",
"chars": 365,
"preview": "<script setup lang='ts'>\ninterface Emit {\n (e: 'click'): void\n}\n\nconst emit = defineEmits<Emit>()\n\nfunction handleClick"
},
{
"path": "src/components/common/HoverButton/index.vue",
"chars": 884,
"preview": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport type { PopoverPlacement } from 'naive-ui'\nimport { NToolt"
},
{
"path": "src/components/common/NaiveProvider/index.vue",
"chars": 885,
"preview": "<script setup lang=\"ts\">\nimport { defineComponent, h } from 'vue'\nimport {\n NDialogProvider,\n NLoadingBarProvider,\n N"
},
{
"path": "src/components/common/Setting/About.vue",
"chars": 1515,
"preview": "<script setup lang='ts'>\nimport { onMounted, ref } from 'vue'\nimport { NSpin } from 'naive-ui'\nimport { fetchChatConfig "
},
{
"path": "src/components/common/Setting/Advance.vue",
"chars": 3059,
"preview": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport { NRadioButton, NRadioGroup, NSlider, useMessage } f"
},
{
"path": "src/components/common/Setting/General.vue",
"chars": 5146,
"preview": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport { NButton, NImage, NInput, useMessage } from 'naive-"
},
{
"path": "src/components/common/Setting/index.vue",
"chars": 1887,
"preview": "<script setup lang='ts'>\nimport { computed, ref } from 'vue'\nimport { NModal, NTabPane, NTabs } from 'naive-ui'\nimport G"
},
{
"path": "src/components/common/SvgIcon/index.vue",
"chars": 419,
"preview": "<script setup lang='ts'>\nimport { computed, useAttrs } from 'vue'\nimport { Icon } from '@iconify/vue'\n\ninterface Props {"
},
{
"path": "src/components/common/UserAvatar/index.vue",
"chars": 1366,
"preview": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport { NAvatar } from 'naive-ui'\nimport { useUserStore } from "
},
{
"path": "src/components/common/index.ts",
"chars": 305,
"preview": "import HoverButton from './HoverButton/index.vue'\nimport NaiveProvider from './NaiveProvider/index.vue'\nimport SvgIcon f"
},
{
"path": "src/components/custom/GithubSite.vue",
"chars": 366,
"preview": "<template>\n <div class=\"text-neutral-400\">\n <span>Forked and modified from </span>\n <a href=\"https://github.com/C"
},
{
"path": "src/components/custom/index.ts",
"chars": 65,
"preview": "import GithubSite from './GithubSite.vue'\n\nexport { GithubSite }\n"
},
{
"path": "src/hooks/useBasicLayout.ts",
"chars": 231,
"preview": "import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'\n\nexport function useBasicLayout() {\n const breakpoin"
},
{
"path": "src/hooks/useIconRender.ts",
"chars": 650,
"preview": "import { h } from 'vue'\nimport { SvgIcon } from '@/components/common'\n\nexport const useIconRender = () => {\n interface "
},
{
"path": "src/hooks/useLanguage.ts",
"chars": 586,
"preview": "import { computed } from 'vue'\nimport { enUS, jaJP, zhCN } from 'naive-ui'\nimport { useAppStore } from '@/store'\nimport "
},
{
"path": "src/hooks/useTheme.ts",
"chars": 942,
"preview": "import type { GlobalThemeOverrides } from 'naive-ui'\nimport { computed, watch } from 'vue'\nimport { darkTheme, useOsThem"
},
{
"path": "src/locales/en-US.ts",
"chars": 3627,
"preview": "export default {\n common: {\n delete: 'Delete',\n save: 'Save',\n reset: 'Reset',\n yes: 'Yes',\n no: 'No',\n "
},
{
"path": "src/locales/index.ts",
"chars": 750,
"preview": "import type { App } from 'vue'\nimport { createI18n } from 'vue-i18n'\nimport en from './en-US'\nimport cn from './zh-CN'\ni"
},
{
"path": "src/locales/ja-JP.ts",
"chars": 2699,
"preview": "export default {\n common: {\n delete: '削除',\n save: '保存する',\n reset: 'リセット',\n yes: 'はい',\n no: 'いいえ',\n no"
},
{
"path": "src/locales/zh-CN.ts",
"chars": 2228,
"preview": "export default {\n common: {\n delete: '删除',\n save: '保存',\n reset: '重置',\n yes: '是',\n no: '否',\n noData: '"
},
{
"path": "src/main.ts",
"chars": 434,
"preview": "import { createApp } from 'vue'\nimport App from './App.vue'\nimport { setupI18n } from './locales'\nimport { setupAssets, "
},
{
"path": "src/plugins/assets.ts",
"chars": 447,
"preview": "import 'katex/dist/katex.min.css'\nimport '@/styles/lib/tailwind.css'\nimport '@/styles/lib/highlight.less'\nimport '@/styl"
},
{
"path": "src/plugins/index.ts",
"chars": 131,
"preview": "import setupAssets from './assets'\nimport setupScrollbarStyle from './scrollbarStyle'\n\nexport { setupAssets, setupScroll"
},
{
"path": "src/plugins/scrollbarStyle.ts",
"chars": 919,
"preview": "import { darkTheme, lightTheme } from 'naive-ui'\n\nconst setupScrollbarStyle = () => {\n const style = document.createEle"
},
{
"path": "src/router/index.ts",
"chars": 1001,
"preview": "import type { App } from 'vue'\nimport type { RouteRecordRaw } from 'vue-router'\nimport { createRouter, createWebHashHist"
},
{
"path": "src/store/index.ts",
"chars": 189,
"preview": "import type { App } from 'vue'\nimport { createPinia } from 'pinia'\n\nexport const store = createPinia()\n\nexport function "
},
{
"path": "src/store/modules/app/helper.ts",
"chars": 1159,
"preview": "import { ss } from '@/utils/storage'\n\nconst LOCAL_NAME = 'appSetting'\nconst ZH_CN = 'zh-CN'\nconst EN_US = 'en-US'\nconst "
},
{
"path": "src/store/modules/app/index.ts",
"chars": 884,
"preview": "import { defineStore } from 'pinia'\nimport type { AppState, Language, Theme } from './helper'\nimport { getLocalSetting, "
},
{
"path": "src/store/modules/chat/helper.ts",
"chars": 469,
"preview": "import { ss } from '@/utils/storage'\n\nconst LOCAL_NAME = 'chatStorage'\n\nexport function defaultState(): Chat.ChatState {"
},
{
"path": "src/store/modules/chat/index.ts",
"chars": 5386,
"preview": "import { defineStore } from 'pinia'\nimport { getLocalState, setLocalState } from './helper'\nimport { router } from '@/ro"
},
{
"path": "src/store/modules/index.ts",
"chars": 68,
"preview": "export * from './app'\nexport * from './chat'\nexport * from './user'\n"
},
{
"path": "src/store/modules/user/helper.ts",
"chars": 770,
"preview": "import { ss } from '@/utils/storage'\n\nconst LOCAL_NAME = 'userStorage'\n\nexport interface UserInfo {\n avatar: string\n n"
},
{
"path": "src/store/modules/user/index.ts",
"chars": 590,
"preview": "import { defineStore } from 'pinia'\nimport type { UserInfo, UserState } from './helper'\nimport { defaultSetting, getLoca"
},
{
"path": "src/styles/global.less",
"chars": 134,
"preview": "html,\nbody,\n#app {\n\theight: 100%;\n}\n\nbody {\n\tpadding-bottom: env(safe-area-inset-bottom);\n}\n\n.clickable-element {\n\tcurso"
},
{
"path": "src/styles/lib/github-markdown.less",
"chars": 25782,
"preview": "html.dark {\n .markdown-body {\n color-scheme: dark;\n --color-prettylights-syntax-comment: #8b949e;\n --color-pre"
},
{
"path": "src/styles/lib/highlight.less",
"chars": 2603,
"preview": "html.dark {\n\tpre code.hljs {\n\t\tdisplay: block;\n\t\toverflow-x: auto;\n\t\tpadding: 1em\n\t}\n\n\tcode.hljs {\n\t\tpadding: 3px 5px\n\t}"
},
{
"path": "src/styles/lib/tailwind.css",
"chars": 59,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
},
{
"path": "src/typings/chat.d.ts",
"chars": 931,
"preview": "declare namespace Chat {\n\n\tinterface Chat {\n\t\tdateTime: string\n\t\ttext: string\n\t\tinversion?: boolean\n\t\terror?: boolean\n\t\t"
},
{
"path": "src/typings/env.d.ts",
"chars": 268,
"preview": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n\treadonly VITE_GLOB_API_URL: string;\n\treadonly VITE_GLO"
},
{
"path": "src/typings/global.d.ts",
"chars": 247,
"preview": "interface Window {\n $loadingBar?: import('naive-ui').LoadingBarProviderInst;\n $dialog?: import('naive-ui').DialogProvi"
},
{
"path": "src/utils/crypto/index.ts",
"chars": 414,
"preview": "import CryptoJS from 'crypto-js'\n\nconst CryptoSecret = '__CRYPTO_SECRET__'\n\nexport function enCrypto(data: any) {\n cons"
},
{
"path": "src/utils/format/index.ts",
"chars": 992,
"preview": "/**\n * 转义 HTML 字符\n * @param source\n */\nexport function encodeHTML(source: string) {\n return source\n .replace(/&/g, '"
},
{
"path": "src/utils/is/index.ts",
"chars": 2149,
"preview": "export function isNumber<T extends number>(value: T | unknown): value is number {\n return Object.prototype.toString.cal"
},
{
"path": "src/utils/request/axios.ts",
"chars": 541,
"preview": "import axios, { type AxiosResponse } from 'axios'\n\nconst service = axios.create({\n baseURL: import.meta.env.VITE_GLOB_A"
},
{
"path": "src/utils/request/index.ts",
"chars": 1899,
"preview": "import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'\nimport request from './axios'\n\nexport"
},
{
"path": "src/utils/storage/index.ts",
"chars": 24,
"preview": "export * from './local'\n"
},
{
"path": "src/utils/storage/local.ts",
"chars": 1486,
"preview": "import { deCrypto, enCrypto } from '../crypto'\n\ninterface StorageData<T = any> {\n data: T\n expire: number | null\n}\n\nex"
},
{
"path": "src/views/chat/components/Message/Avatar.vue",
"chars": 1672,
"preview": "<script lang=\"ts\" setup>\nimport { computed } from 'vue'\nimport { NAvatar } from 'naive-ui'\nimport { useUserStore } from "
},
{
"path": "src/views/chat/components/Message/Text.vue",
"chars": 2486,
"preview": "<script lang=\"ts\" setup>\nimport { computed, ref } from 'vue'\nimport MarkdownIt from 'markdown-it'\nimport mdKatex from '@"
},
{
"path": "src/views/chat/components/Message/index.vue",
"chars": 3510,
"preview": "<script setup lang='ts'>\nimport { computed, ref } from 'vue'\nimport { NDropdown } from 'naive-ui'\nimport AvatarComponent"
},
{
"path": "src/views/chat/components/Message/style.less",
"chars": 879,
"preview": ".markdown-body {\n\tbackground-color: transparent;\n\tfont-size: 14px;\n\n\tp {\n\t\twhite-space: pre-wrap;\n\t}\n\n\tol {\n\t\tlist-style"
},
{
"path": "src/views/chat/components/index.ts",
"chars": 62,
"preview": "import Message from './Message/index.vue'\n\nexport { Message }\n"
},
{
"path": "src/views/chat/hooks/useChat.ts",
"chars": 732,
"preview": "import { useChatStore } from '@/store'\n\nexport function useChat() {\n const chatStore = useChatStore()\n\n const getChatB"
},
{
"path": "src/views/chat/hooks/useCopyCode.ts",
"chars": 801,
"preview": "import { onMounted, onUpdated } from 'vue'\nimport { copyText } from '@/utils/format'\n\nexport function useCopyCode() {\n "
},
{
"path": "src/views/chat/hooks/useScroll.ts",
"chars": 1138,
"preview": "import type { Ref } from 'vue'\nimport { nextTick, ref } from 'vue'\n\ntype ScrollElement = HTMLDivElement | null\n\ninterfac"
},
{
"path": "src/views/chat/index.vue",
"chars": 20134,
"preview": "<script setup lang='ts'>\nimport type { Ref } from 'vue'\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vu"
},
{
"path": "src/views/chat/layout/Layout.vue",
"chars": 1475,
"preview": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport { NLayout, NLayoutContent } from 'naive-ui'\nimport { useR"
},
{
"path": "src/views/chat/layout/header/index.vue",
"chars": 2348,
"preview": "<script lang=\"ts\" setup>\nimport { computed, defineAsyncComponent, nextTick, ref } from 'vue'\nimport { SvgIcon } from '@/"
},
{
"path": "src/views/chat/layout/index.ts",
"chars": 61,
"preview": "import ChatLayout from './Layout.vue'\n\nexport { ChatLayout }\n"
},
{
"path": "src/views/chat/layout/sider/Footer.vue",
"chars": 773,
"preview": "<script setup lang='ts'>\nimport { defineAsyncComponent, ref } from 'vue'\nimport { HoverButton, SvgIcon, UserAvatar } fro"
},
{
"path": "src/views/chat/layout/sider/List.vue",
"chars": 3652,
"preview": "<script setup lang='ts'>\nimport { computed } from 'vue'\nimport { NInput, NPopconfirm, NScrollbar } from 'naive-ui'\nimpor"
},
{
"path": "src/views/chat/layout/sider/index.vue",
"chars": 2127,
"preview": "<script setup lang='ts'>\nimport type { CSSProperties } from 'vue'\nimport { computed, watch } from 'vue'\nimport { NButton"
},
{
"path": "src/views/exception/403/index.vue",
"chars": 917,
"preview": "<script lang=\"ts\" setup>\nimport { NButton } from 'naive-ui'\nimport { useRouter } from 'vue-router'\n\nconst router = useRo"
},
{
"path": "src/views/exception/404/index.vue",
"chars": 888,
"preview": "<script lang=\"ts\" setup>\nimport { NButton } from 'naive-ui'\nimport { useRouter } from 'vue-router'\n\nconst router = useRo"
},
{
"path": "tailwind.config.js",
"chars": 470,
"preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n darkMode: 'class',\n content: [\n './index.html',\n "
},
{
"path": "tsconfig.json",
"chars": 589,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"module\": \"ESNext\",\n \"target\": \"ESNext\",\n \"lib\": [\"DOM\", \"ESNext\""
},
{
"path": "vite.config.ts",
"chars": 1316,
"preview": "import path from 'path'\nimport type { PluginOption } from 'vite'\nimport { defineConfig, loadEnv } from 'vite'\nimport vue"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the WenJing95/chatgpt-web GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 109 files (168.0 KB), approximately 52.3k tokens, and a symbol index with 127 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.