├── .commitlintrc.json ├── .dockerignore ├── .editorconfig ├── .env ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── README.md ├── config ├── index.ts └── proxy.ts ├── docker-compose ├── docker-compose.yml ├── nginx │ └── nginx.conf └── readme.md ├── docs ├── chatglm.gif └── index.png ├── index.html ├── license ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.svg ├── pwa-192x192.png └── pwa-512x512.png ├── service ├── Dockerfile ├── entrypoint.sh ├── errors.py ├── gen_data.py ├── knowledge.py ├── knowledge │ └── test.txt ├── main.py ├── message_store.py └── requirements.txt ├── src ├── App.vue ├── api │ └── index.ts ├── assets │ └── recommend.json ├── components │ ├── common │ │ ├── HoverButton │ │ │ ├── Button.vue │ │ │ └── index.vue │ │ ├── NaiveProvider │ │ │ └── index.vue │ │ ├── PromptStore │ │ │ └── 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 ├── icons │ ├── 403.svg │ └── 404.svg ├── 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 │ │ ├── prompt │ │ ├── helper.ts │ │ └── index.ts │ │ └── user │ │ ├── helper.ts │ │ └── index.ts ├── styles │ ├── global.less │ └── lib │ │ ├── github-markdown.less │ │ ├── highlight.less │ │ └── tailwind.css ├── typings │ ├── chat.d.ts │ ├── env.d.ts │ └── global.d.ts ├── utils │ ├── crypto │ │ └── index.ts │ ├── format │ │ └── index.ts │ ├── 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 │ │ ├── useUsingContext.ts │ │ └── useUsingKnowledge.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 ├── start.sh ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Dockerfile 3 | .* 4 | */.* 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = tab 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # Glob API URL 2 | VITE_GLOB_API_URL=/api 3 | 4 | VITE_APP_API_BASE_URL=http://127.0.0.1:3002/ 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@antfu'], 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.cjs" eol=lf 7 | "*.cts" eol=lf 8 | "*.mjs" eol=lf 9 | "*.mts" eol=lf 10 | "*.json" eol=lf 11 | "*.html" eol=lf 12 | "*.css" eol=lf 13 | "*.less" eol=lf 14 | "*.scss" eol=lf 15 | "*.sass" eol=lf 16 | "*.styl" eol=lf 17 | "*.md" eol=lf 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | service/log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | #backend 12 | service/venv 13 | service/message_store.json 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | docker-compose/nginx/html 18 | 19 | node_modules 20 | .DS_Store 21 | dist 22 | dist-ssr 23 | coverage 24 | *.local 25 | 26 | /cypress/videos/ 27 | /cypress/screenshots/ 28 | 29 | # Editor directories and files 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/extensions.json 33 | .idea 34 | *.suo 35 | *.ntvs* 36 | *.njsproj 37 | *.sln 38 | *.sw? 39 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "eslint.validate": [ 8 | "javascript", 9 | "javascriptreact", 10 | "typescript", 11 | "typescriptreact", 12 | "vue", 13 | "html", 14 | "json", 15 | "jsonc", 16 | "json5", 17 | "yaml", 18 | "yml", 19 | "markdown" 20 | ], 21 | "cSpell.words": [ 22 | "antfu", 23 | "axios", 24 | "bumpp", 25 | "chatgpt", 26 | "chenzhaoyu", 27 | "commitlint", 28 | "davinci", 29 | "dockerhub", 30 | "esno", 31 | "GPTAPI", 32 | "highlightjs", 33 | "hljs", 34 | "iconify", 35 | "katex", 36 | "katexmath", 37 | "linkify", 38 | "logprobs", 39 | "mdhljs", 40 | "mila", 41 | "nodata", 42 | "OPENAI", 43 | "pinia", 44 | "Popconfirm", 45 | "rushstack", 46 | "Sider", 47 | "tailwindcss", 48 | "traptitech", 49 | "tsup", 50 | "Typecheck", 51 | "unplugin", 52 | "VITE", 53 | "vueuse", 54 | "Zhao" 55 | ], 56 | "i18n-ally.enabledParsers": [ 57 | "ts" 58 | ], 59 | "i18n-ally.sortKeys": true, 60 | "i18n-ally.keepFulfilled": true, 61 | "i18n-ally.localesPaths": [ 62 | "src/locales" 63 | ], 64 | "i18n-ally.keystyle": "nested" 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## V0.1.0 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 感谢你的宝贵时间。你的贡献将使这个项目变得更好!在提交贡献之前,请务必花点时间阅读下面的入门指南。 3 | 4 | ## 语义化版本 5 | 该项目遵循语义化版本。我们对重要的漏洞修复发布修订号,对新特性或不重要的变更发布次版本号,对重大且不兼容的变更发布主版本号。 6 | 7 | 每个重大更改都将记录在 `changelog` 中。 8 | 9 | ## 提交 Pull Request 10 | 1. Fork [此仓库](https://github.com/Chanzhaoyu/chatgpt-web),从 `main` 创建分支。新功能实现请发 pull request 到 `feature` 分支。其他更改发到 `main` 分支。 11 | 2. 使用 `npm install pnpm -g` 安装 `pnpm` 工具。 12 | 3. `vscode` 安装了 `Eslint` 插件,其它编辑器如 `webStorm` 打开了 `eslint` 功能。 13 | 4. 根目录下执行 `pnpm bootstrap`。 14 | 5. `/service/` 目录下执行 `pnpm install`。 15 | 6. 对代码库进行更改。如果适用的话,请确保进行了相应的测试。 16 | 7. 请在根目录下执行 `pnpm lint:fix` 进行代码格式检查。 17 | 8. 请在根目录下执行 `pnpm type-check` 进行类型检查。 18 | 9. 提交 git commit, 请同时遵守 [Commit 规范](#commit-指南) 19 | 10. 提交 `pull request`, 如果有对应的 `issue`,请进行[关联](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)。 20 | 21 | ## Commit 指南 22 | 23 | Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/): 24 | 25 | ```bash 26 | <类型>[可选 范围]: <描述> 27 | 28 | [可选 正文] 29 | 30 | [可选 脚注] 31 | ``` 32 | 33 | ### Commit 类型 34 | 35 | 以下是 commit 类型列表: 36 | 37 | - feat: 新特性或功能 38 | - fix: 缺陷修复 39 | - docs: 文档更新 40 | - style: 代码风格或者组件样式更新 41 | - refactor: 代码重构,不引入新功能和缺陷修复 42 | - perf: 性能优化 43 | - test: 单元测试 44 | - chore: 其他不修改 src 或测试文件的提交 45 | 46 | 47 | ## License 48 | 49 | [MIT](./license) 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build front-end 2 | FROM node:lts-alpine AS builder 3 | 4 | COPY ./ /app 5 | WORKDIR /app 6 | 7 | RUN apk add --no-cache git \ 8 | && npm install pnpm -g \ 9 | && pnpm install \ 10 | && pnpm run build \ 11 | && rm -rf /root/.npm /root/.pnpm-store /usr/local/share/.cache /tmp/* 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGLM Web 2 | 3 | ![cover](./docs/chatglm.gif) 4 | 5 | ## 介绍 6 | > 默认模型更改为ChatGLM2-6B 7 | 8 | 这是一个可以自己在本地部署的`ChatGLM`网页,使用`ChatGLM-6B`模型来实现接近`ChatGPT`的对话效果。 9 | 源代码Fork和修改于[Chanzhaoyu/chatgpt-web](https://github.com/Chanzhaoyu/chatgpt-web/) & [WenJing95/chatgpt-web](https://github.com/WenJing95/chatgpt-web/)& 开源模型[ChatGLM](https://github.com/THUDM/ChatGLM-6B) 10 | 11 | 与`ChatGPT`对比,`ChatGLM Web`有以下优势: 12 | 13 | 1. **独立部署**。`ChatGLM Web`只需要一个能运行`ChatGLM-6B`模型的服务器即可使用,可以使用自己微调的GLM模型。 14 | 2. **完全离线**。`ChatGLM Web`依赖于`ChatGLM-6B`模型,可以在离线环境或者内网中使用。 15 | 16 | ## 待实现路线 17 | [✗] 支持chatglm、llama等模型 18 | 19 | [✓] 追上原仓库的功能(权限控制、图片、消息导入导出、Prompt Store) 20 | 21 | [✗] 支持langchain的知识问答 22 | 23 | [✗] More... 24 | 25 | ## 快速部署 26 | 27 | 如果你不需要自己开发,只需要部署使用,可以直接跳到 ~~[使用最新版本docker镜像启动](#使用最新版本docker镜像启动)(待完善)~~ 28 | 29 | ## 开发环境搭建 30 | 31 | ### Node 32 | 33 | `node` 需要 `^16 || ^18` 版本(`node >= 14` 34 | 需要安装 [fetch polyfill](https://github.com/developit/unfetch#usage-as-a-polyfill) 35 | ),使用 [nvm](https://github.com/nvm-sh/nvm) 可管理本地多个 `node` 版本 36 | 37 | ```shell 38 | node -v 39 | ``` 40 | 41 | ### PNPM 42 | 43 | 如果你没有安装过 `pnpm` 44 | 45 | ```shell 46 | npm install pnpm -g 47 | ``` 48 | 49 | ### Python 50 | 51 | `python` 需要 `3.8` 以上版本,进入文件夹 `/service` 运行以下命令 52 | 53 | ```shell 54 | pip install --no-cache-dir -r requirements.txt 55 | ``` 56 | 57 | ## 开发环境启动项目 58 | 59 | ### 后端服务 60 | 61 | #### 硬件需求(参考自chatglm-6b官方仓库) 62 | 63 | | **量化等级** | **最低 GPU 显存**(推理) | **最低 GPU 显存**(高效参数微调) | 64 | | -------------- | ------------------------- | --------------------------------- | 65 | | FP16(无量化) | 13 GB | 14 GB | 66 | | INT8 | 8 GB | 9 GB | 67 | | INT4 | 6 GB | 7 GB | 68 | 69 | ```shell 70 | # 使用知识库功能需要在启动API前运行 71 | python gen_data.py 72 | # 进入文件夹 `/service` 运行以下命令 73 | python main.py 74 | ``` 75 | 还有以下可选参数可用: 76 | 77 | - `device` 使用设备,cpu或者gpu 78 | - `quantize` 量化等级。可选值:16,8,4,默认为16 79 | - `host` HOST,默认值为 0.0.0.0 80 | - `port` PORT,默认值为 3002 81 | 82 | 也就是说可以这样启动(这里修改端口的话前端也需要修改,建议使用默认端口) 83 | ```shell 84 | python main.py --device='cuda:0' --quantize=16 --host='0.0.0.0' --port=3002 85 | ``` 86 | 87 | ### 前端网页 88 | 89 | 根目录下运行以下命令 90 | 91 | ```shell 92 | # 前端网页的默认端口号是3000,对接的后端服务的默认端口号是3002,可以在 .env 和 .vite.config.ts 文件中修改 93 | pnpm bootstrap 94 | pnpm dev 95 | ``` 96 | 97 | ## 打包为docker容器 98 | 99 | -- 待更新 100 | 101 | ## 常见问题 102 | 103 | Q: 为什么 `Git` 提交总是报错? 104 | 105 | A: 因为有提交信息验证,请遵循 [Commit 指南](./CONTRIBUTING.md) 106 | 107 | Q: 如果只使用前端页面,在哪里改请求接口? 108 | 109 | A: 根目录下 `.env` 文件中的 `VITE_GLOB_API_URL` 字段。 110 | 111 | Q: 文件保存时全部爆红? 112 | 113 | A: `vscode` 请安装项目推荐插件,或手动安装 `Eslint` 插件。 114 | 115 | Q: 前端没有打字机效果? 116 | 117 | A: 一种可能原因是经过 Nginx 反向代理,开启了 buffer,则 Nginx 118 | 会尝试从后端缓冲一定大小的数据再发送给浏览器。请尝试在反代参数后添加 `proxy_buffering off;`,然后重载 Nginx。其他 web 119 | server 配置同理。 120 | 121 | Q: build docker容器的时候,显示`exec entrypoint.sh: no such file or directory`? 122 | 123 | A: 因为`entrypoint.sh`文件的换行符是`LF`,而不是`CRLF`,如果你用`CRLF`的IDE操作过这个文件,可能就会出错。可以使用`dos2unix`工具将`LF`换成`CRLF`。 124 | 125 | ## 参与贡献 126 | 127 | 贡献之前请先阅读 [贡献指南](./CONTRIBUTING.md) 128 | 129 | 感谢原作者[Chanzhaoyu](https://github.com/Chanzhaoyu/chatgpt-web/)和所有做过贡献的人,开源模型[ChatGLM](https://github.com/THUDM/ChatGLM-6B) 130 | 131 | 132 | ## 赞助 133 | 134 | 如果你觉得这个项目对你有帮助,请给我点个Star。 135 | 136 | 如果情况允许,请支持原作者[Chanzhaoyu](https://github.com/Chanzhaoyu/chatgpt-web/) 137 | 138 | ## License 139 | 140 | MIT © [NCZkevin](./license) 141 | -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './proxy' 2 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from 'vite' 2 | 3 | export function createViteProxy(isOpenProxy: boolean, viteEnv: ImportMetaEnv) { 4 | if (!isOpenProxy) 5 | return 6 | 7 | const proxy: Record = { 8 | '/api': { 9 | target: viteEnv.VITE_APP_API_BASE_URL, 10 | changeOrigin: true, 11 | rewrite: path => path.replace('/api/', '/'), 12 | }, 13 | } 14 | 15 | return proxy 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | app: 5 | # 根据自己的系统选择x86_64还是aarch64 6 | image: chatglm 7 | # image: wenjing95/chatgpt-web-backend:aarch64 8 | ports: 9 | - 3002:3002 10 | environment: 11 | model_path: "/model" 12 | Device: "cpu" 13 | Quantize: 16 14 | # HOST,可选,默认值为 0.0.0.0 15 | HOST: 0.0.0.0 16 | # PORT,可选,默认值为 3002 17 | PORT: 3002 18 | volumes: 19 | - /home/zkw/code/gpt/chatglm-6b-int4:/model 20 | nginx: 21 | depends_on: 22 | - app 23 | image: nginx:alpine 24 | ports: 25 | - '80:80' 26 | expose: 27 | - '80' 28 | volumes: 29 | - ./nginx/html/:/etc/nginx/html/ 30 | - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf 31 | links: 32 | - app 33 | -------------------------------------------------------------------------------- /docker-compose/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | server_name localhost; 5 | charset utf-8; 6 | error_page 500 502 503 504 /50x.html; 7 | 8 | location / { 9 | root /etc/nginx/html/; 10 | try_files $uri /index.html; 11 | } 12 | 13 | location /api/ { 14 | proxy_set_header X-Real-IP $remote_addr; #转发用户IP 15 | proxy_pass http://app:3002/; 16 | proxy_buffering off; 17 | } 18 | 19 | proxy_set_header Host $host; 20 | proxy_set_header X-Real-IP $remote_addr; 21 | proxy_set_header REMOTE-HOST $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /docker-compose/readme.md: -------------------------------------------------------------------------------- 1 | ### docker-compose 部署教程 2 | - 将打包好的前端文件放到 `nginx/html` 目录下 3 | - ```shell 4 | # 打包启动 5 | docker-compose build 6 | docker-compose up -d 7 | ``` 8 | - ```shell 9 | # 查看运行状态 10 | docker ps 11 | ``` 12 | - ```shell 13 | # 结束运行 14 | docker-compose down 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/chatglm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCZkevin/chatglm-web/fee421f57c5a9d26c116d68a022f7c5e40d38ca3/docs/chatglm.gif -------------------------------------------------------------------------------- /docs/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCZkevin/chatglm-web/fee421f57c5a9d26c116d68a022f7c5e40d38ca3/docs/index.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | ChatGLM Web 13 | 14 | 15 | 16 |
17 | 74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ChenZhaoYu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatglm-web", 3 | "version": "0.1.0", 4 | "private": false, 5 | "description": "ChatGLM Web", 6 | "author": "NCZkevin ", 7 | "keywords": [ 8 | "chatglm-web", 9 | "chatglm", 10 | "chatbot", 11 | "vue" 12 | ], 13 | "scripts": { 14 | "dev": "vite", 15 | "build": "run-p type-check build-only", 16 | "preview": "vite preview", 17 | "build-only": "vite build", 18 | "type-check": "vue-tsc --noEmit", 19 | "lint": "eslint .", 20 | "lint:fix": "eslint . --fix", 21 | "bootstrap": "pnpm install && pnpm run common:prepare", 22 | "common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml", 23 | "common:prepare": "husky install" 24 | }, 25 | "dependencies": { 26 | "@traptitech/markdown-it-katex": "^3.6.0", 27 | "@vueuse/core": "^9.13.0", 28 | "highlight.js": "^11.7.0", 29 | "html2canvas": "^1.4.1", 30 | "katex": "^0.16.4", 31 | "markdown-it": "^13.0.1", 32 | "naive-ui": "^2.34.3", 33 | "pinia": "^2.0.32", 34 | "recorder-core": "^1.2.23020100", 35 | "vite-plugin-pwa": "^0.14.4", 36 | "vue": "^3.2.47", 37 | "vue-i18n": "^9.2.2", 38 | "vue-router": "^4.1.6" 39 | }, 40 | "devDependencies": { 41 | "@antfu/eslint-config": "^0.35.3", 42 | "@commitlint/cli": "^17.4.4", 43 | "@commitlint/config-conventional": "^17.4.4", 44 | "@iconify/vue": "^4.1.0", 45 | "@types/crypto-js": "^4.1.1", 46 | "@types/katex": "^0.16.0", 47 | "@types/markdown-it": "^12.2.3", 48 | "@types/markdown-it-link-attributes": "^3.0.1", 49 | "@types/node": "^18.14.6", 50 | "@vitejs/plugin-vue": "^4.0.0", 51 | "autoprefixer": "^10.4.13", 52 | "axios": "^1.3.4", 53 | "crypto-js": "^4.1.1", 54 | "eslint": "^8.35.0", 55 | "husky": "^8.0.3", 56 | "less": "^4.1.3", 57 | "lint-staged": "^13.1.2", 58 | "markdown-it-link-attributes": "^4.0.1", 59 | "npm-run-all": "^4.1.5", 60 | "postcss": "^8.4.21", 61 | "rimraf": "^4.2.0", 62 | "tailwindcss": "^3.2.7", 63 | "typescript": "~4.9.5", 64 | "vite": "^4.1.4", 65 | "vite-plugin-pwa": "^0.14.4", 66 | "vue-tsc": "^1.2.0" 67 | }, 68 | "lint-staged": { 69 | "*.{ts,tsx,vue}": [ 70 | "pnpm lint:fix" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCZkevin/chatglm-web/fee421f57c5a9d26c116d68a022f7c5e40d38ca3/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCZkevin/chatglm-web/fee421f57c5a9d26c116d68a022f7c5e40d38ca3/public/pwa-192x192.png -------------------------------------------------------------------------------- /public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCZkevin/chatglm-web/fee421f57c5a9d26c116d68a022f7c5e40d38ca3/public/pwa-512x512.png -------------------------------------------------------------------------------- /service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pytorch/pytorch:1.13.1-cuda11.6-cudnn8-runtime 2 | # Install linux packages 3 | ENV DEBIAN_FRONTEND noninteractive 4 | RUN apt update 5 | RUN TZ=Etc/UTC apt install -y tzdata 6 | RUN apt install --no-install-recommends -y gcc git zip curl htop libgl1-mesa-glx libglib2.0-0 libpython3-dev gnupg 7 | 8 | COPY . . 9 | RUN pip3 install -r requirements.txt 10 | ENV model_path="/model" 11 | ENV Device="cpu" 12 | ENV Quantize=16 13 | ENV HOST="0.0.0.0" 14 | ENV PORT=3002 15 | 16 | EXPOSE 3002 17 | 18 | # 修改 entrypoint.sh 的权限 19 | RUN chmod +x entrypoint.sh 20 | ENTRYPOINT ["./entrypoint.sh"] 21 | -------------------------------------------------------------------------------- /service/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 将环境变量传递给 Python 脚本 4 | export Device="$Device" 5 | export Quantize="$Quantize" 6 | export HOST="$HOST" 7 | export PORT="$PORT" 8 | 9 | # 启动 Python 脚本 10 | # python main.py --device="$Device" --quantize=$Quantize --host='$HOST' --port=$PORT 11 | python main.py --device='cpu' --quantize=16 --host='0.0.0.0' --port=3002 12 | -------------------------------------------------------------------------------- /service/errors.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Errors(Enum): 5 | SOMETHING_WRONG = "ChatGptWebServerError:SomethingWrong" 6 | SOMETHING_WRONG_IN_OPENAI_GPT_API = "ChatGptWebServerError:SomethingWrongInOpenaiGptApi" 7 | SOMETHING_WRONG_IN_OPENAI_MODERATION_API = "ChatGptWebServerError:SomethingWrongInOpenaiModerationApi" 8 | SOMETHING_WRONG_IN_OPENAI_WHISPER_API = "ChatGptWebServerError:SomethingWrongInOpenaiWhisperApi" 9 | NOT_COMPLY_POLICY = "ChatGptWebServerError:NotComplyPolicy" 10 | PROMPT_IS_EMPTY = "ChatGptWebServerError:PromptIsEmpty" 11 | -------------------------------------------------------------------------------- /service/gen_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | from whoosh.fields import * 3 | from jieba.analyse import ChineseAnalyzer 4 | from whoosh.filedb.filestore import FileStorage 5 | 6 | def gen_whoosh_data(): 7 | floder='knowledge' 8 | files=os.listdir(floder) 9 | analyzer = ChineseAnalyzer() 10 | schema = Schema(title=TEXT(stored=True), content=TEXT(stored=True, analyzer=analyzer)) 11 | storage = FileStorage('knowdata') 12 | if not os.path.exists('knowdata'): 13 | os.mkdir('knowdata') 14 | ix = storage.create_index(schema) 15 | else: 16 | ix = storage.open_index() 17 | 18 | writer = ix.writer() 19 | for file in files: 20 | try: 21 | with open(floder+'/'+file,"r",encoding='utf-16') as f: 22 | data = f.read() 23 | except: 24 | with open(floder+'/'+file,"r",encoding='utf-8') as f: 25 | data = f.read() 26 | writer.add_document(title=file, content=data) 27 | 28 | writer.commit() # 提交 29 | print("读取知识库文件完成") 30 | 31 | 32 | if __name__ == "__main__": 33 | gen_whoosh_data() 34 | -------------------------------------------------------------------------------- /service/knowledge.py: -------------------------------------------------------------------------------- 1 | from whoosh import highlight 2 | from whoosh.filedb.filestore import FileStorage 3 | storage = FileStorage('knowdata') 4 | ix = storage.open_index() 5 | my_cf = highlight.ContextFragmenter(maxchars=100, surround=50) 6 | def find_whoosh(s): 7 | with ix.searcher() as searcher: 8 | results=searcher.find("content", s) 9 | results.fragmenter.charlimit = None 10 | results.fragmenter = my_cf 11 | results.formatter =highlight.UppercaseFormatter() 12 | return [{'title':results[i]["title"],'content':results[i].highlights("content")} for i in range(min(3, len(results)))] 13 | -------------------------------------------------------------------------------- /service/knowledge/test.txt: -------------------------------------------------------------------------------- 1 | 简介 2 | 关于API v3 3 | 为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。 4 | 相较于之前的微信支付API,主要区别是: 5 | 遵循统一的REST的设计风格 6 | 使用JSON作为数据交互的格式,不再使用XML 7 | 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256 8 | 不再要求携带HTTPS客户端证书(仅需携带证书序列号) 9 | 使用AES-256-GCM,对回调中的关键信息进行加密保护 10 | SDK接入 11 | 我们提供了微信支付API v3官方SDK(目前包含Java、PHP、GO三种语言版本)。此外,我们也提供API v3的Postman调试工具、微信支付平台证书下载工具,你可以通过我们的GitHub获取。 12 | 我们建议商户基于微信支付官方提供的SDK来开发应用。SDK为商户的技术人员封装了请求的签名和应答的验签,简化了商户系统的开发工作。 13 | 自行接入 14 | 在规则说明中,你将了解到微信支付API v3的基础约定,如数据格式、参数兼容性、错误处理、UA说明等。我们还重点介绍了微信支付API v3新的认证机制(证书/密钥/签名)。你可以跟随着开发指南,使用命令行或者你熟悉的编程语言,一步一步实践签名生成、签名验证、证书和回调报文解密和敏感信息加解密。在最后的常见问题中,我们总结了商户接入过程遇到的各种问题。 15 | 如果你有任何问题,欢迎访问我们的开发者社区。 -------------------------------------------------------------------------------- /service/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uvicorn 3 | import json 4 | import traceback 5 | import uuid 6 | import argparse 7 | 8 | from os.path import abspath, dirname 9 | from loguru import logger 10 | from fastapi import FastAPI 11 | from fastapi.responses import JSONResponse, StreamingResponse 12 | from message_store import MessageStore 13 | from transformers import AutoModel, AutoTokenizer 14 | from errors import Errors 15 | import knowledge 16 | import gen_data 17 | 18 | log_folder = os.path.join(abspath(dirname(__file__)), "log") 19 | logger.add(os.path.join(log_folder, "{time}.log"), level="INFO") 20 | 21 | 22 | DEFAULT_DB_SIZE = 100000 23 | 24 | massage_store = MessageStore(db_path="message_store.json", table_name="chatgpt", max_size=DEFAULT_DB_SIZE) 25 | # Timeout for FastAPI 26 | # service_timeout = None 27 | 28 | app = FastAPI() 29 | 30 | 31 | stream_response_headers = { 32 | "Content-Type": "application/octet-stream", 33 | "Cache-Control": "no-cache", 34 | } 35 | 36 | 37 | @app.post("/config") 38 | async def config(): 39 | return JSONResponse(content=dict( 40 | message=None, 41 | status="Success", 42 | data=dict() 43 | )) 44 | 45 | 46 | 47 | async def process(prompt, options, params, message_store, is_knowledge, history=None): 48 | """ 49 | 发文字消息 50 | """ 51 | # 不能是空消息 52 | if not prompt: 53 | logger.error("Prompt is empty.") 54 | yield Errors.PROMPT_IS_EMPTY.value 55 | return 56 | 57 | 58 | try: 59 | chat = {"role": "user", "content": prompt} 60 | 61 | # 组合历史消息 62 | if options: 63 | parent_message_id = options.get("parentMessageId") 64 | messages = message_store.get_from_key(parent_message_id) 65 | if messages: 66 | messages.append(chat) 67 | else: 68 | messages = [] 69 | else: 70 | parent_message_id = None 71 | messages = [chat] 72 | 73 | # 记忆 74 | messages = messages[-params['memory_count']:] 75 | 76 | 77 | history_formatted = [] 78 | if options is not None: 79 | history_formatted = [] 80 | tmp = [] 81 | for i, old_chat in enumerate(messages): 82 | if len(tmp) == 0 and old_chat['role'] == "user": 83 | tmp.append(old_chat['content']) 84 | elif old_chat['role'] == "AI": 85 | tmp.append(old_chat['content']) 86 | history_formatted.append(tuple(tmp)) 87 | tmp = [] 88 | else: 89 | continue 90 | 91 | uid = "chatglm"+uuid.uuid4().hex 92 | footer='' 93 | if is_knowledge: 94 | response_d = knowledge.find_whoosh(prompt) 95 | output_sources = [i['title'] for i in response_d] 96 | results ='\n---\n'.join([i['content'] for i in response_d]) 97 | prompt= f'system:基于以下内容,用中文简洁和专业回答用户的问题。\n\n'+results+'\nuser:'+prompt 98 | footer= "\n参考:\n"+('\n').join(output_sources)+'' 99 | # yield footer 100 | for response, history in model.stream_chat(tokenizer, prompt, history_formatted, max_length=params['max_length'], 101 | top_p=params['top_p'], temperature=params['temperature']): 102 | message = json.dumps(dict( 103 | role="AI", 104 | id=uid, 105 | parentMessageId=parent_message_id, 106 | text=response+footer, 107 | )) 108 | yield "data: " + message 109 | 110 | except: 111 | err = traceback.format_exc() 112 | logger.error(err) 113 | yield Errors.SOMETHING_WRONG.value 114 | return 115 | 116 | try: 117 | # save to cache 118 | chat = {"role": "AI", "content": response} 119 | messages.append(chat) 120 | 121 | parent_message_id = uid 122 | message_store.set(parent_message_id, messages) 123 | except: 124 | err = traceback.format_exc() 125 | logger.error(err) 126 | 127 | 128 | @app.post("/chat-process") 129 | async def chat_process(request_data: dict): 130 | prompt = request_data['prompt'] 131 | max_length = request_data['max_length'] 132 | top_p = request_data['top_p'] 133 | temperature = request_data['temperature'] 134 | options = request_data['options'] 135 | if request_data['memory'] == 1 : 136 | memory_count = 5 137 | elif request_data['memory'] == 50: 138 | memory_count = 20 139 | else: 140 | memory_count = 999 141 | 142 | if 1 == request_data["top_p"]: 143 | top_p = 0.2 144 | elif 50 == request_data["top_p"]: 145 | top_p = 0.5 146 | else: 147 | top_p = 0.9 148 | if temperature is None: 149 | temperature = 0.9 150 | if top_p is None: 151 | top_p = 0.7 152 | is_knowledge = request_data['is_knowledge'] 153 | params = { 154 | "max_length": max_length, 155 | "top_p": top_p, 156 | "temperature": temperature, 157 | "memory_count": memory_count 158 | } 159 | answer_text = process(prompt, options, params, massage_store, is_knowledge) 160 | return StreamingResponse(content=answer_text, headers=stream_response_headers, media_type="text/event-stream") 161 | 162 | 163 | if __name__ == "__main__": 164 | parser = argparse.ArgumentParser(description='Simple API server for ChatGLM-6B') 165 | parser.add_argument('--device', '-d', help='使用设备,cpu或cuda:0等', default='cpu') 166 | parser.add_argument('--quantize', '-q', help='量化等级。可选值:16,8,4', default=16) 167 | parser.add_argument('--host', '-H', type=str, help='监听Host', default='0.0.0.0') 168 | parser.add_argument('--port', '-P', type=int, help='监听端口号', default=3002) 169 | args = parser.parse_args() 170 | 171 | import os 172 | model_path = os.getenv('model_path') 173 | # model_name = "THUDM/chatglm-6b" 174 | quantize = int(args.quantize) 175 | model = None 176 | if args.device == 'cpu': 177 | model_path = model_path if model_path else "THUDM/chatglm2-6b-int4" 178 | tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) 179 | model = AutoModel.from_pretrained(model_path, trust_remote_code=True).float() 180 | else: 181 | model_path = model_path if model_path else "THUDM/chatglm2-6b" 182 | tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) 183 | if quantize == 16: 184 | model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().cuda() 185 | else: 186 | model = AutoModel.from_pretrained(model_path, trust_remote_code=True).half().quantize(quantize).cuda() 187 | model = model.eval() 188 | uvicorn.run(app, host=args.host, port=args.port) 189 | -------------------------------------------------------------------------------- /service/message_store.py: -------------------------------------------------------------------------------- 1 | import time 2 | from tinydb import TinyDB, Query 3 | 4 | class MessageStore: 5 | def __init__(self, db_path, table_name, max_size=100000): 6 | self.db = TinyDB(db_path) 7 | self.table = self.db.table(table_name) 8 | self.max_size = max_size 9 | 10 | def set(self, key, value): 11 | if len(self.table) >= self.max_size: 12 | self._delete_oldest() 13 | self.table.insert({'key': key, 'value': value, 'timestamp': time.time()}) 14 | 15 | def get_from_key(self, key): 16 | query = Query() 17 | result = self.table.get(query.key == key) 18 | if result is None: 19 | return None 20 | return result['value'] 21 | 22 | def _delete_oldest(self): 23 | records = self.table.all() 24 | if len(records) >= self.max_size: 25 | oldest_record = sorted(records, key=lambda r: r['timestamp'])[0] 26 | self.table.remove(doc_ids=[oldest_record.doc_id]) 27 | -------------------------------------------------------------------------------- /service/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NCZkevin/chatglm-web/fee421f57c5a9d26c116d68a022f7c5e40d38ca3/service/requirements.txt -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, GenericAbortSignal } from 'axios' 2 | import { post } from '@/utils/request' 3 | 4 | export function fetchChatConfig() { 5 | return post({ 6 | url: '/config', 7 | }) 8 | } 9 | 10 | export function fetchChatAPIProcess( 11 | params: { 12 | prompt: string 13 | memory: number 14 | top_p: number 15 | max_length: number 16 | temperature: number 17 | is_knowledge: boolean 18 | options?: { conversationId?: string; parentMessageId?: string } 19 | signal?: GenericAbortSignal 20 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void 21 | }, 22 | ) { 23 | return post({ 24 | url: '/chat-process', 25 | data: { 26 | prompt: params.prompt, 27 | options: params.options, 28 | memory: params.memory, 29 | top_p: params.top_p, 30 | max_length: params.max_length, 31 | temperature: params.temperature, 32 | is_knowledge: params.is_knowledge, 33 | }, 34 | signal: params.signal, 35 | onDownloadProgress: params.onDownloadProgress, 36 | }) 37 | } 38 | 39 | export function fetchAudioChatAPIProcess( 40 | params: { 41 | formData: FormData 42 | options?: { conversationId?: string; parentMessageId?: string } 43 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void 44 | }, 45 | ) { 46 | return post({ 47 | url: '/audio-chat-process', 48 | data: params.formData, 49 | onDownloadProgress: params.onDownloadProgress, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/assets/recommend.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "awesome-chatgpt-prompts-zh", 4 | "desc": "ChatGPT 中文调教指南", 5 | "downloadUrl": "https://raw.githubusercontent.com/Nothing1024/chatgpt-prompt-collection/main/awesome-chatgpt-prompts-zh.json", 6 | "url": "https://github.com/PlexPt/awesome-chatgpt-prompts-zh" 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/Button.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/common/HoverButton/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /src/components/common/NaiveProvider/index.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/components/common/PromptStore/index.vue: -------------------------------------------------------------------------------- 1 | 290 | 291 | 443 | -------------------------------------------------------------------------------- /src/components/common/Setting/About.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 61 | -------------------------------------------------------------------------------- /src/components/common/Setting/Advance.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 116 | -------------------------------------------------------------------------------- /src/components/common/Setting/General.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 167 | -------------------------------------------------------------------------------- /src/components/common/Setting/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 73 | -------------------------------------------------------------------------------- /src/components/common/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /src/components/common/UserAvatar/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 51 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | import HoverButton from './HoverButton/index.vue' 2 | import NaiveProvider from './NaiveProvider/index.vue' 3 | import SvgIcon from './SvgIcon/index.vue' 4 | import UserAvatar from './UserAvatar/index.vue' 5 | import Setting from './Setting/index.vue' 6 | import PromptStore from './PromptStore/index.vue' 7 | 8 | export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore } 9 | -------------------------------------------------------------------------------- /src/components/custom/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/custom/index.ts: -------------------------------------------------------------------------------- 1 | import GithubSite from './GithubSite.vue' 2 | 3 | export { GithubSite } 4 | -------------------------------------------------------------------------------- /src/hooks/useBasicLayout.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' 2 | 3 | export function useBasicLayout() { 4 | const breakpoints = useBreakpoints(breakpointsTailwind) 5 | const isMobile = breakpoints.smaller('sm') 6 | 7 | return { isMobile } 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useIconRender.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { SvgIcon } from '@/components/common' 3 | 4 | export const useIconRender = () => { 5 | interface IconConfig { 6 | icon?: string 7 | color?: string 8 | fontSize?: number 9 | } 10 | 11 | interface IconStyle { 12 | color?: string 13 | fontSize?: string 14 | } 15 | 16 | const iconRender = (config: IconConfig) => { 17 | const { color, fontSize, icon } = config 18 | 19 | const style: IconStyle = {} 20 | 21 | if (color) 22 | style.color = color 23 | 24 | if (fontSize) 25 | style.fontSize = `${fontSize}px` 26 | 27 | if (!icon) 28 | window.console.warn('iconRender: icon is required') 29 | 30 | return () => h(SvgIcon, { icon, style }) 31 | } 32 | 33 | return { 34 | iconRender, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { enUS, jaJP, zhCN } from 'naive-ui' 3 | import { useAppStore } from '@/store' 4 | import { setLocale } from '@/locales' 5 | 6 | export function useLanguage() { 7 | const appStore = useAppStore() 8 | 9 | const language = computed(() => { 10 | switch (appStore.language) { 11 | case 'en-US': 12 | setLocale('en-US') 13 | return enUS 14 | case 'zh-CN': 15 | setLocale('zh-CN') 16 | return zhCN 17 | case 'ja-JP': 18 | setLocale('ja-JP') 19 | return jaJP 20 | default: 21 | setLocale('zh-CN') 22 | return enUS 23 | } 24 | }) 25 | 26 | return { language } 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalThemeOverrides } from 'naive-ui' 2 | import { computed, watch } from 'vue' 3 | import { darkTheme, useOsTheme } from 'naive-ui' 4 | import { useAppStore } from '@/store' 5 | 6 | export function useTheme() { 7 | const appStore = useAppStore() 8 | 9 | const OsTheme = useOsTheme() 10 | 11 | const isDark = computed(() => { 12 | if (appStore.theme === 'auto') 13 | return OsTheme.value === 'dark' 14 | else 15 | return appStore.theme === 'dark' 16 | }) 17 | 18 | const theme = computed(() => { 19 | return isDark.value ? darkTheme : undefined 20 | }) 21 | 22 | const themeOverrides = computed(() => { 23 | if (isDark.value) { 24 | return { 25 | common: {}, 26 | } 27 | } 28 | return {} 29 | }) 30 | 31 | watch( 32 | () => isDark.value, 33 | (dark) => { 34 | if (dark) 35 | document.documentElement.classList.add('dark') 36 | else 37 | document.documentElement.classList.remove('dark') 38 | }, 39 | { immediate: true }, 40 | ) 41 | 42 | return { theme, themeOverrides } 43 | } 44 | -------------------------------------------------------------------------------- /src/icons/403.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/404.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: 'Add', 4 | addSuccess: 'Add Success', 5 | edit: 'Edit', 6 | editSuccess: 'Edit Success', 7 | delete: 'Delete', 8 | deleteSuccess: 'Delete Success', 9 | save: 'Save', 10 | saveSuccess: 'Save Success', 11 | reset: 'Reset', 12 | action: 'Action', 13 | export: 'Export', 14 | exportSuccess: 'Export Success', 15 | import: 'Import', 16 | importSuccess: 'Import Success', 17 | clear: 'Clear', 18 | clearSuccess: 'Clear Success', 19 | yes: 'Yes', 20 | no: 'No', 21 | confirm: 'Confirm', 22 | download: 'Download', 23 | noData: 'No Data', 24 | wrong: 'Something went wrong, please try again later.', 25 | success: 'Success', 26 | failed: 'Failed', 27 | verify: 'Verify', 28 | unauthorizedTips: 'Unauthorized, please verify first.', 29 | about_head: 'This project was created by Chanzhaoyu, and has been forked and modified by WenJing. Its released under the MIT License.', 30 | 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!', 31 | }, 32 | chat: { 33 | newChatButton: 'New Chat', 34 | newChat: 'New Chat', 35 | placeholder: 'Ask me anything...(Shift + Enter = line break, "/" to trigger prompts)', 36 | placeholderMobile: 'Ask me anything...', 37 | copy: 'Copy', 38 | copied: 'Copied', 39 | copyCode: 'Copy Code', 40 | clearChat: 'Clear Chat', 41 | clearChatConfirm: 'Are you sure to clear this chat?', 42 | exportImage: 'Export Image', 43 | exportImageConfirm: 'Are you sure to export this chat to png?', 44 | exportSuccess: 'Export Success', 45 | exportFailed: 'Export Failed', 46 | usingKnowledge: 'Knowledge Mode', 47 | turnOnKnowledge: 'In the current mode, sending messages will use the knowledge stored in the knowledge base to provide an answer.', 48 | turnOffKnowledge: 'In the current mode, sending messages will not use the knowledge stored in the knowledge base to provide an answer.', 49 | usingContext: 'Context Mode', 50 | turnOnContext: 'In the current mode, sending messages will carry previous chat records.', 51 | turnOffContext: 'In the current mode, sending messages will not carry previous chat records.', 52 | deleteMessage: 'Delete Message', 53 | deleteMessageConfirm: 'Are you sure to delete this message?', 54 | deleteHistoryConfirm: 'Are you sure to clear this history?', 55 | clickToTalk: 'Click to talk', 56 | clickToSend: 'Click to send', 57 | recordingInProgress: '[Recording in progress...]', 58 | openMicrophoneFailedTitle: 'Microphone Opening Failed', 59 | openMicrophoneFailedText: 'HTTPS environment and permission are required', 60 | stopResponding: 'Stop Responding', 61 | preview: 'Preview', 62 | showRawText: 'Show as raw text', 63 | }, 64 | setting: { 65 | setting: 'Setting', 66 | randomAvatar: 'Generate a random avatar', 67 | general: 'General', 68 | advance: 'Advance', 69 | about: 'About', 70 | avatarLink: 'Avatar Link', 71 | name: 'Name', 72 | description: 'Description', 73 | resetUserInfo: 'Reset UserInfo', 74 | theme: 'Theme', 75 | language: 'Language', 76 | 77 | chatgpt_memory_title: 'ChatGLM\'s memory capacity', 78 | chatgpt_memory_memo: 'The stronger the memory, the more context ChatGLM can remember during conversations, but it may consume more costs.', 79 | chatgpt_memory_choice_1: 'Normal Memory (5 logs)', 80 | chatgpt_memory_choice_2: 'Medium Memory (20 logs)', 81 | chatgpt_memory_choice_3: 'Strong Memory (all logs)', 82 | 83 | chatgpt_top_p_title: 'The personality of ChatGLM', 84 | chatgpt_top_p_1_memo: 'Tends to precise analysis, reducing the possibility of ChatGLM\'s nonsense.', 85 | chatgpt_top_p_2_memo: 'Balancing accuracy and creativity in responses.', 86 | chatgpt_top_p_3_memo: 'Brainstorming mode, tends to provide richer information.', 87 | chatgpt_top_p_choice_1: 'Accurate', 88 | chatgpt_top_p_choice_2: 'Balanced personality', 89 | chatgpt_top_p_choice_3: 'Exploratory', 90 | chatgpt_max_length_title: 'Max Length', 91 | chatgpt_temperature_title: 'Temperature', 92 | api: 'API', 93 | timeout: 'Timeout', 94 | socks: 'Socks', 95 | }, 96 | server: { 97 | PromptIsEmpty: 'Hello! How can I assist you today?', 98 | 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.', 99 | SomethingWrong: 'Oops, something went wrong. Please try again later.', 100 | SomethingWrongInOpenaiGptApi: 'Oops, something went wrong in OpenAI GPT API. Please try again later.', 101 | SomethingWrongInOpenaiModerationApi: 'Oops, something went wrong in OpenAI Moderation API. Please try again later.', 102 | SomethingWrongInOpenaiWhisperApi: 'Oops, something went wrong in OpenAI Whisper API. Please try again later.', 103 | }, 104 | store: { 105 | siderButton: 'Prompt Store', 106 | local: 'Local', 107 | online: 'Online', 108 | title: 'Title', 109 | description: 'Description', 110 | clearStoreConfirm: 'Whether to clear the data?', 111 | importPlaceholder: 'Please paste the JSON data here', 112 | addRepeatTitleTips: 'Title duplicate, please re-enter', 113 | addRepeatContentTips: 'Content duplicate: {msg}, please re-enter', 114 | editRepeatTitleTips: 'Title conflict, please revise', 115 | editRepeatContentTips: 'Content conflict {msg} , please re-modify', 116 | importError: 'Key value mismatch', 117 | importRepeatTitle: 'Title repeatedly skipped: {msg}', 118 | importRepeatContent: 'Content is repeatedly skipped: {msg}', 119 | onlineImportWarning: 'Note: Please check the JSON file source!', 120 | downloadError: 'Please check the network status and JSON file validity', 121 | }, 122 | } 123 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createI18n } from 'vue-i18n' 3 | import en from './en-US' 4 | import cn from './zh-CN' 5 | import jp from './ja-JP' 6 | import { useAppStoreWithOut } from '@/store/modules/app' 7 | import type { Language } from '@/store/modules/app/helper' 8 | 9 | const appStore = useAppStoreWithOut() 10 | 11 | const defaultLocale = appStore.language || 'zh-CN' 12 | 13 | const i18n = createI18n({ 14 | locale: defaultLocale, 15 | fallbackLocale: 'en-US', 16 | allowComposition: true, 17 | messages: { 18 | 'en-US': en, 19 | 'zh-CN': cn, 20 | 'ja-JP': jp, 21 | }, 22 | }) 23 | 24 | export const t = i18n.global.t 25 | 26 | export function setLocale(locale: Language) { 27 | i18n.global.locale = locale 28 | } 29 | 30 | export function setupI18n(app: App) { 31 | app.use(i18n) 32 | } 33 | 34 | export default i18n 35 | -------------------------------------------------------------------------------- /src/locales/ja-JP.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | delete: '削除', 4 | save: '保存する', 5 | reset: 'リセット', 6 | yes: 'はい', 7 | no: 'いいえ', 8 | noData: 'データなし', 9 | wrong: '問題が発生しました。後でもう一度試してください。', 10 | success: '成功しました', 11 | failed: '失敗しました', 12 | about_head: '作成者はChanzhaoyuで、編集者はWenJingです。ライセンスはMITです。', 13 | about_body: 'もしプロジェクトが役に立った場合は、Githubでスターをつけていただくか、元の作者に寄付をご検討いただけると幸いです。', 14 | }, 15 | chat: { 16 | newChat: '新しい会話', 17 | placeholder: '何でも聞いてください...(Shift + Enter = 改行)', 18 | placeholderMobile: '何でも聞いてください...', 19 | copy: 'コピー', 20 | copied: 'コピー済み', 21 | copyCode: 'コードをコピー', 22 | clearChat: 'チャットをクリア', 23 | clearChatConfirm: 'このチャットをクリアしてもよろしいですか?', 24 | deleteMessage: 'メッセージを削除', 25 | deleteMessageConfirm: 'このメッセージを削除してもよろしいですか?', 26 | deleteHistoryConfirm: 'この履歴をクリアしてもよろしいですか?', 27 | clickToTalk: 'クリックして録音開始', 28 | clickToSend: '送信', 29 | recordingInProgress: '[録音中...]', 30 | openMicrophoneFailedTitle: 'マイクのオープンに失敗しました', 31 | openMicrophoneFailedText: 'HTTPS環境下で、設定でマイクの使用が許可されていることを確認してください', 32 | stopResponding: '応答を停止する', 33 | preview: 'プレビュー', 34 | showRawText: '生のテキストを表示する', 35 | }, 36 | setting: { 37 | setting: '設定', 38 | randomAvatar: 'アバターをランダムに生成する', 39 | general: '一般', 40 | advance: '高度な設定', 41 | about: 'このアプリについて', 42 | avatarLink: 'アバターリンク', 43 | name: '名前', 44 | description: '説明', 45 | resetUserInfo: 'ユーザー情報をリセット', 46 | theme: 'テーマ', 47 | language: '言語', 48 | 49 | chatgpt_memory_title: '記憶力', 50 | chatgpt_memory_memo: '記憶力が強いほど、ChatGptは会話中に覚えている文脈が多くなりますが、より多くのコストがかかる可能性があります。', 51 | chatgpt_memory_choice_1: '記憶力が弱い(5件)', 52 | chatgpt_memory_choice_2: '記憶力が普通(20件)', 53 | chatgpt_memory_choice_3: '記憶力が強い(すべて)', 54 | 55 | chatgpt_top_p_title: '性格', 56 | chatgpt_top_p_1_memo: '正確な分析に傾くことで、ChatGptの無意味な発言の可能性を減らします。', 57 | chatgpt_top_p_2_memo: '回答の正確さと創造性のバランスを兼ね備える。', 58 | chatgpt_top_p_3_memo: 'ブレインストーミングモードで、より豊富な情報を提供する傾向があります。', 59 | chatgpt_top_p_choice_1: '正確性重視', 60 | chatgpt_top_p_choice_2: '一石二鳥', 61 | chatgpt_top_p_choice_3: 'アイデア出し重視', 62 | 63 | api: 'API', 64 | timeout: 'タイムアウト', 65 | socks: 'ソックス', 66 | }, 67 | server: { 68 | PromptIsEmpty: 'こんにちは!今日は何かお手伝いできますか?', 69 | NotComplyPolicy: '申し訳ありませんが、送信されたコンテンツが私たちの使用ポリシーに準拠していません。当社のプラットフォームでは、ハラスメント、差別、暴力、ポルノグラフィ、その他の法律、規制、社会倫理に違反するコンテンツの投稿を禁止しています。ご質問がある場合は、開発者にお問い合わせいただくか、サポートをご利用ください。ありがとうございます。', 70 | SomethingWrong: '申し訳ありませんが、問題が発生しました。後でもう一度お試しください。', 71 | SomethingWrongInOpenaiGptApi: '申し訳ありませんが、OpenAI GPT APIへのアクセス中に問題が発生しました。後でもう一度お試しください。', 72 | SomethingWrongInOpenaiModerationApi: '申し訳ありませんが、OpenAI Moderation APIへのアクセス中に問題が発生しました。後でもう一度お試しください。', 73 | SomethingWrongInOpenaiWhisperApi: '申し訳ありませんが、OpenAI Whisper APIへのアクセス中に問題が発生しました。後でもう一度お試しください。', 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | common: { 3 | add: '添加', 4 | addSuccess: '添加成功', 5 | edit: '编辑', 6 | editSuccess: '编辑成功', 7 | delete: '删除', 8 | deleteSuccess: '删除成功', 9 | save: '保存', 10 | saveSuccess: '保存成功', 11 | reset: '重置', 12 | action: '操作', 13 | export: '导出', 14 | exportSuccess: '导出成功', 15 | import: '导入', 16 | importSuccess: '导入成功', 17 | clear: '清空', 18 | clearSuccess: '清空成功', 19 | yes: '是', 20 | no: '否', 21 | confirm: '确定', 22 | download: '下载', 23 | noData: '暂无数据', 24 | wrong: '好像出错了,请稍后再试。', 25 | success: '操作成功', 26 | failed: '操作失败', 27 | verify: '验证', 28 | unauthorizedTips: '未经授权,请先进行验证。', 29 | about_head: '本项目原作者为Chanzhaoyu&WenJing, 经NCZkevin分叉和修改,基于 MIT 协议开源。', 30 | about_body: '如果你觉得此项目对你有帮助,请在Github给我点个Star,谢谢你!', 31 | }, 32 | chat: { 33 | newChatButton: '新建聊天', 34 | newChat: '新的对话', 35 | placeholder: '有问题尽管问我...(Shift + Enter = 换行,"/" 触发提示词)', 36 | placeholderMobile: '有问题尽管问我...', 37 | copy: '复制', 38 | copied: '复制成功', 39 | copyCode: '复制代码', 40 | clearChat: '清空会话', 41 | clearChatConfirm: '是否清空会话?', 42 | exportImage: '保存会话到图片', 43 | exportImageConfirm: '是否将会话保存为图片?', 44 | exportSuccess: '保存成功', 45 | exportFailed: '保存失败', 46 | usingKnowledge: '知识库模式', 47 | turnOnKnowledge: '当前模式下, 发送消息会使用知识库中知识回答', 48 | turnOffKnowledge: '当前模式下, 发送消息不会使用知识库中知识回答', 49 | usingContext: '上下文模式', 50 | turnOnContext: '当前模式下, 发送消息会携带之前的聊天记录', 51 | turnOffContext: '当前模式下, 发送消息不会携带之前的聊天记录', 52 | deleteMessage: '删除消息', 53 | deleteMessageConfirm: '是否删除此消息?', 54 | deleteHistoryConfirm: '确定删除此记录?', 55 | clickToTalk: '点我开始录音', 56 | clickToSend: '正在录音,点击发送', 57 | recordingInProgress: '[正在录音...]', 58 | openMicrophoneFailedTitle: '打开麦克风失败', 59 | openMicrophoneFailedText: '需要https环境并且在设置中开启权限', 60 | stopResponding: '停止回复', 61 | preview: '预览', 62 | showRawText: '显示原文', 63 | }, 64 | setting: { 65 | setting: '设置', 66 | randomAvatar: '随机生成一个头像', 67 | general: '一般', 68 | advance: '高级', 69 | about: '关于', 70 | avatarLink: '头像链接', 71 | name: '名称', 72 | description: '描述', 73 | resetUserInfo: '重置用户信息', 74 | theme: '主题', 75 | language: '语言', 76 | 77 | chatgpt_memory_title: '记忆力', 78 | chatgpt_memory_memo: '记忆力越强,ChatGLM 在对话过程中能记住的上下文越多,但可能会消耗更多的显存', 79 | chatgpt_memory_choice_1: '普通记忆(5条)', 80 | chatgpt_memory_choice_2: '中等记忆(20条)', 81 | chatgpt_memory_choice_3: '最强记忆(全部)', 82 | 83 | chatgpt_top_p_title: '性格', 84 | chatgpt_top_p_1_memo: '倾向于提供精确的分析,减少ChatGLM胡说八道的可能性', 85 | chatgpt_top_p_2_memo: '兼顾回答的准确性和想象力', 86 | chatgpt_top_p_3_memo: '倾向于提供更丰富的信息', 87 | chatgpt_top_p_choice_1: '准确可信', 88 | chatgpt_top_p_choice_2: '平衡性格', 89 | chatgpt_top_p_choice_3: '发散思维', 90 | 91 | chatgpt_max_length_title: '文本长度', 92 | chatgpt_temperature_title: '温度', 93 | api: 'API', 94 | timeout: '超时', 95 | socks: 'Socks', 96 | }, 97 | server: { 98 | PromptIsEmpty: '你好!今天我能为您提供什么帮助?', 99 | NotComplyPolicy: '对不起,您发送的内容不符合我们的使用政策。请注意,我们的平台禁止发布涉及骚扰、歧视、暴力、色情等违反法律法规和社会道德的内容。如有疑问,请联系开发者获取更多帮助。谢谢。', 100 | SomethingWrong: '出错了,请稍后再试', 101 | SomethingWrongInOpenaiGptApi: '访问OpenAI GPT API出错,请稍后再试', 102 | SomethingWrongInOpenaiModerationApi: '访问OpenAI Moderation API出错,请稍后再试', 103 | SomethingWrongInOpenaiWhisperApi: '访问OpenAI Whisper API出错,请稍后再试', 104 | }, 105 | store: { 106 | siderButton: '提示词商店', 107 | local: '本地', 108 | online: '在线', 109 | title: '标题', 110 | description: '描述', 111 | clearStoreConfirm: '是否清空数据?', 112 | importPlaceholder: '请粘贴 JSON 数据到此处', 113 | addRepeatTitleTips: '标题重复,请重新输入', 114 | addRepeatContentTips: '内容重复:{msg},请重新输入', 115 | editRepeatTitleTips: '标题冲突,请重新修改', 116 | editRepeatContentTips: '内容冲突{msg} ,请重新修改', 117 | importError: '键值不匹配', 118 | importRepeatTitle: '标题重复跳过:{msg}', 119 | importRepeatContent: '内容重复跳过:{msg}', 120 | onlineImportWarning: '注意:请检查 JSON 文件来源!', 121 | downloadError: '请检查网络状态与 JSON 文件有效性', 122 | }, 123 | } 124 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import { setupI18n } from './locales' 4 | import { setupAssets, setupScrollbarStyle } from './plugins' 5 | import { setupStore } from './store' 6 | import { setupRouter } from './router' 7 | 8 | async function bootstrap() { 9 | const app = createApp(App) 10 | setupAssets() 11 | 12 | setupScrollbarStyle() 13 | 14 | setupStore(app) 15 | 16 | setupI18n(app) 17 | 18 | await setupRouter(app) 19 | 20 | app.mount('#app') 21 | } 22 | 23 | bootstrap() 24 | -------------------------------------------------------------------------------- /src/plugins/assets.ts: -------------------------------------------------------------------------------- 1 | import 'katex/dist/katex.min.css' 2 | import '@/styles/lib/tailwind.css' 3 | import '@/styles/lib/highlight.less' 4 | import '@/styles/lib/github-markdown.less' 5 | import '@/styles/global.less' 6 | 7 | /** Tailwind's Preflight Style Override */ 8 | function naiveStyleOverride() { 9 | const meta = document.createElement('meta') 10 | meta.name = 'naive-ui-style' 11 | document.head.appendChild(meta) 12 | } 13 | 14 | function setupAssets() { 15 | naiveStyleOverride() 16 | } 17 | 18 | export default setupAssets 19 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from './assets' 2 | import setupScrollbarStyle from './scrollbarStyle' 3 | 4 | export { setupAssets, setupScrollbarStyle } 5 | -------------------------------------------------------------------------------- /src/plugins/scrollbarStyle.ts: -------------------------------------------------------------------------------- 1 | import { darkTheme, lightTheme } from 'naive-ui' 2 | 3 | const setupScrollbarStyle = () => { 4 | const style = document.createElement('style') 5 | const styleContent = ` 6 | ::-webkit-scrollbar { 7 | background-color: transparent; 8 | width: ${lightTheme.Scrollbar.common?.scrollbarWidth}; 9 | } 10 | ::-webkit-scrollbar-thumb { 11 | background-color: ${lightTheme.Scrollbar.common?.scrollbarColor}; 12 | border-radius: ${lightTheme.Scrollbar.common?.scrollbarBorderRadius}; 13 | } 14 | html.dark ::-webkit-scrollbar { 15 | background-color: transparent; 16 | width: ${darkTheme.Scrollbar.common?.scrollbarWidth}; 17 | } 18 | html.dark ::-webkit-scrollbar-thumb { 19 | background-color: ${darkTheme.Scrollbar.common?.scrollbarColor}; 20 | border-radius: ${darkTheme.Scrollbar.common?.scrollbarBorderRadius}; 21 | } 22 | ` 23 | 24 | style.innerHTML = styleContent 25 | document.head.appendChild(style) 26 | } 27 | 28 | export default setupScrollbarStyle 29 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | import { ChatLayout } from '@/views/chat/layout' 5 | 6 | const routes: RouteRecordRaw[] = [ 7 | { 8 | path: '/', 9 | name: 'Root', 10 | component: ChatLayout, 11 | redirect: '/chat', 12 | children: [ 13 | { 14 | path: '/chat/:uuid?', 15 | name: 'Chat', 16 | component: () => import('@/views/chat/index.vue'), 17 | }, 18 | ], 19 | }, 20 | 21 | { 22 | path: '/403', 23 | name: '403', 24 | component: () => import('@/views/exception/403/index.vue'), 25 | }, 26 | 27 | { 28 | path: '/404', 29 | name: '404', 30 | component: () => import('@/views/exception/404/index.vue'), 31 | }, 32 | 33 | { 34 | path: '/:pathMatch(.*)*', 35 | name: 'notFound', 36 | redirect: '/404', 37 | }, 38 | ] 39 | 40 | export const router = createRouter({ 41 | history: createWebHashHistory(), 42 | routes, 43 | scrollBehavior: () => ({ left: 0, top: 0 }), 44 | }) 45 | 46 | export async function setupRouter(app: App) { 47 | app.use(router) 48 | await router.isReady() 49 | } 50 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createPinia } from 'pinia' 3 | 4 | export const store = createPinia() 5 | 6 | export function setupStore(app: App) { 7 | app.use(store) 8 | } 9 | 10 | export * from './modules' 11 | -------------------------------------------------------------------------------- /src/store/modules/app/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'appSetting' 4 | 5 | export type Theme = 'light' | 'dark' | 'auto' 6 | 7 | export type Language = 'zh-CN' | 'en-US' | 'ja-JP' 8 | 9 | export type focusTextarea = true 10 | 11 | export interface AppState { 12 | siderCollapsed: boolean 13 | theme: Theme 14 | language: Language 15 | focusTextarea: focusTextarea 16 | } 17 | 18 | export function defaultSetting(): AppState { 19 | return { siderCollapsed: false, theme: 'dark', language: 'zh-CN', focusTextarea: true } 20 | } 21 | 22 | export function getLocalSetting(): AppState { 23 | const localSetting: AppState | undefined = ss.get(LOCAL_NAME) 24 | return { ...defaultSetting(), ...localSetting } 25 | } 26 | 27 | export function setLocalSetting(setting: AppState): void { 28 | ss.set(LOCAL_NAME, setting) 29 | } 30 | -------------------------------------------------------------------------------- /src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { AppState, Language, Theme } from './helper' 3 | import { getLocalSetting, setLocalSetting } from './helper' 4 | import { store } from '@/store' 5 | 6 | export const useAppStore = defineStore('app-store', { 7 | state: (): AppState => getLocalSetting(), 8 | actions: { 9 | setSiderCollapsed(collapsed: boolean) { 10 | this.siderCollapsed = collapsed 11 | this.recordState() 12 | }, 13 | 14 | setTheme(theme: Theme) { 15 | this.theme = theme 16 | this.recordState() 17 | }, 18 | 19 | setLanguage(language: Language) { 20 | if (this.language !== language) { 21 | this.language = language 22 | this.recordState() 23 | } 24 | }, 25 | 26 | setFocusTextarea() { 27 | this.focusTextarea = true 28 | this.recordState() 29 | }, 30 | 31 | recordState() { 32 | setLocalSetting(this.$state) 33 | }, 34 | }, 35 | }) 36 | 37 | export function useAppStoreWithOut() { 38 | return useAppStore(store) 39 | } 40 | -------------------------------------------------------------------------------- /src/store/modules/chat/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'chatStorage' 4 | 5 | export function defaultState(): Chat.ChatState { 6 | const uuid = 1002 7 | return { 8 | active: uuid, 9 | usingContext: true, 10 | usingKnowledge: false, 11 | history: [{ uuid, title: 'New Chat', isEdit: false }], 12 | chat: [{ uuid, data: [] }], 13 | } 14 | } 15 | 16 | export function getLocalState(): Chat.ChatState { 17 | const localState = ss.get(LOCAL_NAME) 18 | return localState ?? defaultState() 19 | } 20 | 21 | export function setLocalState(state: Chat.ChatState) { 22 | ss.set(LOCAL_NAME, state) 23 | } 24 | -------------------------------------------------------------------------------- /src/store/modules/chat/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { getLocalState, setLocalState } from './helper' 3 | import { router } from '@/router' 4 | 5 | export const useChatStore = defineStore('chat-store', { 6 | state: (): Chat.ChatState => getLocalState(), 7 | 8 | getters: { 9 | getChatHistoryByCurrentActive(state: Chat.ChatState) { 10 | const index = state.history.findIndex(item => item.uuid === state.active) 11 | if (index !== -1) 12 | return state.history[index] 13 | return null 14 | }, 15 | 16 | getChatByUuid(state: Chat.ChatState) { 17 | return (uuid?: number) => { 18 | if (uuid) 19 | return state.chat.find(item => item.uuid === uuid)?.data ?? [] 20 | return state.chat.find(item => item.uuid === state.active)?.data ?? [] 21 | } 22 | }, 23 | }, 24 | 25 | actions: { 26 | setUsingContext(context: boolean) { 27 | this.usingContext = context 28 | this.recordState() 29 | }, 30 | 31 | setUsingKnowledge(knowledge: boolean) { 32 | this.usingKnowledge = knowledge 33 | this.recordState() 34 | }, 35 | 36 | addHistory(history: Chat.History, chatData: Chat.Chat[] = []) { 37 | this.history.unshift(history) 38 | this.chat.unshift({ uuid: history.uuid, data: chatData }) 39 | this.active = history.uuid 40 | this.reloadRoute(history.uuid) 41 | }, 42 | 43 | updateHistory(uuid: number, edit: Partial) { 44 | const index = this.history.findIndex(item => item.uuid === uuid) 45 | if (index !== -1) { 46 | this.history[index] = { ...this.history[index], ...edit } 47 | this.recordState() 48 | } 49 | }, 50 | 51 | async deleteHistory(index: number) { 52 | this.history.splice(index, 1) 53 | this.chat.splice(index, 1) 54 | 55 | if (this.history.length === 0) { 56 | this.active = null 57 | this.reloadRoute() 58 | return 59 | } 60 | 61 | if (index > 0 && index <= this.history.length) { 62 | const uuid = this.history[index - 1].uuid 63 | this.active = uuid 64 | this.reloadRoute(uuid) 65 | return 66 | } 67 | 68 | if (index === 0) { 69 | if (this.history.length > 0) { 70 | const uuid = this.history[0].uuid 71 | this.active = uuid 72 | this.reloadRoute(uuid) 73 | } 74 | } 75 | 76 | if (index > this.history.length) { 77 | const uuid = this.history[this.history.length - 1].uuid 78 | this.active = uuid 79 | this.reloadRoute(uuid) 80 | } 81 | }, 82 | 83 | async setActive(uuid: number) { 84 | this.active = uuid 85 | return await this.reloadRoute(uuid) 86 | }, 87 | 88 | getChatByUuidAndIndex(uuid: number, index: number) { 89 | if (!uuid || uuid === 0) { 90 | if (this.chat.length) 91 | return this.chat[0].data[index] 92 | return null 93 | } 94 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 95 | if (chatIndex !== -1) 96 | return this.chat[chatIndex].data[index] 97 | return null 98 | }, 99 | 100 | addChatByUuid(uuid: number, chat: Chat.Chat, newChatText: string) { 101 | if (!uuid || uuid === 0) { 102 | if (this.history.length === 0) { 103 | const uuid = Date.now() 104 | this.history.push({ uuid, title: chat.text, isEdit: false }) 105 | this.chat.push({ uuid, data: [chat] }) 106 | this.active = uuid 107 | this.recordState() 108 | } 109 | else { 110 | this.chat[0].data.push(chat) 111 | if (this.history[0].title === newChatText) 112 | this.history[0].title = chat.text 113 | this.recordState() 114 | } 115 | } 116 | 117 | const index = this.chat.findIndex(item => item.uuid === uuid) 118 | if (index !== -1) { 119 | this.chat[index].data.push(chat) 120 | if (this.history[index].title === newChatText) 121 | this.history[index].title = chat.text 122 | this.recordState() 123 | } 124 | }, 125 | 126 | updateChatByUuid(uuid: number, index: number, chat: Chat.Chat) { 127 | if (!uuid || uuid === 0) { 128 | if (this.chat.length) { 129 | this.chat[0].data[index] = chat 130 | this.recordState() 131 | } 132 | return 133 | } 134 | 135 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 136 | if (chatIndex !== -1) { 137 | this.chat[chatIndex].data[index] = chat 138 | this.recordState() 139 | } 140 | }, 141 | 142 | updateChatSomeByUuid(uuid: number, index: number, chat: Partial) { 143 | if (!uuid || uuid === 0) { 144 | if (this.chat.length) { 145 | this.chat[0].data[index] = { ...this.chat[0].data[index], ...chat } 146 | this.recordState() 147 | } 148 | return 149 | } 150 | 151 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 152 | if (chatIndex !== -1) { 153 | this.chat[chatIndex].data[index] = { ...this.chat[chatIndex].data[index], ...chat } 154 | this.recordState() 155 | } 156 | }, 157 | 158 | deleteChatByUuid(uuid: number, index: number) { 159 | if (!uuid || uuid === 0) { 160 | if (this.chat.length) { 161 | this.chat[0].data.splice(index, 1) 162 | this.recordState() 163 | } 164 | return 165 | } 166 | 167 | const chatIndex = this.chat.findIndex(item => item.uuid === uuid) 168 | if (chatIndex !== -1) { 169 | this.chat[chatIndex].data.splice(index, 1) 170 | this.recordState() 171 | } 172 | }, 173 | 174 | clearChatByUuid(uuid: number, chat_title: string) { 175 | if (!uuid || uuid === 0) { 176 | if (this.chat.length) { 177 | this.chat[0].data = [] 178 | this.history[0].title = chat_title 179 | this.recordState() 180 | } 181 | return 182 | } 183 | 184 | const index = this.chat.findIndex(item => item.uuid === uuid) 185 | if (index !== -1) { 186 | this.chat[index].data = [] 187 | this.recordState() 188 | } 189 | }, 190 | 191 | async reloadRoute(uuid?: number) { 192 | this.recordState() 193 | await router.push({ name: 'Chat', params: { uuid } }) 194 | }, 195 | 196 | recordState() { 197 | setLocalState(this.$state) 198 | }, 199 | }, 200 | }) 201 | -------------------------------------------------------------------------------- /src/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app' 2 | export * from './chat' 3 | export * from './user' 4 | export * from './prompt' 5 | -------------------------------------------------------------------------------- /src/store/modules/prompt/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'promptStore' 4 | 5 | export type PromptList = [] 6 | 7 | export interface PromptStore { 8 | promptList: PromptList 9 | } 10 | 11 | export function getLocalPromptList(): PromptStore { 12 | const promptStore: PromptStore | undefined = ss.get(LOCAL_NAME) 13 | return promptStore ?? { promptList: [] } 14 | } 15 | 16 | export function setLocalPromptList(promptStore: PromptStore): void { 17 | ss.set(LOCAL_NAME, promptStore) 18 | } 19 | -------------------------------------------------------------------------------- /src/store/modules/prompt/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { PromptStore } from './helper' 3 | import { getLocalPromptList, setLocalPromptList } from './helper' 4 | 5 | export const usePromptStore = defineStore('prompt-store', { 6 | state: (): PromptStore => getLocalPromptList(), 7 | 8 | actions: { 9 | updatePromptList(promptList: []) { 10 | this.$patch({ promptList }) 11 | setLocalPromptList({ promptList }) 12 | }, 13 | getPromptList() { 14 | return this.$state 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/store/modules/user/helper.ts: -------------------------------------------------------------------------------- 1 | import { ss } from '@/utils/storage' 2 | 3 | const LOCAL_NAME = 'userStorage' 4 | 5 | export interface UserInfo { 6 | avatar: string 7 | name: string 8 | description: string 9 | chatgpt_top_p: number 10 | chatgpt_memory: number 11 | chatgpt_max_length: number 12 | chatgpt_temperature: number 13 | } 14 | 15 | export interface UserState { 16 | userInfo: UserInfo 17 | } 18 | 19 | export function defaultSetting(): UserState { 20 | return { 21 | userInfo: { 22 | avatar: 'https://api.multiavatar.com/0.8481955987976837.svg', 23 | name: 'ChatGLM Web', 24 | 25 | description: '', 26 | chatgpt_top_p: 100, 27 | chatgpt_memory: 50, 28 | chatgpt_max_length: 2048, 29 | chatgpt_temperature: 0.8, 30 | }, 31 | } 32 | } 33 | 34 | export function getLocalState(): UserState { 35 | const localSetting: UserState | undefined = ss.get(LOCAL_NAME) 36 | return { ...defaultSetting(), ...localSetting } 37 | } 38 | 39 | export function setLocalState(setting: UserState): void { 40 | ss.set(LOCAL_NAME, setting) 41 | } 42 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { UserInfo, UserState } from './helper' 3 | import { defaultSetting, getLocalState, setLocalState } from './helper' 4 | 5 | export const useUserStore = defineStore('user-store', { 6 | state: (): UserState => getLocalState(), 7 | actions: { 8 | updateUserInfo(userInfo: Partial) { 9 | this.userInfo = { ...this.userInfo, ...userInfo } 10 | this.recordState() 11 | }, 12 | 13 | resetUserInfo() { 14 | this.userInfo = { ...defaultSetting().userInfo } 15 | this.recordState() 16 | }, 17 | 18 | recordState() { 19 | setLocalState(this.$state) 20 | }, 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/styles/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | padding-bottom: env(safe-area-inset-bottom); 9 | } 10 | 11 | .clickable-element { 12 | cursor: pointer; 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/lib/highlight.less: -------------------------------------------------------------------------------- 1 | html.dark { 2 | pre code.hljs { 3 | display: block; 4 | overflow-x: auto; 5 | padding: 1em 6 | } 7 | 8 | code.hljs { 9 | padding: 3px 5px 10 | } 11 | 12 | .hljs { 13 | color: #abb2bf; 14 | background: #282c34 15 | } 16 | 17 | .hljs-keyword, 18 | .hljs-operator, 19 | .hljs-pattern-match { 20 | color: #f92672 21 | } 22 | 23 | .hljs-function, 24 | .hljs-pattern-match .hljs-constructor { 25 | color: #61aeee 26 | } 27 | 28 | .hljs-function .hljs-params { 29 | color: #a6e22e 30 | } 31 | 32 | .hljs-function .hljs-params .hljs-typing { 33 | color: #fd971f 34 | } 35 | 36 | .hljs-module-access .hljs-module { 37 | color: #7e57c2 38 | } 39 | 40 | .hljs-constructor { 41 | color: #e2b93d 42 | } 43 | 44 | .hljs-constructor .hljs-string { 45 | color: #9ccc65 46 | } 47 | 48 | .hljs-comment, 49 | .hljs-quote { 50 | color: #b18eb1; 51 | font-style: italic 52 | } 53 | 54 | .hljs-doctag, 55 | .hljs-formula { 56 | color: #c678dd 57 | } 58 | 59 | .hljs-deletion, 60 | .hljs-name, 61 | .hljs-section, 62 | .hljs-selector-tag, 63 | .hljs-subst { 64 | color: #e06c75 65 | } 66 | 67 | .hljs-literal { 68 | color: #56b6c2 69 | } 70 | 71 | .hljs-addition, 72 | .hljs-attribute, 73 | .hljs-meta .hljs-string, 74 | .hljs-regexp, 75 | .hljs-string { 76 | color: #98c379 77 | } 78 | 79 | .hljs-built_in, 80 | .hljs-class .hljs-title, 81 | .hljs-title.class_ { 82 | color: #e6c07b 83 | } 84 | 85 | .hljs-attr, 86 | .hljs-number, 87 | .hljs-selector-attr, 88 | .hljs-selector-class, 89 | .hljs-selector-pseudo, 90 | .hljs-template-variable, 91 | .hljs-type, 92 | .hljs-variable { 93 | color: #d19a66 94 | } 95 | 96 | .hljs-bullet, 97 | .hljs-link, 98 | .hljs-meta, 99 | .hljs-selector-id, 100 | .hljs-symbol, 101 | .hljs-title { 102 | color: #61aeee 103 | } 104 | 105 | .hljs-emphasis { 106 | font-style: italic 107 | } 108 | 109 | .hljs-strong { 110 | font-weight: 700 111 | } 112 | 113 | .hljs-link { 114 | text-decoration: underline 115 | } 116 | } 117 | 118 | html { 119 | pre code.hljs { 120 | display: block; 121 | overflow-x: auto; 122 | padding: 1em 123 | } 124 | 125 | code.hljs { 126 | padding: 3px 5px 127 | } 128 | 129 | .hljs { 130 | color: #383a42; 131 | background: #fafafa 132 | } 133 | 134 | .hljs-comment, 135 | .hljs-quote { 136 | color: #a0a1a7; 137 | font-style: italic 138 | } 139 | 140 | .hljs-doctag, 141 | .hljs-formula, 142 | .hljs-keyword { 143 | color: #a626a4 144 | } 145 | 146 | .hljs-deletion, 147 | .hljs-name, 148 | .hljs-section, 149 | .hljs-selector-tag, 150 | .hljs-subst { 151 | color: #e45649 152 | } 153 | 154 | .hljs-literal { 155 | color: #0184bb 156 | } 157 | 158 | .hljs-addition, 159 | .hljs-attribute, 160 | .hljs-meta .hljs-string, 161 | .hljs-regexp, 162 | .hljs-string { 163 | color: #50a14f 164 | } 165 | 166 | .hljs-attr, 167 | .hljs-number, 168 | .hljs-selector-attr, 169 | .hljs-selector-class, 170 | .hljs-selector-pseudo, 171 | .hljs-template-variable, 172 | .hljs-type, 173 | .hljs-variable { 174 | color: #986801 175 | } 176 | 177 | .hljs-bullet, 178 | .hljs-link, 179 | .hljs-meta, 180 | .hljs-selector-id, 181 | .hljs-symbol, 182 | .hljs-title { 183 | color: #4078f2 184 | } 185 | 186 | .hljs-built_in, 187 | .hljs-class .hljs-title, 188 | .hljs-title.class_ { 189 | color: #c18401 190 | } 191 | 192 | .hljs-emphasis { 193 | font-style: italic 194 | } 195 | 196 | .hljs-strong { 197 | font-weight: 700 198 | } 199 | 200 | .hljs-link { 201 | text-decoration: underline 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/styles/lib/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/typings/chat.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Chat { 2 | 3 | interface Chat { 4 | dateTime: string 5 | text: string 6 | inversion?: boolean 7 | error?: boolean 8 | loading?: boolean 9 | conversationOptions?: ConversationRequest | null 10 | requestOptions: { prompt: string; options?: ConversationRequest | null } 11 | } 12 | 13 | interface History { 14 | title: string 15 | isEdit: boolean 16 | uuid: number 17 | } 18 | 19 | interface ChatState { 20 | active: number | null 21 | usingContext: boolean 22 | usingKnowledge: boolean 23 | history: History[] 24 | chat: { uuid: number; data: Chat[] }[] 25 | } 26 | 27 | interface ConversationRequest { 28 | conversationId?: string 29 | parentMessageId?: string 30 | } 31 | 32 | interface ConversationResponse { 33 | conversationId: string 34 | detail: { 35 | choices: { finish_reason: string; index: number; logprobs: any; text: string }[] 36 | created: number 37 | id: string 38 | model: string 39 | object: string 40 | usage: { completion_tokens: number; prompt_tokens: number; total_tokens: number } 41 | } 42 | id: string 43 | parentMessageId: string 44 | role: string 45 | text: string 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/typings/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GLOB_API_URL: string; 5 | readonly VITE_GLOB_API_TIMEOUT: string; 6 | readonly VITE_APP_API_BASE_URL: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar?: import('naive-ui').LoadingBarProviderInst; 3 | $dialog?: import('naive-ui').DialogProviderInst; 4 | $message?: import('naive-ui').MessageProviderInst; 5 | $notification?: import('naive-ui').NotificationProviderInst; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from 'crypto-js' 2 | 3 | const CryptoSecret = '__CRYPTO_SECRET__' 4 | 5 | export function enCrypto(data: any) { 6 | const str = JSON.stringify(data) 7 | return CryptoJS.AES.encrypt(str, CryptoSecret).toString() 8 | } 9 | 10 | export function deCrypto(data: string) { 11 | const bytes = CryptoJS.AES.decrypt(data, CryptoSecret) 12 | const str = bytes.toString(CryptoJS.enc.Utf8) 13 | 14 | if (str) 15 | return JSON.parse(str) 16 | 17 | return null 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/format/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 转义 HTML 字符 3 | * @param source 4 | */ 5 | export function encodeHTML(source: string) { 6 | return source 7 | .replace(/&/g, '&') 8 | .replace(//g, '>') 10 | .replace(/"/g, '"') 11 | .replace(/'/g, ''') 12 | } 13 | 14 | /** 15 | * 判断是否为代码块 16 | * @param text 17 | */ 18 | export function includeCode(text: string | null | undefined) { 19 | const regexp = /^(?:\s{4}|\t).+/gm 20 | return !!(text?.includes(' = ') || text?.match(regexp)) 21 | } 22 | 23 | /** 24 | * 复制文本 25 | * @param options 26 | */ 27 | export function copyText(options: { text: string; origin?: boolean }) { 28 | const props = { origin: true, ...options } 29 | 30 | let input: HTMLInputElement | HTMLTextAreaElement 31 | 32 | if (props.origin) 33 | input = document.createElement('textarea') 34 | else 35 | input = document.createElement('input') 36 | 37 | input.setAttribute('readonly', 'readonly') 38 | input.value = props.text 39 | document.body.appendChild(input) 40 | input.select() 41 | if (document.execCommand('copy')) 42 | document.execCommand('copy') 43 | document.body.removeChild(input) 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/is/index.ts: -------------------------------------------------------------------------------- 1 | export function isNumber(value: T | unknown): value is number { 2 | return Object.prototype.toString.call(value) === '[object Number]' 3 | } 4 | 5 | export function isString(value: T | unknown): value is string { 6 | return Object.prototype.toString.call(value) === '[object String]' 7 | } 8 | 9 | export function isBoolean(value: T | unknown): value is boolean { 10 | return Object.prototype.toString.call(value) === '[object Boolean]' 11 | } 12 | 13 | export function isNull(value: T | unknown): value is null { 14 | return Object.prototype.toString.call(value) === '[object Null]' 15 | } 16 | 17 | export function isUndefined(value: T | unknown): value is undefined { 18 | return Object.prototype.toString.call(value) === '[object Undefined]' 19 | } 20 | 21 | export function isObject(value: T | unknown): value is object { 22 | return Object.prototype.toString.call(value) === '[object Object]' 23 | } 24 | 25 | export function isArray(value: T | unknown): value is T { 26 | return Object.prototype.toString.call(value) === '[object Array]' 27 | } 28 | 29 | export function isFunction any | void | never>(value: T | unknown): value is T { 30 | return Object.prototype.toString.call(value) === '[object Function]' 31 | } 32 | 33 | export function isDate(value: T | unknown): value is T { 34 | return Object.prototype.toString.call(value) === '[object Date]' 35 | } 36 | 37 | export function isRegExp(value: T | unknown): value is T { 38 | return Object.prototype.toString.call(value) === '[object RegExp]' 39 | } 40 | 41 | export function isPromise>(value: T | unknown): value is T { 42 | return Object.prototype.toString.call(value) === '[object Promise]' 43 | } 44 | 45 | export function isSet>(value: T | unknown): value is T { 46 | return Object.prototype.toString.call(value) === '[object Set]' 47 | } 48 | 49 | export function isMap>(value: T | unknown): value is T { 50 | return Object.prototype.toString.call(value) === '[object Map]' 51 | } 52 | 53 | export function isFile(value: T | unknown): value is T { 54 | return Object.prototype.toString.call(value) === '[object File]' 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/request/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosResponse } from 'axios' 2 | 3 | const service = axios.create({ 4 | baseURL: import.meta.env.VITE_GLOB_API_URL, 5 | }) 6 | 7 | service.interceptors.request.use( 8 | (config) => { 9 | return config 10 | }, 11 | (error) => { 12 | return Promise.reject(error.response) 13 | }, 14 | ) 15 | 16 | service.interceptors.response.use( 17 | (response: AxiosResponse): AxiosResponse => { 18 | if (response.status === 200) 19 | return response 20 | 21 | throw new Error(response.status.toString()) 22 | }, 23 | (error) => { 24 | return Promise.reject(error) 25 | }, 26 | ) 27 | 28 | export default service 29 | -------------------------------------------------------------------------------- /src/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios' 2 | import request from './axios' 3 | 4 | export interface HttpOption { 5 | url: string 6 | data?: any 7 | method?: string 8 | headers?: any 9 | onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void 10 | signal?: GenericAbortSignal 11 | beforeRequest?: () => void 12 | afterRequest?: () => void 13 | } 14 | 15 | export interface Response { 16 | data: T 17 | message: string | null 18 | status: string 19 | } 20 | 21 | function http( 22 | { url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 23 | ) { 24 | const successHandler = (res: AxiosResponse>) => { 25 | if (res.data.status === 'Success' || typeof res.data === 'string') 26 | return res.data 27 | 28 | return Promise.reject(res.data) 29 | } 30 | 31 | const failHandler = (error: Response) => { 32 | afterRequest?.() 33 | throw new Error(error?.message || 'Error') 34 | } 35 | 36 | beforeRequest?.() 37 | 38 | method = method || 'GET' 39 | 40 | const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {}) 41 | 42 | return method === 'GET' 43 | ? request.get(url, { params, signal, onDownloadProgress }).then(successHandler, failHandler) 44 | : request.post(url, params, { headers, signal, onDownloadProgress }).then(successHandler, failHandler) 45 | } 46 | 47 | export function get( 48 | { url, data, method = 'GET', onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 49 | ): Promise> { 50 | return http({ 51 | url, 52 | method, 53 | data, 54 | onDownloadProgress, 55 | signal, 56 | beforeRequest, 57 | afterRequest, 58 | }) 59 | } 60 | 61 | export function post( 62 | { url, data, method = 'POST', headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption, 63 | ): Promise> { 64 | return http({ 65 | url, 66 | method, 67 | data, 68 | headers, 69 | onDownloadProgress, 70 | signal, 71 | beforeRequest, 72 | afterRequest, 73 | }) 74 | } 75 | 76 | export default post 77 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './local' 2 | -------------------------------------------------------------------------------- /src/utils/storage/local.ts: -------------------------------------------------------------------------------- 1 | import { deCrypto, enCrypto } from '../crypto' 2 | 3 | interface StorageData { 4 | data: T 5 | expire: number | null 6 | } 7 | 8 | export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) { 9 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7 10 | 11 | const { expire, crypto } = Object.assign( 12 | { 13 | expire: DEFAULT_CACHE_TIME, 14 | crypto: true, 15 | }, 16 | options, 17 | ) 18 | 19 | function set(key: string, data: T) { 20 | const storageData: StorageData = { 21 | data, 22 | expire: expire !== null ? new Date().getTime() + expire * 1000 : null, 23 | } 24 | 25 | const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData) 26 | window.localStorage.setItem(key, json) 27 | } 28 | 29 | function get(key: string) { 30 | const json = window.localStorage.getItem(key) 31 | if (json) { 32 | let storageData: StorageData | null = null 33 | 34 | try { 35 | storageData = crypto ? deCrypto(json) : JSON.parse(json) 36 | } 37 | catch { 38 | // Prevent failure 39 | } 40 | 41 | if (storageData) { 42 | const { data, expire } = storageData 43 | if (expire === null || expire >= Date.now()) 44 | return data 45 | } 46 | 47 | remove(key) 48 | return null 49 | } 50 | } 51 | 52 | function remove(key: string) { 53 | window.localStorage.removeItem(key) 54 | } 55 | 56 | function clear() { 57 | window.localStorage.clear() 58 | } 59 | 60 | return { 61 | set, 62 | get, 63 | remove, 64 | clear, 65 | } 66 | } 67 | 68 | export const ls = createLocalStorage() 69 | 70 | export const ss = createLocalStorage({ expire: null, crypto: false }) 71 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Avatar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/Text.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 82 | 83 | 86 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/index.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 117 | -------------------------------------------------------------------------------- /src/views/chat/components/Message/style.less: -------------------------------------------------------------------------------- 1 | .markdown-body { 2 | background-color: transparent; 3 | font-size: 14px; 4 | 5 | p { 6 | white-space: pre-wrap; 7 | } 8 | 9 | ol { 10 | list-style-type: decimal; 11 | } 12 | 13 | ul { 14 | list-style-type: disc; 15 | } 16 | 17 | pre code, 18 | pre tt { 19 | line-height: 1.65; 20 | } 21 | 22 | .highlight pre, 23 | pre { 24 | background-color: #fff; 25 | } 26 | 27 | code.hljs { 28 | padding: 0; 29 | } 30 | 31 | .code-block { 32 | &-wrapper { 33 | position: relative; 34 | padding-top: 24px; 35 | } 36 | 37 | &-header { 38 | position: absolute; 39 | top: 5px; 40 | right: 0; 41 | width: 100%; 42 | padding: 0 1rem; 43 | display: flex; 44 | justify-content: flex-end; 45 | align-items: center; 46 | color: #b3b3b3; 47 | 48 | &__copy{ 49 | cursor: pointer; 50 | margin-left: 0.5rem; 51 | user-select: none; 52 | &:hover { 53 | color: #65a665; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | html.dark { 61 | .message-reply { 62 | .raw-text { 63 | white-space: pre-wrap; 64 | color: var(--n-text-color); 65 | } 66 | } 67 | 68 | .highlight pre, 69 | pre { 70 | background-color: #282c34; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/views/chat/components/index.ts: -------------------------------------------------------------------------------- 1 | import Message from './Message/index.vue' 2 | 3 | export { Message } 4 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useChat.ts: -------------------------------------------------------------------------------- 1 | import { useChatStore } from '@/store' 2 | 3 | export function useChat() { 4 | const chatStore = useChatStore() 5 | 6 | const getChatByUuidAndIndex = (uuid: number, index: number) => { 7 | return chatStore.getChatByUuidAndIndex(uuid, index) 8 | } 9 | 10 | const addChat = (uuid: number, chat: Chat.Chat, newChatText: string) => { 11 | chatStore.addChatByUuid(uuid, chat, newChatText) 12 | } 13 | 14 | const updateChat = (uuid: number, index: number, chat: Chat.Chat) => { 15 | chatStore.updateChatByUuid(uuid, index, chat) 16 | } 17 | 18 | const updateChatSome = (uuid: number, index: number, chat: Partial) => { 19 | chatStore.updateChatSomeByUuid(uuid, index, chat) 20 | } 21 | 22 | return { 23 | addChat, 24 | updateChat, 25 | updateChatSome, 26 | getChatByUuidAndIndex, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useCopyCode.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUpdated } from 'vue' 2 | import { copyText } from '@/utils/format' 3 | 4 | export function useCopyCode() { 5 | function copyCodeBlock() { 6 | const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper') 7 | codeBlockWrapper.forEach((wrapper) => { 8 | const copyBtn = wrapper.querySelector('.code-block-header__copy') 9 | const codeBlock = wrapper.querySelector('.code-block-body') 10 | if (copyBtn && codeBlock) { 11 | copyBtn.addEventListener('click', () => { 12 | if (navigator.clipboard?.writeText) 13 | navigator.clipboard.writeText(codeBlock.textContent ?? '') 14 | else 15 | copyText({ text: codeBlock.textContent ?? '', origin: true }) 16 | }) 17 | } 18 | }) 19 | } 20 | 21 | onMounted(() => copyCodeBlock()) 22 | 23 | onUpdated(() => copyCodeBlock()) 24 | } 25 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { nextTick, ref } from 'vue' 3 | 4 | type ScrollElement = HTMLDivElement | null 5 | 6 | interface ScrollReturn { 7 | scrollRef: Ref 8 | scrollToBottom: () => Promise 9 | scrollToTop: () => Promise 10 | scrollToBottomIfAtBottom: () => Promise 11 | } 12 | 13 | export function useScroll(): ScrollReturn { 14 | const scrollRef = ref(null) 15 | 16 | const scrollToBottom = async () => { 17 | await nextTick() 18 | if (scrollRef.value) 19 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 20 | } 21 | 22 | const scrollToTop = async () => { 23 | await nextTick() 24 | if (scrollRef.value) 25 | scrollRef.value.scrollTop = 0 26 | } 27 | 28 | const scrollToBottomIfAtBottom = async () => { 29 | await nextTick() 30 | if (scrollRef.value) { 31 | const threshold = 50 // 阈值,表示滚动条到底部的距离阈值 32 | const distanceToBottom = scrollRef.value.scrollHeight - scrollRef.value.scrollTop - scrollRef.value.clientHeight 33 | if (distanceToBottom <= threshold) 34 | scrollRef.value.scrollTop = scrollRef.value.scrollHeight 35 | } 36 | } 37 | 38 | return { 39 | scrollRef, 40 | scrollToBottom, 41 | scrollToTop, 42 | scrollToBottomIfAtBottom, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useUsingContext.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useMessage } from 'naive-ui' 3 | import { t } from '@/locales' 4 | import { useChatStore } from '@/store' 5 | 6 | export function useUsingContext() { 7 | const ms = useMessage() 8 | const chatStore = useChatStore() 9 | const usingContext = computed(() => chatStore.usingContext) 10 | 11 | function toggleUsingContext() { 12 | chatStore.setUsingContext(!usingContext.value) 13 | if (usingContext.value) 14 | ms.success(t('chat.turnOnContext')) 15 | else 16 | ms.warning(t('chat.turnOffContext')) 17 | } 18 | 19 | return { 20 | usingContext, 21 | toggleUsingContext, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/chat/hooks/useUsingKnowledge.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useMessage } from 'naive-ui' 3 | import { t } from '@/locales' 4 | import { useChatStore } from '@/store' 5 | 6 | export function useUsingKnowledge() { 7 | const ms = useMessage() 8 | const chatStore = useChatStore() 9 | const usingKnowledge = computed(() => chatStore.usingKnowledge) 10 | 11 | function toggleUsingKnowledge() { 12 | chatStore.setUsingKnowledge(!usingKnowledge.value) 13 | if (usingKnowledge.value) 14 | ms.success(t('chat.turnOnKnowledge')) 15 | else 16 | ms.warning(t('chat.turnOffKnowledge')) 17 | } 18 | 19 | return { 20 | usingKnowledge, 21 | toggleUsingKnowledge, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/views/chat/layout/Layout.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 49 | -------------------------------------------------------------------------------- /src/views/chat/layout/header/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 69 | -------------------------------------------------------------------------------- /src/views/chat/layout/index.ts: -------------------------------------------------------------------------------- 1 | import ChatLayout from './Layout.vue' 2 | 3 | export { ChatLayout } 4 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/Footer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 25 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/List.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 102 | -------------------------------------------------------------------------------- /src/views/chat/layout/sider/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 88 | -------------------------------------------------------------------------------- /src/views/exception/403/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/views/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | 2 | cd ./service 3 | nohup python main.py --device 'cuda:0' 4 | echo "Start service complete!" 5 | 6 | 7 | cd .. 8 | echo "" > front.log 9 | nohup pnpm dev > front.log & 10 | echo "Start front complete!" 11 | tail -f front.log 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: [ 5 | './index.html', 6 | './src/**/*.{vue,js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | animation: { 11 | blink: 'blink 1.2s infinite steps(1, start)', 12 | }, 13 | keyframes: { 14 | blink: { 15 | '0%, 100%': { 'background-color': 'currentColor' }, 16 | '50%': { 'background-color': 'transparent' }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "ESNext", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "jsx": "preserve", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "noUnusedLocals": false, 14 | "strictNullChecks": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "skipLibCheck": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | }, 20 | "types": ["vite/client", "node", "naive-ui/volar"] 21 | }, 22 | "exclude": ["node_modules", "dist", "service"] 23 | } 24 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig, loadEnv } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { VitePWA } from 'vite-plugin-pwa' 5 | 6 | export default defineConfig((env) => { 7 | const viteEnv = loadEnv(env.mode, process.cwd()) as unknown as ImportMetaEnv 8 | 9 | return { 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(process.cwd(), 'src'), 13 | }, 14 | }, 15 | plugins: [ 16 | vue(), 17 | VitePWA({ 18 | injectRegister: 'auto', 19 | manifest: { 20 | name: 'ChatGLM Web', 21 | short_name: 'ChatGLM', 22 | icons: [ 23 | { src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' }, 24 | { src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' }, 25 | ], 26 | }, 27 | }), 28 | ], 29 | server: { 30 | host: '0.0.0.0', 31 | port: 3000, 32 | open: false, 33 | proxy: { 34 | '/api': { 35 | target: viteEnv.VITE_APP_API_BASE_URL, 36 | changeOrigin: true, // 允许跨域 37 | rewrite: path => path.replace('/api/', '/'), 38 | }, 39 | }, 40 | }, 41 | build: { 42 | reportCompressedSize: false, 43 | sourcemap: false, 44 | commonjsOptions: { 45 | ignoreTryCatch: false, 46 | }, 47 | }, 48 | } 49 | }) 50 | --------------------------------------------------------------------------------