Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fdf75eba0 | ||
|
|
2293969070 | ||
|
|
f7f87e266f | ||
|
|
f95af40ab8 | ||
|
|
20d6135658 | ||
|
|
f19998d59b | ||
|
|
21cf1bdd9e | ||
|
|
f234aebaec |
12
.vscode/settings.json
vendored
@@ -31,6 +31,7 @@
|
||||
"GPTAPI",
|
||||
"hljs",
|
||||
"iconify",
|
||||
"katex",
|
||||
"logprobs",
|
||||
"nodata",
|
||||
"OPENAI",
|
||||
@@ -45,5 +46,14 @@
|
||||
"VITE",
|
||||
"vueuse",
|
||||
"Zhao"
|
||||
]
|
||||
],
|
||||
"i18n-ally.enabledParsers": [
|
||||
"ts"
|
||||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.keepFulfilled": true,
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
|
||||
37
CHANGELOG.md
@@ -1,3 +1,40 @@
|
||||
## v2.9.2
|
||||
|
||||
`2023-03-04`
|
||||
|
||||
手动部署的同学,务必删除根目录和`service`中的`node_modules`重新安装依赖,降低出现问题的概率,自动部署的不需要做改动。
|
||||
|
||||
### Feature
|
||||
- 感谢 [hyln9](https://github.com/Chanzhaoyu/chatgpt-web/pull/247) 添加对渲染 `LaTex` 数学公式的支持
|
||||
- 感谢 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/227) 添加支持 `webAPP` (苹果添加到主页书签访问)支持
|
||||
- 添加 `OPENAI_API_BASE_URL` 可选环境变量[#249]
|
||||
## Enhancement
|
||||
- 优化在高分屏上主题内容的最大宽度[#257]
|
||||
- 现在文字按单词截断[#215][#225]
|
||||
### BugFix
|
||||
- 修复动态生成时代码块不能被复制的问题[#251][#260]
|
||||
- 修复 `iOS` 移动端输入框不会被键盘顶起的问题[#256]
|
||||
- 修复控制台渲染警告
|
||||
## Other
|
||||
- 更新依赖至最新
|
||||
- 修改 `README` 内容
|
||||
|
||||
## v2.9.1
|
||||
|
||||
`2023-03-02`
|
||||
|
||||
### Feature
|
||||
- 代码块添加当前代码语言显示和复制功能[#197][#196]
|
||||
- 完善多语言,现在可以切换中英文显示
|
||||
|
||||
## Enhancement
|
||||
- 由[Zo3i](https://github.com/Chanzhaoyu/chatgpt-web/pull/187) 完善 `docker-compose` 部署文档
|
||||
|
||||
### BugFix
|
||||
- 由 [ottocsb](https://github.com/Chanzhaoyu/chatgpt-web/pull/200) 修复头像修改不同步的问题
|
||||
## Other
|
||||
- 更新依赖至最新
|
||||
- 修改 `README` 内容
|
||||
## v2.9.0
|
||||
|
||||
`2023-03-02`
|
||||
|
||||
58
README.md
@@ -2,8 +2,8 @@
|
||||
|
||||
> 声明:此项目只发布于 Github,基于 MIT 协议,免费且作为开源学习使用。并且不会有任何形式的卖号、付费服务、讨论群、讨论组等行为。谨防受骗。
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
- [ChatGPT Web](#chatgpt-web)
|
||||
- [介绍](#介绍)
|
||||
@@ -36,13 +36,13 @@
|
||||
|
||||
支持双模型,提供了两种非官方 `ChatGPT API` 方法
|
||||
|
||||
| 方式 | 免费? | 可靠性 | 质量 |
|
||||
| ---- | ---- | ---- | ---- |
|
||||
| `ChatGPTAPI(GPT-3)` | 否 | 可靠 | 较笨 |
|
||||
| `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 |
|
||||
| 方式 | 免费? | 可靠性 | 质量 |
|
||||
| --------------------------------------------- | ------ | ---------- | ---- |
|
||||
| `ChatGPTAPI(gpt-3.5-turbo-0301)` | 否 | 可靠 | 相对较笨 |
|
||||
| `ChatGPTUnofficialProxyAPI(网页 accessToken)` | 是 | 相对不可靠 | 聪明 |
|
||||
|
||||
对比:
|
||||
1. `ChatGPTAPI` 使用 `text-davinci-003` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型)
|
||||
1. `ChatGPTAPI` 使用 `gpt-3.5-turbo-0301` 通过官方`OpenAI`补全`API`模拟`ChatGPT`(最稳健的方法,但它不是免费的,并且没有使用针对聊天进行微调的模型)
|
||||
2. `ChatGPTUnofficialProxyAPI` 使用非官方代理服务器访问 `ChatGPT` 的后端`API`,绕过`Cloudflare`(使用真实的的`ChatGPT`,非常轻量级,但依赖于第三方服务器,并且有速率限制)
|
||||
|
||||
[查看详情](https://github.com/Chanzhaoyu/chatgpt-web/issues/138)
|
||||
@@ -55,13 +55,21 @@
|
||||
|
||||
反向代理:
|
||||
|
||||
`ChatGPTUnofficialProxyAPI`时可用 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy)
|
||||
`ChatGPTUnofficialProxyAPI`时可用
|
||||
|
||||
```shell
|
||||
# service/.env
|
||||
API_REVERSE_PROXY=
|
||||
```
|
||||
|
||||
环境变量:
|
||||
|
||||
全部参数变量请查看或[这里](#docker-参数示例)
|
||||
|
||||
```
|
||||
/service/.env
|
||||
```
|
||||
|
||||
## 待实现路线
|
||||
[✓] 双模型
|
||||
|
||||
@@ -69,9 +77,9 @@ API_REVERSE_PROXY=
|
||||
|
||||
[✓] 对代码等消息类型的格式化美化处理
|
||||
|
||||
[✗] 界面多语言
|
||||
[✓] 界面多语言
|
||||
|
||||
[✗] 界面主题
|
||||
[✓] 界面主题
|
||||
|
||||
[✗] More...
|
||||
|
||||
@@ -145,8 +153,11 @@ pnpm dev
|
||||
|
||||
- `OPENAI_API_KEY` 二选一
|
||||
- `OPENAI_ACCESS_TOKEN` 二选一,同时存在时,`OPENAI_API_KEY` 优先
|
||||
- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用
|
||||
- `API_REVERSE_PROXY` 可选,设置 `OPENAI_ACCESS_TOKEN` 时可用 [参考](#介绍)
|
||||
- `TIMEOUT_MS` 超时,单位毫秒,可选
|
||||
- `SOCKS_PROXY_HOST` 可选,和 SOCKS_PROXY_PORT 一起时生效
|
||||
- `SOCKS_PROXY_PORT` 可选,和 SOCKS_PROXY_HOST 一起时生效
|
||||
|
||||

|
||||
|
||||
@@ -174,7 +185,7 @@ version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
|
||||
image: chenzhaoyu94/chatgpt-web # 总是使用 latest ,更新时重新 pull 该 tag 镜像即可
|
||||
ports:
|
||||
- 3002:3002
|
||||
environment:
|
||||
@@ -182,25 +193,34 @@ services:
|
||||
OPENAI_API_KEY: xxxxxx
|
||||
# 二选一
|
||||
OPENAI_ACCESS_TOKEN: xxxxxx
|
||||
# API接口地址,可选,设置 OPENAI_API_KEY 时可用
|
||||
OPENAI_API_BASE_URL: xxxx
|
||||
# 反向代理,可选
|
||||
API_REVERSE_PROXY: xxx
|
||||
# 超时,单位毫秒,可选
|
||||
TIMEOUT_MS: 60000
|
||||
# Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效
|
||||
SOCKS_PROXY_HOST: xxxx
|
||||
# Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效
|
||||
SOCKS_PROXY_PORT: xxxx
|
||||
```
|
||||
|
||||
- `OPENAI_API_BASE_URL` 可选,设置 `OPENAI_API_KEY` 时可用
|
||||
### 使用 Railway 部署
|
||||
|
||||
[](https://railway.app/new/template/yytmgc)
|
||||
|
||||
#### Railway 环境变量
|
||||
|
||||
| 环境变量名称 | 必填 | 备注 |
|
||||
| --------------------------- | ---- | ----------------------- |
|
||||
| `PORT` | 必填 | 默认 `3002` |
|
||||
| `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒, |
|
||||
| `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) |
|
||||
| `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) |
|
||||
| `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) |
|
||||
| 环境变量名称 | 必填 | 备注 |
|
||||
| --------------------- | ---------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | 必填 | 默认 `3002` |
|
||||
| `TIMEOUT_MS` | 可选 | 超时时间,单位毫秒, |
|
||||
| `OPENAI_API_KEY` | `OpenAI API` 二选一 | 使用 `OpenAI API` 所需的 `apiKey` [(获取 apiKey)](https://platform.openai.com/overview) |
|
||||
| `OPENAI_ACCESS_TOKEN` | `Web API` 二选一 | 使用 `Web API` 所需的 `accessToken` [(获取 accessToken)](https://chat.openai.com/api/auth/session) |
|
||||
| `OPENAI_API_BASE_URL` | 可选,`OpenAI API` 时可用 | `API`接口地址 |
|
||||
| `API_REVERSE_PROXY` | 可选,`Web API` 时可用 | `Web API` 反向代理地址 [详情](https://github.com/transitive-bullshit/chatgpt-api#reverse-proxy) |
|
||||
| `SOCKS_PROXY_HOST` | 可选,和 `SOCKS_PROXY_PORT` 一起时生效 | Socks代理 |
|
||||
| `SOCKS_PROXY_PORT` | 可选,和 `SOCKS_PROXY_HOST` 一起时生效 | Socks代理端口 |
|
||||
|
||||
> 注意: `Railway` 修改环境变量会重新 `Deploy`
|
||||
|
||||
|
||||
33
docker-compose/docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: chenzhaoyu94/chatgpt-web # 总是使用latest,更新时重新pull该tag镜像即可
|
||||
ports:
|
||||
- 3002:3002
|
||||
environment:
|
||||
# 二选一
|
||||
OPENAI_API_KEY: xxxx
|
||||
# 二选一
|
||||
OPENAI_ACCESS_TOKEN: xxxxxx
|
||||
# API接口地址,可选,设置 OPENAI_API_KEY 时可用
|
||||
OPENAI_API_BASE_URL: xxxx
|
||||
# 反向代理,可选
|
||||
API_REVERSE_PROXY: xxx
|
||||
# 超时,单位毫秒,可选
|
||||
TIMEOUT_MS: 60000
|
||||
# Socks代理,可选,和 SOCKS_PROXY_PORT 一起时生效
|
||||
SOCKS_PROXY_HOST: xxxx
|
||||
# Socks代理端口,可选,和 SOCKS_PROXY_HOST 一起时生效
|
||||
SOCKS_PROXY_PORT: xxxx
|
||||
nginx:
|
||||
build: nginx
|
||||
image: chatgpt/nginx
|
||||
ports:
|
||||
- '80:80'
|
||||
expose:
|
||||
- '80'
|
||||
volumes:
|
||||
- ./nginx/html/:/etc/nginx/html/
|
||||
links:
|
||||
- app
|
||||
10
docker-compose/nginx/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM hub.c.163.com/library/nginx
|
||||
|
||||
MAINTAINER jo "tionsin@live.com"
|
||||
|
||||
RUN rm -rf /etc/nginx/conf.d/default.conf
|
||||
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY ./html/ /usr/share/nginx/html/
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
20
docker-compose/nginx/nginx.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
charset utf-8;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_set_header X-Real-IP $remote_addr; #转发用户IP
|
||||
proxy_pass http://app:3002;
|
||||
}
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
15
docker-compose/readme.md
Normal file
@@ -0,0 +1,15 @@
|
||||
### docker-compose 部署教程
|
||||
- 将打包好的前端文件放到 `nginx/html` 目录下
|
||||
- ```shell
|
||||
# 打包启动
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
```
|
||||
- ```shell
|
||||
# 查看运行状态
|
||||
docker ps
|
||||
```
|
||||
- ```shell
|
||||
# 结束运行
|
||||
docker-compose down
|
||||
```
|
||||
BIN
docs/c1.png
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 282 KiB |
BIN
docs/c2.png
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 128 KiB |
BIN
docs/cover.png
|
Before Width: | Height: | Size: 96 KiB |
BIN
docs/cover2.png
|
Before Width: | Height: | Size: 518 KiB |
@@ -3,6 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta content="yes" name="apple-mobile-web-app-capable"/>
|
||||
<link rel="apple-touch-icon" href="/favicon.ico">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
|
||||
<title>ChatGPT Web</title>
|
||||
|
||||
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chatgpt-web",
|
||||
"version": "2.9.0",
|
||||
"version": "2.9.2",
|
||||
"private": false,
|
||||
"description": "ChatGPT Web",
|
||||
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||
@@ -25,36 +25,38 @@
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"katex": "^0.16.4",
|
||||
"marked": "^4.2.12",
|
||||
"naive-ui": "^2.34.3",
|
||||
"pinia": "^2.0.30",
|
||||
"pinia": "^2.0.32",
|
||||
"vue": "^3.2.47",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.35.2",
|
||||
"@antfu/eslint-config": "^0.35.3",
|
||||
"@commitlint/cli": "^17.4.4",
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"@iconify/vue": "^4.1.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/katex": "^0.16.0",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/web-bluetooth": "^0.0.16",
|
||||
"@types/node": "^18.14.6",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"axios": "^1.3.3",
|
||||
"axios": "^1.3.4",
|
||||
"crypto-js": "^4.1.1",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint": "^8.35.0",
|
||||
"husky": "^8.0.3",
|
||||
"less": "^4.1.3",
|
||||
"lint-staged": "^13.1.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.21",
|
||||
"rimraf": "^4.1.2",
|
||||
"rimraf": "^4.2.0",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "~4.9.5",
|
||||
"vite": "^4.1.2",
|
||||
"vue-tsc": "^1.1.4"
|
||||
"vite": "^4.1.4",
|
||||
"vue-tsc": "^1.2.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,vue}": [
|
||||
|
||||
577
pnpm-lock.yaml
generated
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -4,8 +4,17 @@ OPENAI_API_KEY=
|
||||
# change this to an `accessToken` extracted from the ChatGPT site's `https://chat.openai.com/api/auth/session` response
|
||||
OPENAI_ACCESS_TOKEN=
|
||||
|
||||
# OpenAI API Base URL - https://api.openai.com
|
||||
OPENAI_API_BASE_URL=
|
||||
|
||||
# Reverse Proxy
|
||||
API_REVERSE_PROXY=
|
||||
|
||||
# timeout
|
||||
TIMEOUT_MS=100000
|
||||
|
||||
# Socks Proxy Host
|
||||
SOCKS_PROXY_HOST=
|
||||
|
||||
# Socks Proxy Port
|
||||
SOCKS_PROXY_PORT=
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"common:cleanup": "rimraf node_modules && rimraf pnpm-lock.yaml"
|
||||
},
|
||||
"dependencies": {
|
||||
"chatgpt": "^5.0.1",
|
||||
"chatgpt": "^5.0.6",
|
||||
"dotenv": "^16.0.3",
|
||||
"esno": "^0.16.3",
|
||||
"express": "^4.18.2",
|
||||
@@ -35,9 +35,9 @@
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^0.35.3",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^18.14.3",
|
||||
"@types/node": "^18.14.6",
|
||||
"eslint": "^8.35.0",
|
||||
"rimraf": "^4.1.3",
|
||||
"rimraf": "^4.2.0",
|
||||
"tsup": "^6.6.3",
|
||||
"typescript": "^4.9.5"
|
||||
}
|
||||
|
||||
75
service/pnpm-lock.yaml
generated
@@ -3,21 +3,21 @@ lockfileVersion: 5.4
|
||||
specifiers:
|
||||
'@antfu/eslint-config': ^0.35.3
|
||||
'@types/express': ^4.17.17
|
||||
'@types/node': ^18.14.3
|
||||
chatgpt: ^5.0.1
|
||||
'@types/node': ^18.14.6
|
||||
chatgpt: ^5.0.6
|
||||
dotenv: ^16.0.3
|
||||
eslint: ^8.35.0
|
||||
esno: ^0.16.3
|
||||
express: ^4.18.2
|
||||
isomorphic-fetch: ^3.0.0
|
||||
node-fetch: ^3.3.0
|
||||
rimraf: ^4.1.3
|
||||
rimraf: ^4.2.0
|
||||
socks-proxy-agent: ^7.0.0
|
||||
tsup: ^6.6.3
|
||||
typescript: ^4.9.5
|
||||
|
||||
dependencies:
|
||||
chatgpt: 5.0.1
|
||||
chatgpt: 5.0.6
|
||||
dotenv: 16.0.3
|
||||
esno: 0.16.3
|
||||
express: 4.18.2
|
||||
@@ -28,9 +28,9 @@ dependencies:
|
||||
devDependencies:
|
||||
'@antfu/eslint-config': 0.35.3_ycpbpc6yetojsgtrx3mwntkhsu
|
||||
'@types/express': 4.17.17
|
||||
'@types/node': 18.14.3
|
||||
'@types/node': 18.14.6
|
||||
eslint: 8.35.0
|
||||
rimraf: 4.1.3
|
||||
rimraf: 4.2.0
|
||||
tsup: 6.6.3_typescript@4.9.5
|
||||
typescript: 4.9.5
|
||||
|
||||
@@ -428,19 +428,19 @@ packages:
|
||||
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
|
||||
dependencies:
|
||||
'@types/connect': 3.4.35
|
||||
'@types/node': 18.14.3
|
||||
'@types/node': 18.14.6
|
||||
dev: true
|
||||
|
||||
/@types/connect/3.4.35:
|
||||
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||
dependencies:
|
||||
'@types/node': 18.14.3
|
||||
'@types/node': 18.14.6
|
||||
dev: true
|
||||
|
||||
/@types/express-serve-static-core/4.17.33:
|
||||
resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
|
||||
dependencies:
|
||||
'@types/node': 18.14.3
|
||||
'@types/node': 18.14.6
|
||||
'@types/qs': 6.9.7
|
||||
'@types/range-parser': 1.2.4
|
||||
dev: true
|
||||
@@ -472,8 +472,8 @@ packages:
|
||||
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
||||
dev: true
|
||||
|
||||
/@types/node/18.14.3:
|
||||
resolution: {integrity: sha512-1y36CC5iL5CMyKALzwX9cwwxcWIxvIBe3gzs4GrXWXEQ8klQnCZ2U/WDGiNrXHmQcUhnaun17XG9TEIDlGj2RA==}
|
||||
/@types/node/18.14.6:
|
||||
resolution: {integrity: sha512-93+VvleD3mXwlLI/xASjw0FzKcwzl3OdTCzm1LaRfqgS21gfFtK3zDXM5Op9TeeMsJVOaJ2VRDpT9q4Y3d0AvA==}
|
||||
dev: true
|
||||
|
||||
/@types/normalize-package-data/2.4.1:
|
||||
@@ -495,7 +495,7 @@ packages:
|
||||
resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==}
|
||||
dependencies:
|
||||
'@types/mime': 3.0.1
|
||||
'@types/node': 18.14.3
|
||||
'@types/node': 18.14.6
|
||||
dev: true
|
||||
|
||||
/@types/unist/2.0.6:
|
||||
@@ -816,6 +816,12 @@ packages:
|
||||
concat-map: 0.0.1
|
||||
dev: true
|
||||
|
||||
/brace-expansion/2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
dev: true
|
||||
|
||||
/braces/3.0.2:
|
||||
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -896,8 +902,8 @@ packages:
|
||||
resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==}
|
||||
dev: true
|
||||
|
||||
/chatgpt/5.0.1:
|
||||
resolution: {integrity: sha512-Wy+/2XL0FobiJFaQ6N5WnhRCnOwrUJCpoVCn67qqhiWrM1QW6lgmpvtDDKKDyvj7D1MLMjc2xB/kK8aT27mL/w==}
|
||||
/chatgpt/5.0.6:
|
||||
resolution: {integrity: sha512-BQ0eN2GpAlw9ojsxkHmW+bTuekbllZbOkKqjDSIVSMtbMoYDx59vcg6xXnCPkB0OBFGuG89eqGhLAfMIW34Hcw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@@ -1955,6 +1961,16 @@ packages:
|
||||
path-is-absolute: 1.0.1
|
||||
dev: true
|
||||
|
||||
/glob/9.2.1:
|
||||
resolution: {integrity: sha512-Pxxgq3W0HyA3XUvSXcFhRSs+43Jsx0ddxcFrbjxNGkL2Ak5BAUBxLqI5G6ADDeCHLfzzXFhe0b1yYcctGmytMA==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
minimatch: 7.4.2
|
||||
minipass: 4.2.4
|
||||
path-scurry: 1.6.1
|
||||
dev: true
|
||||
|
||||
/globals/13.20.0:
|
||||
resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2439,6 +2455,11 @@ packages:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
/lru-cache/7.18.1:
|
||||
resolution: {integrity: sha512-8/HcIENyQnfUTCDizRu9rrDyG6XG/21M4X7/YEGZeD76ZJilFPAUVb/2zysFf7VVO1LEjCDFyHp8pMMvozIrvg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/mdast-util-from-markdown/0.8.5:
|
||||
resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==}
|
||||
dependencies:
|
||||
@@ -2534,10 +2555,22 @@ packages:
|
||||
brace-expansion: 1.1.11
|
||||
dev: true
|
||||
|
||||
/minimatch/7.4.2:
|
||||
resolution: {integrity: sha512-xy4q7wou3vUoC9k1xGTXc+awNdGaGVHtFUaey8tiX4H1QRc04DZ/rmDFwNm2EBsuYEhAZ6SgMmYf3InGY6OauA==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
dev: true
|
||||
|
||||
/minimist/1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: true
|
||||
|
||||
/minipass/4.2.4:
|
||||
resolution: {integrity: sha512-lwycX3cBMTvcejsHITUgYj6Gy6A7Nh4Q6h9NP4sTHY1ccJlC7yKzDmiShEHsJ16Jf1nKGDEaiHxiltsJEvk0nQ==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/ms/2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
dev: false
|
||||
@@ -2804,6 +2837,14 @@ packages:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
dev: true
|
||||
|
||||
/path-scurry/1.6.1:
|
||||
resolution: {integrity: sha512-OW+5s+7cw6253Q4E+8qQ/u1fVvcJQCJo/VFD8pje+dbJCF1n5ZRMV2AEHbGp+5Q7jxQIYJxkHopnj6nzdGeZLA==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
lru-cache: 7.18.1
|
||||
minipass: 4.2.4
|
||||
dev: true
|
||||
|
||||
/path-to-regexp/0.1.7:
|
||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||
dev: false
|
||||
@@ -3007,10 +3048,12 @@ packages:
|
||||
glob: 7.2.3
|
||||
dev: true
|
||||
|
||||
/rimraf/4.1.3:
|
||||
resolution: {integrity: sha512-iyzalDLo3l5FZxxaIGUY7xI4Bf90Xt7pCipc1Mr7RsdU7H3538z+M0tlsUDrz0aHeGS9uNqiKHUJyTewwRP91Q==}
|
||||
/rimraf/4.2.0:
|
||||
resolution: {integrity: sha512-tPt+gLORNVqRCk0NwuJ5SlMEcOGvt4CCU8sUPqgCFtCbnoNCTd9Q6vq7JlBbxQlACiH14OR28y7piA2Bak9Sxw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
glob: 9.2.1
|
||||
dev: true
|
||||
|
||||
/rollup/3.15.0:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import 'isomorphic-fetch'
|
||||
import type { ChatMessage, SendMessageOptions } from 'chatgpt'
|
||||
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt'
|
||||
import { ChatGPTAPI, ChatGPTUnofficialProxyAPI } from 'chatgpt'
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent'
|
||||
import fetch from 'node-fetch'
|
||||
import { sendResponse } from '../utils'
|
||||
import type { ApiModel, ChatContext, ChatGPTAPIOptions, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
|
||||
import type { ApiModel, ChatContext, ChatGPTUnofficialProxyAPIOptions, ModelConfig } from '../types'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@@ -18,7 +18,6 @@ if (!process.env.OPENAI_API_KEY && !process.env.OPENAI_ACCESS_TOKEN)
|
||||
|
||||
let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||
|
||||
// To use ESM in CommonJS, you can use a dynamic import
|
||||
(async () => {
|
||||
// More Info: https://github.com/transitive-bullshit/chatgpt-api
|
||||
|
||||
@@ -28,6 +27,19 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||
debug: false,
|
||||
}
|
||||
|
||||
if (process.env.OPENAI_API_BASE_URL && process.env.OPENAI_API_BASE_URL.trim().length > 0)
|
||||
options.apiBaseUrl = process.env.OPENAI_API_BASE_URL
|
||||
|
||||
if (process.env.SOCKS_PROXY_HOST && process.env.SOCKS_PROXY_PORT) {
|
||||
const agent = new SocksProxyAgent({
|
||||
hostname: process.env.SOCKS_PROXY_HOST,
|
||||
port: process.env.SOCKS_PROXY_PORT,
|
||||
})
|
||||
options.fetch = (url, options) => {
|
||||
return fetch(url, { agent, ...options })
|
||||
}
|
||||
}
|
||||
|
||||
api = new ChatGPTAPI({ ...options })
|
||||
apiModel = 'ChatGPTAPI'
|
||||
}
|
||||
@@ -50,10 +62,7 @@ let api: ChatGPTAPI | ChatGPTUnofficialProxyAPI
|
||||
if (process.env.API_REVERSE_PROXY)
|
||||
options.apiReverseProxyUrl = process.env.API_REVERSE_PROXY
|
||||
|
||||
api = new ChatGPTUnofficialProxyAPI({
|
||||
accessToken: process.env.OPENAI_ACCESS_TOKEN,
|
||||
...options,
|
||||
})
|
||||
api = new ChatGPTUnofficialProxyAPI({ ...options })
|
||||
apiModel = 'ChatGPTUnofficialProxyAPI'
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import type { FetchFn, openai } from 'chatgpt'
|
||||
import type { FetchFn } from 'chatgpt'
|
||||
|
||||
export interface ChatContext {
|
||||
conversationId?: string
|
||||
parentMessageId?: string
|
||||
}
|
||||
|
||||
export interface ChatGPTAPIOptions {
|
||||
apiKey: string
|
||||
debug?: boolean
|
||||
completionParams?: Partial<openai.CompletionParams>
|
||||
}
|
||||
|
||||
export interface ChatGPTUnofficialProxyAPIOptions {
|
||||
accessToken: string
|
||||
apiReverseProxyUrl?: string
|
||||
|
||||
@@ -47,16 +47,16 @@ onMounted(() => {
|
||||
>
|
||||
Github
|
||||
</a>
|
||||
免费,并且没有任何形式分付费行为!
|
||||
,免费且基于 MIT 协议,没有任何形式的付费行为!
|
||||
</p>
|
||||
<p>
|
||||
如果你觉得此项目对你有帮助,请在 Github 帮我点个 Star 或者给予一点赞助,谢谢!
|
||||
</p>
|
||||
</div>
|
||||
<p>API方式:{{ config?.apiModel ?? '-' }}</p>
|
||||
<p>反向代理:{{ config?.reverseProxy ?? '-' }}</p>
|
||||
<p>超时时间:{{ config?.timeoutMs ?? '-' }}</p>
|
||||
<p>Socks代理:{{ config?.socksProxy ?? '-' }}</p>
|
||||
<p>{{ $t("setting.api") }}:{{ config?.apiModel ?? '-' }}</p>
|
||||
<p>{{ $t("setting.reverseProxy") }}:{{ config?.reverseProxy ?? '-' }}</p>
|
||||
<p>{{ $t("setting.timeout") }}:{{ config?.timeoutMs ?? '-' }}</p>
|
||||
<p>{{ $t("setting.socks") }}:{{ config?.socksProxy ?? '-' }}</p>
|
||||
</div>
|
||||
</NSpin>
|
||||
</template>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Language, Theme } from '@/store/modules/app/helper'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useAppStore, useUserStore } from '@/store'
|
||||
import type { UserInfo } from '@/store/modules/user/helper'
|
||||
import { t } from '@/locales'
|
||||
|
||||
interface Emit {
|
||||
(event: 'update'): void
|
||||
@@ -61,12 +62,12 @@ const languageOptions: { label: string; key: Language; value: Language }[] = [
|
||||
|
||||
function updateUserInfo(options: Partial<UserInfo>) {
|
||||
userStore.updateUserInfo(options)
|
||||
ms.success('Update success')
|
||||
ms.success(t('common.success'))
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
userStore.resetUserInfo()
|
||||
ms.success('Reset success')
|
||||
ms.success(t('common.success'))
|
||||
emit('update')
|
||||
}
|
||||
</script>
|
||||
@@ -75,40 +76,40 @@ function handleReset() {
|
||||
<div class="p-4 space-y-5 min-h-[200px]">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">Avatar Link</span>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="avatar" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
|
||||
Save
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">Name</span>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
|
||||
<div class="w-[200px]">
|
||||
<NInput v-model:value="name" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
|
||||
Save
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">Description</span>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="description" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
|
||||
Save
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">Reset UserInfo</span>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
|
||||
<NButton text type="primary" @click="handleReset">
|
||||
Reset
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">Theme</span>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
|
||||
<div class="flex items-center space-x-4">
|
||||
<template v-for="item of themeOptions" :key="item.key">
|
||||
<a
|
||||
@@ -124,7 +125,7 @@ function handleReset() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">Language</span>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
|
||||
<div class="flex items-center space-x-4">
|
||||
<template v-for="item of languageOptions" :key="item.key">
|
||||
<a
|
||||
|
||||
@@ -5,6 +5,10 @@ import General from './General.vue'
|
||||
import About from './About.vue'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
@@ -13,10 +17,6 @@ interface Emit {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const active = ref('General')
|
||||
|
||||
const reload = ref(false)
|
||||
@@ -39,13 +39,13 @@ function handleReload() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show">
|
||||
<NModal v-model:show="show" :auto-focus="false">
|
||||
<NCard role="dialog" aria-modal="true" :bordered="false" style="width: 100%; max-width: 640px">
|
||||
<NTabs v-model:value="active" type="line" animated>
|
||||
<NTabPane name="General" tab="General">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:file-user-line" />
|
||||
<span class="ml-2">General</span>
|
||||
<span class="ml-2">{{ $t('setting.general') }}</span>
|
||||
</template>
|
||||
<div class="min-h-[100px]">
|
||||
<General v-if="!reload" @update="handleReload" />
|
||||
@@ -54,7 +54,7 @@ function handleReload() {
|
||||
<NTabPane name="Config" tab="Config">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
||||
<span class="ml-2">Config</span>
|
||||
<span class="ml-2">{{ $t('setting.config') }}</span>
|
||||
</template>
|
||||
<About />
|
||||
</NTabPane>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { computed } from 'vue'
|
||||
import { enUS, zhCN } from 'naive-ui'
|
||||
import { useAppStore } from '@/store'
|
||||
import { setLocale } from '@/locales'
|
||||
|
||||
export function useLanguage() {
|
||||
const appStore = useAppStore()
|
||||
|
||||
const language = computed(() => {
|
||||
if (appStore.language === 'zh-CN')
|
||||
return zhCN
|
||||
else
|
||||
return enUS
|
||||
switch (appStore.language) {
|
||||
case 'en-US':
|
||||
setLocale('en-US')
|
||||
return enUS
|
||||
case 'zh-CN':
|
||||
setLocale('zh-CN')
|
||||
return zhCN
|
||||
default:
|
||||
setLocale('zh-CN')
|
||||
return enUS
|
||||
}
|
||||
})
|
||||
|
||||
return { language }
|
||||
|
||||
41
src/locales/en-US.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export default {
|
||||
common: {
|
||||
delete: 'Delete',
|
||||
save: 'Save',
|
||||
reset: 'Reset',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
noData: 'No Data',
|
||||
wrong: 'Something went wrong, please try again later.',
|
||||
success: 'Success',
|
||||
failed: 'Failed',
|
||||
},
|
||||
chat: {
|
||||
placeholder: 'Ask me anything...(Shift + Enter = line break)',
|
||||
placeholderMobile: 'Ask me anything...',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
copyCode: 'Copy Code',
|
||||
clearChat: 'Clear Chat',
|
||||
clearChatConfirm: 'Are you sure to clear this chat?',
|
||||
deleteMessage: 'Delete Message',
|
||||
deleteMessageConfirm: 'Are you sure to delete this message?',
|
||||
deleteHistoryConfirm: 'Are you sure to clear this history?',
|
||||
},
|
||||
setting: {
|
||||
setting: 'Setting',
|
||||
general: 'General',
|
||||
config: 'Config',
|
||||
avatarLink: 'Avatar Link',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
resetUserInfo: 'Reset UserInfo',
|
||||
theme: 'Theme',
|
||||
language: 'Language',
|
||||
api: 'API',
|
||||
reverseProxy: 'Reverse Proxy',
|
||||
timeout: 'Timeout',
|
||||
socks: 'Socks',
|
||||
},
|
||||
|
||||
}
|
||||
34
src/locales/index.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { App } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from './en-US'
|
||||
import cn from './zh-CN'
|
||||
import { useAppStoreWithOut } from '@/store/modules/app'
|
||||
import type { Language } from '@/store/modules/app/helper'
|
||||
|
||||
const appStore = useAppStoreWithOut()
|
||||
|
||||
const defaultLocale = appStore.language || 'zh-CN'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: defaultLocale,
|
||||
fallbackLocale: 'en-US',
|
||||
allowComposition: true,
|
||||
messages: {
|
||||
'en-US': en,
|
||||
'zh-CN': cn,
|
||||
},
|
||||
})
|
||||
|
||||
export function t(key: string) {
|
||||
return i18n.global.t(key)
|
||||
}
|
||||
|
||||
export function setLocale(locale: Language) {
|
||||
i18n.global.locale = locale
|
||||
}
|
||||
|
||||
export function setupI18n(app: App) {
|
||||
app.use(i18n)
|
||||
}
|
||||
|
||||
export default i18n
|
||||
41
src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export default {
|
||||
common: {
|
||||
delete: '删除',
|
||||
save: '保存',
|
||||
reset: '重置',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
noData: '暂无数据',
|
||||
wrong: '好像出错了,请稍后再试。',
|
||||
success: '操作成功',
|
||||
failed: '操作失败',
|
||||
},
|
||||
chat: {
|
||||
placeholder: '来说点什么...(Shift + Enter = 换行)',
|
||||
placeholderMobile: '来说点什么...',
|
||||
copy: '复制',
|
||||
copied: '复制成功',
|
||||
copyCode: '复制代码',
|
||||
clearChat: '清空会话',
|
||||
clearChatConfirm: '是否清空会话?',
|
||||
deleteMessage: '删除消息',
|
||||
deleteMessageConfirm: '是否删除此消息?',
|
||||
deleteHistoryConfirm: '确定删除此记录?',
|
||||
},
|
||||
setting: {
|
||||
setting: '设置',
|
||||
general: '总览',
|
||||
config: '配置',
|
||||
avatarLink: '头像链接',
|
||||
name: '名称',
|
||||
description: '描述',
|
||||
resetUserInfo: '重置用户信息',
|
||||
theme: '主题',
|
||||
language: '语言',
|
||||
api: 'API',
|
||||
reverseProxy: '反向代理',
|
||||
timeout: '超时',
|
||||
socks: 'Socks',
|
||||
},
|
||||
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { setupDirectives } from './directives'
|
||||
import { setupAssets } from '@/plugins'
|
||||
import { setupStore } from '@/store'
|
||||
import { setupRouter } from '@/router'
|
||||
import { setupI18n } from './locales'
|
||||
import { setupAssets } from './plugins'
|
||||
import { setupStore } from './store'
|
||||
import { setupRouter } from './router'
|
||||
|
||||
async function bootstrap() {
|
||||
const app = createApp(App)
|
||||
@@ -13,6 +14,8 @@ async function bootstrap() {
|
||||
|
||||
setupDirectives(app)
|
||||
|
||||
setupI18n(app)
|
||||
|
||||
await setupRouter(app)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'katex/dist/katex.min.css'
|
||||
import '@/styles/lib/tailwind.css'
|
||||
import '@/styles/lib/highlight.less'
|
||||
import '@/styles/lib/github-markdown.less'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { App } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
export const store = createPinia()
|
||||
|
||||
export function setupStore(app: App) {
|
||||
const store = createPinia()
|
||||
app.use(store)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { AppState, Language, Theme } from './helper'
|
||||
import { getLocalSetting, setLocalSetting } from './helper'
|
||||
import { store } from '@/store'
|
||||
|
||||
export const useAppStore = defineStore('app-store', {
|
||||
state: (): AppState => getLocalSetting(),
|
||||
@@ -27,3 +28,7 @@ export const useAppStore = defineStore('app-store', {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export function useAppStoreWithOut() {
|
||||
return useAppStore(store)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import { NAvatar } from 'naive-ui'
|
||||
import { useUserStore } from '@/store'
|
||||
import { isString } from '@/utils/is'
|
||||
import defaultAvatar from '@/assets/avatar.jpg'
|
||||
|
||||
interface Props {
|
||||
image?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const avatar = computed(() => userStore.userInfo.avatar)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img v-if="image" src="@/assets/avatar.jpg" class="object-cover w-full h-full " alt="avatar">
|
||||
<span v-else class="text-[27px] dark:text-white">
|
||||
<template v-if="image">
|
||||
<NAvatar v-if="isString(avatar) && avatar.length > 0" :src="avatar" :fallback-src="defaultAvatar" />
|
||||
<NAvatar v-else round :src="defaultAvatar" />
|
||||
</template>
|
||||
<span v-else class="text-[28px] dark:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" width="1em" height="1em">
|
||||
<path d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import katex from 'katex'
|
||||
import { marked } from 'marked'
|
||||
import hljs from 'highlight.js'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { encodeHTML } from '@/utils/format'
|
||||
import { t } from '@/locales'
|
||||
|
||||
interface Props {
|
||||
inversion?: boolean
|
||||
@@ -26,8 +28,10 @@ renderer.html = (html) => {
|
||||
|
||||
renderer.code = (code, language) => {
|
||||
const validLang = !!(language && hljs.getLanguage(language))
|
||||
if (validLang)
|
||||
return `<pre><code class="hljs ${language}">${hljs.highlight(language, code).value}</code></pre>`
|
||||
if (validLang) {
|
||||
const lang = language ?? ''
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${language}">${hljs.highlight(code, { language: lang }).value}</code></pre>`
|
||||
}
|
||||
return `<pre style="background: none">${hljs.highlightAuto(code).value}</pre>`
|
||||
}
|
||||
|
||||
@@ -38,6 +42,54 @@ marked.setOptions({
|
||||
},
|
||||
})
|
||||
|
||||
const katexOptions = {
|
||||
throwOnError: false,
|
||||
}
|
||||
|
||||
const katexInline = {
|
||||
name: 'katexInline',
|
||||
level: 'inline',
|
||||
start(src: string) {
|
||||
return src.indexOf('$')
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(/^\$+([^$\n]+?)\$+/)
|
||||
if (match) {
|
||||
return {
|
||||
type: 'katexInline',
|
||||
raw: match[0],
|
||||
text: match[1].trim(),
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer(token: marked.Tokens.Generic) {
|
||||
return katex.renderToString(token.text, katexOptions)
|
||||
},
|
||||
}
|
||||
|
||||
const katexBlock = {
|
||||
name: 'katexBlock',
|
||||
level: 'block',
|
||||
start(src: string) {
|
||||
return src.indexOf('\n$$')
|
||||
},
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(/^\$\$+\n([^$]+?)\n\$\$+\n/)
|
||||
if (match) {
|
||||
return {
|
||||
type: 'katexBlock',
|
||||
raw: match[0],
|
||||
text: match[1].trim(),
|
||||
}
|
||||
}
|
||||
},
|
||||
renderer(token: marked.Tokens.Generic) {
|
||||
return `<p>${katex.renderToString(token.text, katexOptions)}</p>`
|
||||
},
|
||||
}
|
||||
|
||||
marked.use({ extensions: [katexInline, katexBlock] })
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
return [
|
||||
'text-wrap',
|
||||
@@ -66,7 +118,7 @@ defineExpose({ textRef })
|
||||
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div ref="textRef" class="leading-relaxed break-all">
|
||||
<div ref="textRef" class="leading-relaxed break-words">
|
||||
<div v-if="!inversion" class="markdown-body" v-html="text" />
|
||||
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang='ts'>
|
||||
import { ref } from 'vue'
|
||||
import { NDropdown, useMessage } from 'naive-ui'
|
||||
import { NDropdown } from 'naive-ui'
|
||||
import AvatarComponent from './Avatar.vue'
|
||||
import TextComponent from './Text.vue'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { copyText } from '@/utils/format'
|
||||
import { useIconRender } from '@/hooks/useIconRender'
|
||||
import { t } from '@/locales'
|
||||
|
||||
interface Props {
|
||||
dateTime?: string
|
||||
@@ -24,25 +25,18 @@ const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const ms = useMessage()
|
||||
|
||||
const { iconRender } = useIconRender()
|
||||
|
||||
const textRef = ref<HTMLElement>()
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: 'Copy Raw',
|
||||
key: 'copyRaw',
|
||||
label: t('chat.copy'),
|
||||
key: 'copyText',
|
||||
icon: iconRender({ icon: 'ri:file-copy-2-line' }),
|
||||
},
|
||||
{
|
||||
label: 'Copy Text',
|
||||
key: 'copyText',
|
||||
icon: iconRender({ icon: 'ri:file-copy-line' }),
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: iconRender({ icon: 'ri:delete-bin-line' }),
|
||||
},
|
||||
@@ -50,15 +44,8 @@ const options = [
|
||||
|
||||
function handleSelect(key: 'copyRaw' | 'copyText' | 'delete') {
|
||||
switch (key) {
|
||||
case 'copyRaw':
|
||||
if (textRef.value && (textRef.value as any).textRef) {
|
||||
copyText({ text: (textRef.value as any).textRef.innerText })
|
||||
ms.success('Copied Raw')
|
||||
}
|
||||
return
|
||||
case 'copyText':
|
||||
copyText({ text: props.text ?? '', origin: false })
|
||||
ms.success('Copied Text')
|
||||
copyText({ text: props.text ?? '' })
|
||||
return
|
||||
case 'delete':
|
||||
emit('delete')
|
||||
|
||||
@@ -24,9 +24,37 @@
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
code.hljs{
|
||||
code.hljs {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #b3b3b3;
|
||||
|
||||
&__copy{
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
user-select: none;
|
||||
&:hover {
|
||||
color: #65a665;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
html.dark {
|
||||
|
||||
20
src/views/chat/hooks/useCopyCode.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { onMounted, onUpdated } from 'vue'
|
||||
|
||||
export function useCopyCode() {
|
||||
function copyCodeBlock() {
|
||||
const codeBlockWrapper = document.querySelectorAll('.code-block-wrapper')
|
||||
codeBlockWrapper.forEach((wrapper) => {
|
||||
const copyBtn = wrapper.querySelector('.code-block-header__copy')
|
||||
const codeBlock = wrapper.querySelector('.code-block-body')
|
||||
if (copyBtn && codeBlock) {
|
||||
copyBtn.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(codeBlock.textContent ?? '')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => copyCodeBlock())
|
||||
|
||||
onUpdated(() => copyCodeBlock())
|
||||
}
|
||||
@@ -1,23 +1,25 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { NButton, NInput, useDialog, useMessage } from 'naive-ui'
|
||||
import { NButton, NInput, useDialog } from 'naive-ui'
|
||||
import { Message } from './components'
|
||||
import { useScroll } from './hooks/useScroll'
|
||||
import { useChat } from './hooks/useChat'
|
||||
import { useCopyCode } from './hooks/useCopyCode'
|
||||
import { HoverButton, SvgIcon } from '@/components/common'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useChatStore } from '@/store'
|
||||
import { fetchChatAPIProcess } from '@/api'
|
||||
import { t } from '@/locales'
|
||||
|
||||
let controller = new AbortController()
|
||||
|
||||
const route = useRoute()
|
||||
const dialog = useDialog()
|
||||
const ms = useMessage()
|
||||
|
||||
const chatStore = useChatStore()
|
||||
|
||||
useCopyCode()
|
||||
const { isMobile } = useBasicLayout()
|
||||
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
|
||||
const { scrollRef, scrollToBottom } = useScroll()
|
||||
@@ -118,7 +120,7 @@ async function onConversation() {
|
||||
})
|
||||
}
|
||||
catch (error: any) {
|
||||
const errorMessage = error?.message ?? 'Something went wrong, please try again later.'
|
||||
const errorMessage = error?.message ?? t('common.wrong')
|
||||
|
||||
if (error.message === 'canceled') {
|
||||
updateChatSome(
|
||||
@@ -245,7 +247,7 @@ async function onRegenerate(index: number) {
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessage = error?.message ?? 'Something went wrong, please try again later.'
|
||||
const errorMessage = error?.message ?? t('common.wrong')
|
||||
|
||||
updateChat(
|
||||
+uuid,
|
||||
@@ -271,13 +273,12 @@ function handleDelete(index: number) {
|
||||
return
|
||||
|
||||
dialog.warning({
|
||||
title: 'Delete Message',
|
||||
content: 'Are you sure to delete this message?',
|
||||
positiveText: 'Yes',
|
||||
negativeText: 'No',
|
||||
title: t('chat.deleteMessage'),
|
||||
content: t('chat.deleteMessageConfirm'),
|
||||
positiveText: t('common.yes'),
|
||||
negativeText: t('common.no'),
|
||||
onPositiveClick: () => {
|
||||
chatStore.deleteChatByUuid(+uuid, index)
|
||||
ms.success('Message deleted successfully.')
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -287,10 +288,10 @@ function handleClear() {
|
||||
return
|
||||
|
||||
dialog.warning({
|
||||
title: 'Clear Chat',
|
||||
content: 'Are you sure to clear this chat?',
|
||||
positiveText: 'Yes',
|
||||
negativeText: 'No',
|
||||
title: t('chat.clearChat'),
|
||||
content: t('chat.clearChatConfirm'),
|
||||
positiveText: t('common.yes'),
|
||||
negativeText: t('common.no'),
|
||||
onPositiveClick: () => {
|
||||
chatStore.clearChatByUuid(+uuid)
|
||||
},
|
||||
@@ -315,8 +316,8 @@ function handleStop() {
|
||||
|
||||
const placeholder = computed(() => {
|
||||
if (isMobile.value)
|
||||
return 'Ask me anything...'
|
||||
return 'Ask me anything... (Shift + Enter = line break)'
|
||||
return t('chat.placeholderMobile')
|
||||
return t('chat.placeholder')
|
||||
})
|
||||
|
||||
const buttonDisabled = computed(() => {
|
||||
@@ -325,15 +326,14 @@ const buttonDisabled = computed(() => {
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['pt-14', 'pb-16']
|
||||
|
||||
return ['pt-14']
|
||||
return []
|
||||
})
|
||||
|
||||
const footerClass = computed(() => {
|
||||
let classes = ['p-4']
|
||||
if (isMobile.value)
|
||||
classes = ['p-2', 'pr-4', 'fixed', 'bottom-4', 'left-0', 'right-0', 'z-30', 'h-14', 'overflow-hidden']
|
||||
classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-4', 'h-14', 'overflow-hidden']
|
||||
return classes
|
||||
})
|
||||
|
||||
@@ -348,7 +348,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full" :class="wrapClass">
|
||||
<div class="flex flex-col w-full h-full" :class="wrapClass">
|
||||
<main class="flex-1 overflow-hidden">
|
||||
<div
|
||||
id="scrollRef"
|
||||
@@ -356,58 +356,62 @@ onUnmounted(() => {
|
||||
class="h-full overflow-hidden overflow-y-auto"
|
||||
:class="[isMobile ? 'p-2' : 'p-4']"
|
||||
>
|
||||
<template v-if="!dataSources.length">
|
||||
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
|
||||
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
|
||||
<span>Aha~</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<Message
|
||||
v-for="(item, index) of dataSources"
|
||||
:key="index"
|
||||
:date-time="item.dateTime"
|
||||
:text="item.text"
|
||||
:inversion="item.inversion"
|
||||
:error="item.error"
|
||||
:loading="item.loading"
|
||||
@regenerate="onRegenerate(index)"
|
||||
@delete="handleDelete(index)"
|
||||
/>
|
||||
<div class="sticky bottom-0 left-0 flex justify-center">
|
||||
<NButton v-if="loading" type="warning" @click="handleStop">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:stop-circle-line" />
|
||||
</template>
|
||||
Stop Responding
|
||||
</NButton>
|
||||
<div class="w-full max-w-screen-xl m-auto">
|
||||
<template v-if="!dataSources.length">
|
||||
<div class="flex items-center justify-center mt-4 text-center text-neutral-300">
|
||||
<SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
|
||||
<span>Aha~</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div>
|
||||
<Message
|
||||
v-for="(item, index) of dataSources"
|
||||
:key="index"
|
||||
:date-time="item.dateTime"
|
||||
:text="item.text"
|
||||
:inversion="item.inversion"
|
||||
:error="item.error"
|
||||
:loading="item.loading"
|
||||
@regenerate="onRegenerate(index)"
|
||||
@delete="handleDelete(index)"
|
||||
/>
|
||||
<div class="sticky bottom-0 left-0 flex justify-center">
|
||||
<NButton v-if="loading" type="warning" @click="handleStop">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:stop-circle-line" />
|
||||
</template>
|
||||
Stop Responding
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer :class="footerClass">
|
||||
<div class="flex items-center justify-between space-x-2">
|
||||
<HoverButton @click="handleClear">
|
||||
<span class="text-xl text-[#4f555e] dark:text-white">
|
||||
<SvgIcon icon="ri:delete-bin-line" />
|
||||
</span>
|
||||
</HoverButton>
|
||||
<NInput
|
||||
v-model:value="prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 1, maxRows: 2 }"
|
||||
:placeholder="placeholder"
|
||||
@keypress="handleEnter"
|
||||
/>
|
||||
<NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
|
||||
<template #icon>
|
||||
<span class="dark:text-black">
|
||||
<SvgIcon icon="ri:send-plane-fill" />
|
||||
<div class="w-full max-w-screen-xl m-auto">
|
||||
<div class="flex items-center justify-between space-x-2">
|
||||
<HoverButton @click="handleClear">
|
||||
<span class="text-xl text-[#4f555e] dark:text-white">
|
||||
<SvgIcon icon="ri:delete-bin-line" />
|
||||
</span>
|
||||
</template>
|
||||
</NButton>
|
||||
</HoverButton>
|
||||
<NInput
|
||||
v-model:value="prompt"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 1, maxRows: 2 }"
|
||||
:placeholder="placeholder"
|
||||
@keypress="handleEnter"
|
||||
/>
|
||||
<NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
|
||||
<template #icon>
|
||||
<span class="dark:text-black">
|
||||
<SvgIcon icon="ri:send-plane-fill" />
|
||||
</span>
|
||||
</template>
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ const show = ref(false)
|
||||
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800">
|
||||
<UserAvatar />
|
||||
|
||||
<HoverButton tooltip="Setting" @click="show = true">
|
||||
<HoverButton :tooltip="$t('setting.setting')" @click="show = true">
|
||||
<span class="text-xl text-[#4f555e] dark:text-white">
|
||||
<SvgIcon icon="ri:settings-4-line" />
|
||||
</span>
|
||||
|
||||
@@ -49,7 +49,7 @@ function isActive(uuid: number) {
|
||||
<template v-if="!dataSources.length">
|
||||
<div class="flex flex-col items-center mt-4 text-center text-neutral-300">
|
||||
<SvgIcon icon="ri:inbox-line" class="mb-2 text-3xl" />
|
||||
<span>No history</span>
|
||||
<span>{{ $t('common.noData') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -87,7 +87,7 @@ function isActive(uuid: number) {
|
||||
<SvgIcon icon="ri:delete-bin-line" />
|
||||
</button>
|
||||
</template>
|
||||
Are you sure to clear this history?
|
||||
{{ $t('chat.deleteHistoryConfirm') }}
|
||||
</NPopconfirm>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vite/client", "node", "naive-ui/volar", "web-bluetooth"]
|
||||
"types": ["vite/client", "node", "naive-ui/volar"]
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "service"]
|
||||
}
|
||||
|
||||