Full Code of WenJing95/chatgpt-web for AI

main ea19057bd9a2 cached
109 files
168.0 KB
52.3k tokens
127 symbols
1 requests
Download .txt
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

### 使用界面
![cover3](./docs/c3.png)
![cover](./docs/c1.png)
![cover2](./docs/c2.png)

### 性格微调功能
![cover4](./docs/c4.png)

### 文本审查功能(使用OpenAI官方接口)
![cover5](./docs/c5.png)

- [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(&quot;common.about_head&quot;)" />
        <p v-text="$t(&quot;common.about_body&quot;)" />
      </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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

/**
 * 判断是否为代码块
 * @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,
      },
    },
  }
})
Download .txt
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
Download .txt
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![cover3](./docs/c3.png)\n![cover](./docs/c1.png)\n![cover2](./docs/c2.png)\n\n### 性格微调功能\n![cover4]("
  },
  {
    "path": "config/index.ts",
    "chars": 24,
    "preview": "export * from './proxy'\n"
  },
  {
    "path": "config/proxy.ts",
    "chars": 376,
    "preview": "import type { ProxyOptions } from 'vite'\n\nexport function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) "
  },
  {
    "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.

Copied to clipboard!