Merge branch 'main' into dev
7
.github/workflows/sync.yml
vendored
@ -31,3 +31,10 @@ jobs:
|
|||||||
|
|
||||||
# Set test_mode true to run tests instead of the true action!!
|
# Set test_mode true to run tests instead of the true action!!
|
||||||
test_mode: false
|
test_mode: false
|
||||||
|
|
||||||
|
- name: Sync check
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。"
|
||||||
|
echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]."
|
||||||
|
exit 1
|
||||||
|
57
README.md
@ -3,11 +3,15 @@
|
|||||||
|
|
||||||
<h1 align="center">ChatGPT Next Web</h1>
|
<h1 align="center">ChatGPT Next Web</h1>
|
||||||
|
|
||||||
|
English / [简体中文](./README_CN.md)
|
||||||
|
|
||||||
One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||||
|
|
||||||
一键免费部署你的私人 ChatGPT 网页应用。
|
一键免费部署你的私人 ChatGPT 网页应用。
|
||||||
|
|
||||||
[演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [QQ 群](https://user-images.githubusercontent.com/16968934/231095592-330adc52-0337-4c13-8452-938ec169e367.jpeg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
|
[Demo](https://chat-gpt-next-web.vercel.app/) / [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Join Discord](https://discord.gg/zrhvHCr79N) / [Buy Me a Coffee](https://www.buymeacoffee.com/yidadaa)
|
||||||
|
|
||||||
|
[演示](https://chat-gpt-next-web.vercel.app/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/231789746-41f34d05-6ef9-43f3-a1d1-ff109d4c3c14.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||||
|
|
||||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FYidadaa%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=chatgpt-next-web&repository-name=ChatGPT-Next-Web)
|
||||||
|
|
||||||
@ -22,7 +26,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
|||||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||||
- Privacy first, all data stored locally in the browser
|
- Privacy first, all data stored locally in the browser
|
||||||
- Responsive design, dark mode and PWA
|
- Responsive design, dark mode and PWA
|
||||||
- Fast first screen loading speed (~100kb)
|
- Fast first screen loading speed (~100kb), support streaming response
|
||||||
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
|
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
|
||||||
- Automatically compresses chat history to support long conversations while also saving your tokens
|
- Automatically compresses chat history to support long conversations while also saving your tokens
|
||||||
- One-click export all chat history with full Markdown support
|
- One-click export all chat history with full Markdown support
|
||||||
@ -31,7 +35,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
|||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
- [x] System Prompt: pin a user defined prompt as system prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||||
- [ ] User Prompt: user can edit and save custom prompts to prompt list
|
- [x] User Prompt: user can edit and save custom prompts to prompt list
|
||||||
|
- [ ] Prompt Template: create a new chat with pre-defined in-context prompts
|
||||||
|
- [ ] Share as image, share to ShareGPT
|
||||||
- [ ] Desktop App with tauri
|
- [ ] Desktop App with tauri
|
||||||
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
|
- [ ] Self-host Model: support llama, alpaca, ChatGLM, BELLE etc.
|
||||||
- [ ] Plugins: support network search, caculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
- [ ] Plugins: support network search, caculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
@ -45,7 +51,7 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
|||||||
|
|
||||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||||
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
|
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
|
||||||
- 极快的首屏加载速度(~100kb)
|
- 极快的首屏加载速度(~100kb),支持流式响应
|
||||||
- 隐私安全,所有数据保存在用户浏览器本地
|
- 隐私安全,所有数据保存在用户浏览器本地
|
||||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||||
@ -55,7 +61,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
|||||||
## 开发计划
|
## 开发计划
|
||||||
|
|
||||||
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
- [x] 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||||
- [ ] 允许用户自行编辑内置 Prompt 列表
|
- [x] 允许用户自行编辑内置 Prompt 列表
|
||||||
|
- [ ] 提示词模板:使用预制上下文快速定制新对话
|
||||||
|
- [ ] 分享为图片,分享到 ShareGPT
|
||||||
- [ ] 使用 tauri 打包桌面应用
|
- [ ] 使用 tauri 打包桌面应用
|
||||||
- [ ] 支持自部署的大语言模型
|
- [ ] 支持自部署的大语言模型
|
||||||
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
@ -76,7 +84,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
|||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
[简体中文 > 常见问题](./docs/faq-cn.md) | [English > FAQ](./docs/faq.en.md)
|
[简体中文 > 常见问题](./docs/faq-cn.md)
|
||||||
|
|
||||||
|
[English > FAQ](./docs/faq-en.md)
|
||||||
|
|
||||||
## Keep Updated
|
## Keep Updated
|
||||||
|
|
||||||
@ -86,12 +96,19 @@ If you have deployed your own project with just one click following the steps ab
|
|||||||
|
|
||||||
We recommend that you follow the steps below to re-deploy:
|
We recommend that you follow the steps below to re-deploy:
|
||||||
|
|
||||||
- Delete the original repo;
|
- Delete the original repository;
|
||||||
- Fork this project;
|
- Use the fork button in the upper right corner of the page to fork this project;
|
||||||
- Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy;
|
- Choose and deploy in Vercel again, [please see the detailed tutorial](./docs/vercel-cn.md).
|
||||||
- Please manually add an environment variable named `OPENAI_API_KEY` and enter your API key as the value during the redeploy process.
|
|
||||||
|
|
||||||
This project will be continuously updated, and after forking the project, the upstream code will be automatically synchronized every day without additional operations.
|
### Enable Automatic Updates
|
||||||
|
|
||||||
|
After forking the project, due to the limitations imposed by Github, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every hour:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Manually Updating Code
|
||||||
|
|
||||||
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
|
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
|
||||||
|
|
||||||
@ -123,18 +140,12 @@ Access passsword, separated by comma.
|
|||||||
|
|
||||||
### `BASE_URL` (optional)
|
### `BASE_URL` (optional)
|
||||||
|
|
||||||
> Default: `api.openai.com`
|
> Default: `https://api.openai.com`
|
||||||
|
|
||||||
|
> Examples: `http://your-openai-proxy.com`
|
||||||
|
|
||||||
Override openai api request base url.
|
Override openai api request base url.
|
||||||
|
|
||||||
### `PROTOCOL` (optional)
|
|
||||||
|
|
||||||
> Default: `https`
|
|
||||||
|
|
||||||
> Values: `http` | `https`
|
|
||||||
|
|
||||||
Override openai api request protocol.
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||||
@ -202,6 +213,8 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
|||||||
|
|
||||||
### Sponsor
|
### Sponsor
|
||||||
|
|
||||||
|
> 仅列出捐赠金额 >= 100RMB 的用户。
|
||||||
|
|
||||||
[@mushan0x0](https://github.com/mushan0x0)
|
[@mushan0x0](https://github.com/mushan0x0)
|
||||||
[@ClarenceDan](https://github.com/ClarenceDan)
|
[@ClarenceDan](https://github.com/ClarenceDan)
|
||||||
[@zhangjia](https://github.com/zhangjia)
|
[@zhangjia](https://github.com/zhangjia)
|
||||||
@ -211,6 +224,10 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
|||||||
[@webees](https://github.com/webees)
|
[@webees](https://github.com/webees)
|
||||||
[@chazzhou](https://github.com/chazzhou)
|
[@chazzhou](https://github.com/chazzhou)
|
||||||
[@hauy](https://github.com/hauy)
|
[@hauy](https://github.com/hauy)
|
||||||
|
[@Corwin006](https://github.com/Corwin006)
|
||||||
|
[@yankunsong](https://github.com/yankunsong)
|
||||||
|
[@ypwhs](https://github.com/ypwhs)
|
||||||
|
[@fxxxchao](https://github.com/fxxxchao)
|
||||||
|
|
||||||
### Contributor
|
### Contributor
|
||||||
|
|
||||||
|
39
README_CN.md
@ -38,12 +38,19 @@
|
|||||||
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
|
如果你按照上述步骤一键部署了自己的项目,可能会发现总是提示“存在更新”的问题,这是由于 Vercel 会默认为你创建一个新项目而不是 fork 本项目,这会导致无法正确地检测更新。
|
||||||
推荐你按照下列步骤重新部署:
|
推荐你按照下列步骤重新部署:
|
||||||
|
|
||||||
- 删除掉原先的 repo;
|
- 删除掉原先的仓库;
|
||||||
- fork 本项目;
|
- 使用页面右上角的 fork 按钮,fork 本项目;
|
||||||
- 前往 vercel 控制台,删除掉原先的 project,然后新建 project,选择你刚刚 fork 出来的项目重新进行部署即可;
|
- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
|
||||||
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
|
|
||||||
|
|
||||||
本项目会持续更新,当你 Fork 项目之后,默认会每天自动同步上游代码,无需额外操作。
|
### 打开自动更新
|
||||||
|
|
||||||
|
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 手动更新代码
|
||||||
|
|
||||||
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
|
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
|
||||||
|
|
||||||
@ -79,17 +86,13 @@ OpanAI 密钥,你在 openai 账户页面申请的 api key。
|
|||||||
|
|
||||||
### `BASE_URL` (可选)
|
### `BASE_URL` (可选)
|
||||||
|
|
||||||
> Default: `api.openai.com`
|
> Default: `https://api.openai.com`
|
||||||
|
|
||||||
|
> Examples: `http://your-openai-proxy.com`
|
||||||
|
|
||||||
OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填写此选项。
|
OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填写此选项。
|
||||||
|
|
||||||
### `PROTOCOL` (可选)
|
> 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
|
||||||
|
|
||||||
> Default: `https`
|
|
||||||
|
|
||||||
> Values: `http` | `https`
|
|
||||||
|
|
||||||
OpenAI 代理接口协议,如果遇到 ssl 证书问题,请尝试通过此选项设置为 http。
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
@ -131,7 +134,8 @@ docker run -d -p 3000:3000 \
|
|||||||
docker run -d -p 3000:3000 \
|
docker run -d -p 3000:3000 \
|
||||||
-e OPENAI_API_KEY="sk-xxxx" \
|
-e OPENAI_API_KEY="sk-xxxx" \
|
||||||
-e CODE="页面访问密码" \
|
-e CODE="页面访问密码" \
|
||||||
-e PROXY_URL="http://localhost:7890" \
|
--net=host \
|
||||||
|
-e PROXY_URL="http://127.0.0.1:7890" \
|
||||||
yidadaa/chatgpt-next-web
|
yidadaa/chatgpt-next-web
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -147,12 +151,7 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
|||||||
|
|
||||||
### 捐赠者
|
### 捐赠者
|
||||||
|
|
||||||
> 仅列出了部分大额打赏,小额打赏(< 100RMB)人数太多,在此不再列出,敬请谅解。
|
> 见英文版。
|
||||||
|
|
||||||
[@mushan0x0](https://github.com/mushan0x0)
|
|
||||||
[@ClarenceDan](https://github.com/ClarenceDan)
|
|
||||||
[@zhangjia](https://github.com/zhangjia)
|
|
||||||
[@hoochanlon](https://github.com/hoochanlon)
|
|
||||||
|
|
||||||
### 贡献者
|
### 贡献者
|
||||||
|
|
||||||
|
@ -9,10 +9,17 @@ export async function requestOpenai(req: NextRequest) {
|
|||||||
const apiKey = req.headers.get("token");
|
const apiKey = req.headers.get("token");
|
||||||
const openaiPath = req.headers.get("path");
|
const openaiPath = req.headers.get("path");
|
||||||
|
|
||||||
|
let baseUrl = BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `${PROTOCOL}://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[Proxy] ", openaiPath);
|
console.log("[Proxy] ", openaiPath);
|
||||||
console.log("[apiKey] ", apiKey);
|
console.log("[apiKey] ", apiKey);
|
||||||
|
console.log("[Base Url]", baseUrl);
|
||||||
|
|
||||||
return fetch(`${PROTOCOL}://${BASE_URL}/${openaiPath}`, {
|
return fetch(`${baseUrl}/${openaiPath}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
@ -17,7 +17,7 @@ async function makeRequest(req: NextRequest) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -29,3 +29,7 @@ export async function POST(req: NextRequest) {
|
|||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
return makeRequest(req);
|
return makeRequest(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
runtime: "edge",
|
||||||
|
};
|
||||||
|
@ -1,5 +1,29 @@
|
|||||||
@import "../styles/animation.scss";
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
.chat-input-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.chat-input-action {
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
border: var(--border-in-light);
|
||||||
|
padding: 4px 10px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
transition: all ease 0.3s;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-toast {
|
.prompt-toast {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -50px;
|
bottom: -50px;
|
||||||
|
@ -3,6 +3,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
|
|||||||
|
|
||||||
import SendWhiteIcon from "../icons/send-white.svg";
|
import SendWhiteIcon from "../icons/send-white.svg";
|
||||||
import BrainIcon from "../icons/brain.svg";
|
import BrainIcon from "../icons/brain.svg";
|
||||||
|
import RenameIcon from "../icons/rename.svg";
|
||||||
import ExportIcon from "../icons/share.svg";
|
import ExportIcon from "../icons/share.svg";
|
||||||
import MenuIcon from "../icons/menu.svg";
|
import MenuIcon from "../icons/menu.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
@ -14,6 +15,12 @@ import DeleteIcon from "../icons/delete.svg";
|
|||||||
import MaxIcon from "../icons/max.svg";
|
import MaxIcon from "../icons/max.svg";
|
||||||
import MinIcon from "../icons/min.svg";
|
import MinIcon from "../icons/min.svg";
|
||||||
|
|
||||||
|
import LightIcon from "../icons/light.svg";
|
||||||
|
import DarkIcon from "../icons/dark.svg";
|
||||||
|
import AutoIcon from "../icons/auto.svg";
|
||||||
|
import BottomIcon from "../icons/bottom.svg";
|
||||||
|
import StopIcon from "../icons/pause.svg";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
SubmitKey,
|
SubmitKey,
|
||||||
@ -22,6 +29,7 @@ import {
|
|||||||
ROLES,
|
ROLES,
|
||||||
createMessage,
|
createMessage,
|
||||||
useAccessStore,
|
useAccessStore,
|
||||||
|
Theme,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -60,7 +68,11 @@ export function Avatar(props: { role: Message["role"] }) {
|
|||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
if (props.role !== "user") {
|
if (props.role !== "user") {
|
||||||
return <BotIcon className={styles["user-avtar"]} />;
|
return (
|
||||||
|
<div className="no-dark">
|
||||||
|
<BotIcon className={styles["user-avtar"]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -316,22 +328,90 @@ function useScrollToBottom() {
|
|||||||
// for auto-scroll
|
// for auto-scroll
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const dom = scrollRef.current;
|
||||||
|
if (dom) {
|
||||||
|
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// auto scroll
|
// auto scroll
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const dom = scrollRef.current;
|
autoScroll && scrollToBottom();
|
||||||
if (dom && autoScroll) {
|
|
||||||
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollRef,
|
scrollRef,
|
||||||
autoScroll,
|
autoScroll,
|
||||||
setAutoScroll,
|
setAutoScroll,
|
||||||
|
scrollToBottom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ChatActions(props: {
|
||||||
|
showPromptModal: () => void;
|
||||||
|
scrollToBottom: () => void;
|
||||||
|
hitBottom: boolean;
|
||||||
|
}) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
// switch themes
|
||||||
|
const theme = chatStore.config.theme;
|
||||||
|
function nextTheme() {
|
||||||
|
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
||||||
|
const themeIndex = themes.indexOf(theme);
|
||||||
|
const nextIndex = (themeIndex + 1) % themes.length;
|
||||||
|
const nextTheme = themes[nextIndex];
|
||||||
|
chatStore.updateConfig((config) => (config.theme = nextTheme));
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop all responses
|
||||||
|
const couldStop = ControllerPool.hasPending();
|
||||||
|
const stopAll = () => ControllerPool.stopAll();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={chatStyle["chat-input-actions"]}>
|
||||||
|
{couldStop && (
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={stopAll}
|
||||||
|
>
|
||||||
|
<StopIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!props.hitBottom && (
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={props.scrollToBottom}
|
||||||
|
>
|
||||||
|
<BottomIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.hitBottom && (
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={props.showPromptModal}
|
||||||
|
>
|
||||||
|
<BrainIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`${chatStyle["chat-input-action"]} clickable`}
|
||||||
|
onClick={nextTheme}
|
||||||
|
>
|
||||||
|
{theme === Theme.Auto ? (
|
||||||
|
<AutoIcon />
|
||||||
|
) : theme === Theme.Light ? (
|
||||||
|
<LightIcon />
|
||||||
|
) : theme === Theme.Dark ? (
|
||||||
|
<DarkIcon />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Chat(props: {
|
export function Chat(props: {
|
||||||
showSideBar?: () => void;
|
showSideBar?: () => void;
|
||||||
sideBarShowing?: boolean;
|
sideBarShowing?: boolean;
|
||||||
@ -357,11 +437,10 @@ export function Chat(props: {
|
|||||||
const [beforeInput, setBeforeInput] = useState("");
|
const [beforeInput, setBeforeInput] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||||
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||||
const [hitBottom, setHitBottom] = useState(false);
|
const [hitBottom, setHitBottom] = useState(false);
|
||||||
|
|
||||||
const onChatBodyScroll = (e: HTMLElement) => {
|
const onChatBodyScroll = (e: HTMLElement) => {
|
||||||
setAutoScroll(false);
|
|
||||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||||
setHitBottom(isTouchBottom);
|
setHitBottom(isTouchBottom);
|
||||||
};
|
};
|
||||||
@ -383,16 +462,6 @@ export function Chat(props: {
|
|||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollInput = () => {
|
|
||||||
const dom = inputRef.current;
|
|
||||||
if (!dom) return;
|
|
||||||
const paddingBottomNum: number = parseInt(
|
|
||||||
window.getComputedStyle(dom).paddingBottom,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
|
||||||
};
|
|
||||||
|
|
||||||
// auto grow input
|
// auto grow input
|
||||||
const [inputRows, setInputRows] = useState(2);
|
const [inputRows, setInputRows] = useState(2);
|
||||||
const measure = useDebouncedCallback(
|
const measure = useDebouncedCallback(
|
||||||
@ -417,7 +486,6 @@ export function Chat(props: {
|
|||||||
// only search prompts when user input is short
|
// only search prompts when user input is short
|
||||||
const SEARCH_TEXT_LIMIT = 30;
|
const SEARCH_TEXT_LIMIT = 30;
|
||||||
const onInput = (text: string) => {
|
const onInput = (text: string) => {
|
||||||
scrollInput();
|
|
||||||
setUserInput(text);
|
setUserInput(text);
|
||||||
const n = text.trim().length;
|
const n = text.trim().length;
|
||||||
|
|
||||||
@ -475,21 +543,44 @@ export function Chat(props: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResend = (botIndex: number) => {
|
const findLastUesrIndex = (messageId: number) => {
|
||||||
// find last user input message and resend
|
// find last user input message and resend
|
||||||
for (let i = botIndex; i >= 0; i -= 1) {
|
let lastUserMessageIndex: number | null = null;
|
||||||
if (messages[i].role === "user") {
|
for (let i = 0; i < session.messages.length; i += 1) {
|
||||||
setIsLoading(true);
|
const message = session.messages[i];
|
||||||
chatStore
|
if (message.id === messageId) {
|
||||||
.onUserInput(messages[i].content)
|
break;
|
||||||
.then(() => setIsLoading(false));
|
}
|
||||||
chatStore.updateCurrentSession((session) =>
|
if (message.role === "user") {
|
||||||
session.messages.splice(i, 2),
|
lastUserMessageIndex = i;
|
||||||
);
|
|
||||||
inputRef.current?.focus();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return lastUserMessageIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessage = (userIndex: number) => {
|
||||||
|
chatStore.updateCurrentSession((session) =>
|
||||||
|
session.messages.splice(userIndex, 2),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = (botMessageId: number) => {
|
||||||
|
const userIndex = findLastUesrIndex(botMessageId);
|
||||||
|
if (userIndex === null) return;
|
||||||
|
deleteMessage(userIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResend = (botMessageId: number) => {
|
||||||
|
// find last user input message and resend
|
||||||
|
const userIndex = findLastUesrIndex(botMessageId);
|
||||||
|
if (userIndex === null) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const content = session.messages[userIndex].content;
|
||||||
|
deleteMessage(userIndex);
|
||||||
|
chatStore.onUserInput(content).then(() => setIsLoading(false));
|
||||||
|
inputRef.current?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
@ -541,6 +632,13 @@ export function Chat(props: {
|
|||||||
|
|
||||||
const [showPromptModal, setShowPromptModal] = useState(false);
|
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||||
|
|
||||||
|
const renameSession = () => {
|
||||||
|
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||||
|
if (newTopic && newTopic !== session.topic) {
|
||||||
|
chatStore.updateCurrentSession((session) => (session.topic = newTopic!));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Auto focus
|
// Auto focus
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.sideBarShowing && isMobileScreen()) return;
|
if (props.sideBarShowing && isMobileScreen()) return;
|
||||||
@ -554,14 +652,7 @@ export function Chat(props: {
|
|||||||
<div className={styles["window-header-title"]}>
|
<div className={styles["window-header-title"]}>
|
||||||
<div
|
<div
|
||||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
onClickCapture={() => {
|
onClickCapture={renameSession}
|
||||||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
|
||||||
if (newTopic && newTopic !== session.topic) {
|
|
||||||
chatStore.updateCurrentSession(
|
|
||||||
(session) => (session.topic = newTopic!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{session.topic}
|
{session.topic}
|
||||||
</div>
|
</div>
|
||||||
@ -578,6 +669,7 @@ export function Chat(props: {
|
|||||||
onClick={props?.showSideBar}
|
onClick={props?.showSideBar}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isMobileScreen() && (
|
{!isMobileScreen() && (
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@ -590,6 +682,15 @@ export function Chat(props: {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<RenameIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={renameSession}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ExportIcon />}
|
icon={<ExportIcon />}
|
||||||
@ -678,12 +779,20 @@ export function Chat(props: {
|
|||||||
{Locale.Chat.Actions.Stop}
|
{Locale.Chat.Actions.Stop}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<>
|
||||||
className={styles["chat-message-top-action"]}
|
<div
|
||||||
onClick={() => onResend(i)}
|
className={styles["chat-message-top-action"]}
|
||||||
>
|
onClick={() => onDelete(message.id ?? i)}
|
||||||
{Locale.Chat.Actions.Retry}
|
>
|
||||||
</div>
|
{Locale.Chat.Actions.Delete}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onResend(message.id ?? i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Retry}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -694,22 +803,20 @@ export function Chat(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(message.preview || message.content.length === 0) &&
|
<Markdown
|
||||||
!isUser ? (
|
content={message.content}
|
||||||
<LoadingIcon />
|
loading={
|
||||||
) : (
|
(message.preview || message.content.length === 0) &&
|
||||||
<div
|
!isUser
|
||||||
className="markdown-body"
|
}
|
||||||
style={{ fontSize: `${fontSize}px` }}
|
onContextMenu={(e) => onRightClick(e, message)}
|
||||||
onContextMenu={(e) => onRightClick(e, message)}
|
onDoubleClickCapture={() => {
|
||||||
onDoubleClickCapture={() => {
|
if (!isMobileScreen()) return;
|
||||||
if (!isMobileScreen()) return;
|
setUserInput(message.content);
|
||||||
setUserInput(message.content);
|
}}
|
||||||
}}
|
fontSize={fontSize}
|
||||||
>
|
parentRef={scrollRef}
|
||||||
<Markdown content={message.content} />
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!isUser && !message.preview && (
|
{!isUser && !message.preview && (
|
||||||
<div className={styles["chat-message-actions"]}>
|
<div className={styles["chat-message-actions"]}>
|
||||||
@ -726,6 +833,12 @@ export function Chat(props: {
|
|||||||
|
|
||||||
<div className={styles["chat-input-panel"]}>
|
<div className={styles["chat-input-panel"]}>
|
||||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||||
|
|
||||||
|
<ChatActions
|
||||||
|
showPromptModal={() => setShowPromptModal(true)}
|
||||||
|
scrollToBottom={scrollToBottom}
|
||||||
|
hitBottom={hitBottom}
|
||||||
|
/>
|
||||||
<div className={styles["chat-input-panel-inner"]}>
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
max-height: var(--full-height);
|
max-height: var(--full-height);
|
||||||
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,6 +231,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
padding-bottom: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,11 +356,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 5px;
|
padding-top: 10px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
border-top: var(--border-in-light);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin single-line {
|
@mixin single-line {
|
||||||
|
@ -17,7 +17,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
|||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
|
||||||
import { useChatStore } from "../store";
|
import { useChatStore } from "../store";
|
||||||
import { isMobileScreen } from "../utils";
|
import { getCSSVar, isMobileScreen } from "../utils";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { Chat } from "./chat";
|
import { Chat } from "./chat";
|
||||||
|
|
||||||
@ -66,9 +66,7 @@ function useSwitchTheme() {
|
|||||||
metaDescriptionDark?.setAttribute("content", "#151515");
|
metaDescriptionDark?.setAttribute("content", "#151515");
|
||||||
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
||||||
} else {
|
} else {
|
||||||
const themeColor = getComputedStyle(document.body)
|
const themeColor = getCSSVar("--themeColor");
|
||||||
.getPropertyValue("--theme-color")
|
|
||||||
.trim();
|
|
||||||
metaDescriptionDark?.setAttribute("content", themeColor);
|
metaDescriptionDark?.setAttribute("content", themeColor);
|
||||||
metaDescriptionLight?.setAttribute("content", themeColor);
|
metaDescriptionLight?.setAttribute("content", themeColor);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import RehypeHighlight from "rehype-highlight";
|
|||||||
import { useRef, useState, RefObject, useEffect } from "react";
|
import { useRef, useState, RefObject, useEffect } from "react";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
|
|
||||||
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
|
|
||||||
export function PreCode(props: { children: any }) {
|
export function PreCode(props: { children: any }) {
|
||||||
const ref = useRef<HTMLPreElement>(null);
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
|
|
||||||
@ -27,49 +29,78 @@ export function PreCode(props: { children: any }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const useLazyLoad = (ref: RefObject<Element>): boolean => {
|
export function Markdown(
|
||||||
const [isIntersecting, setIntersecting] = useState<boolean>(false);
|
props: {
|
||||||
|
content: string;
|
||||||
|
loading?: boolean;
|
||||||
|
fontSize?: number;
|
||||||
|
parentRef: RefObject<HTMLDivElement>;
|
||||||
|
} & React.DOMAttributes<HTMLDivElement>,
|
||||||
|
) {
|
||||||
|
const mdRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const parent = props.parentRef.current;
|
||||||
|
const md = mdRef.current;
|
||||||
|
const rendered = useRef(true); // disable lazy loading for bad ux
|
||||||
|
const [counter, setCounter] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(([entry]) => {
|
// to triggr rerender
|
||||||
if (entry.isIntersecting) {
|
setCounter(counter + 1);
|
||||||
setIntersecting(true);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
observer.disconnect();
|
}, [props.loading]);
|
||||||
|
|
||||||
|
const inView =
|
||||||
|
rendered.current ||
|
||||||
|
(() => {
|
||||||
|
if (parent && md) {
|
||||||
|
const parentBounds = parent.getBoundingClientRect();
|
||||||
|
const mdBounds = md.getBoundingClientRect();
|
||||||
|
const isInRange = (x: number) =>
|
||||||
|
x <= parentBounds.bottom && x >= parentBounds.top;
|
||||||
|
const inView = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
|
||||||
|
|
||||||
|
if (inView) {
|
||||||
|
rendered.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inView;
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
|
|
||||||
if (ref.current) {
|
const shouldLoading = props.loading || !inView;
|
||||||
observer.observe(ref.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
}, [ref]);
|
|
||||||
|
|
||||||
return isIntersecting;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Markdown(props: { content: string }) {
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<div
|
||||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
className="markdown-body"
|
||||||
rehypePlugins={[
|
style={{ fontSize: `${props.fontSize ?? 14}px` }}
|
||||||
RehypeKatex,
|
ref={mdRef}
|
||||||
[
|
onContextMenu={props.onContextMenu}
|
||||||
RehypeHighlight,
|
onDoubleClickCapture={props.onDoubleClickCapture}
|
||||||
{
|
|
||||||
detect: false,
|
|
||||||
ignoreMissing: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
]}
|
|
||||||
components={{
|
|
||||||
pre: PreCode,
|
|
||||||
}}
|
|
||||||
linkTarget={"_blank"}
|
|
||||||
>
|
>
|
||||||
{props.content}
|
{shouldLoading ? (
|
||||||
</ReactMarkdown>
|
<LoadingIcon />
|
||||||
|
) : (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||||
|
rehypePlugins={[
|
||||||
|
RehypeKatex,
|
||||||
|
[
|
||||||
|
RehypeHighlight,
|
||||||
|
{
|
||||||
|
detect: false,
|
||||||
|
ignoreMissing: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}
|
||||||
|
components={{
|
||||||
|
pre: PreCode,
|
||||||
|
}}
|
||||||
|
linkTarget={"_blank"}
|
||||||
|
>
|
||||||
|
{props.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,3 +32,63 @@
|
|||||||
min-width: 80%;
|
min-width: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-prompt-modal {
|
||||||
|
min-height: 40vh;
|
||||||
|
|
||||||
|
.user-prompt-search {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: var(--gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-prompt-list {
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.user-prompt-item {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
widows: 100%;
|
||||||
|
|
||||||
|
.user-prompt-header {
|
||||||
|
display: flex;
|
||||||
|
widows: 100%;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.user-prompt-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-right: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-prompt-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.user-prompt-button {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-prompt-content {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-prompt-actions {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo, HTMLProps } from "react";
|
import { useState, useEffect, useMemo, HTMLProps, useRef } from "react";
|
||||||
|
|
||||||
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
|
import EmojiPicker, { Theme as EmojiTheme } from "emoji-picker-react";
|
||||||
|
|
||||||
@ -6,12 +6,13 @@ import styles from "./settings.module.scss";
|
|||||||
|
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import CopyIcon from "../icons/copy.svg";
|
||||||
import ClearIcon from "../icons/clear.svg";
|
import ClearIcon from "../icons/clear.svg";
|
||||||
import EditIcon from "../icons/edit.svg";
|
import EditIcon from "../icons/edit.svg";
|
||||||
import EyeIcon from "../icons/eye.svg";
|
import EyeIcon from "../icons/eye.svg";
|
||||||
import EyeOffIcon from "../icons/eye-off.svg";
|
import EyeOffIcon from "../icons/eye-off.svg";
|
||||||
|
|
||||||
import { List, ListItem, Popover, showToast } from "./ui-lib";
|
import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
|
||||||
|
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import {
|
import {
|
||||||
@ -26,14 +27,114 @@ import {
|
|||||||
import { Avatar } from "./chat";
|
import { Avatar } from "./chat";
|
||||||
|
|
||||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||||
import { getEmojiUrl } from "../utils";
|
import { copyToClipboard, getEmojiUrl } from "../utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { UPDATE_URL } from "../constant";
|
import { UPDATE_URL } from "../constant";
|
||||||
import { SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { requestUsage } from "../requests";
|
|
||||||
import { ErrorBoundary } from "./error";
|
import { ErrorBoundary } from "./error";
|
||||||
import { InputRange } from "./input-range";
|
import { InputRange } from "./input-range";
|
||||||
|
|
||||||
|
function UserPromptModal(props: { onClose?: () => void }) {
|
||||||
|
const promptStore = usePromptStore();
|
||||||
|
const userPrompts = promptStore.getUserPrompts();
|
||||||
|
const builtinPrompts = SearchService.builtinPrompts;
|
||||||
|
const allPrompts = userPrompts.concat(builtinPrompts);
|
||||||
|
const [searchInput, setSearchInput] = useState("");
|
||||||
|
const [searchPrompts, setSearchPrompts] = useState<Prompt[]>([]);
|
||||||
|
const prompts = searchInput.length > 0 ? searchPrompts : allPrompts;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchInput.length > 0) {
|
||||||
|
const searchResult = SearchService.search(searchInput);
|
||||||
|
setSearchPrompts(searchResult);
|
||||||
|
} else {
|
||||||
|
setSearchPrompts([]);
|
||||||
|
}
|
||||||
|
}, [searchInput]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Settings.Prompt.Modal.Title}
|
||||||
|
onClose={() => props.onClose?.()}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="add"
|
||||||
|
onClick={() => promptStore.add({ title: "", content: "" })}
|
||||||
|
icon={<ClearIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Settings.Prompt.Modal.Add}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className={styles["user-prompt-modal"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles["user-prompt-search"]}
|
||||||
|
placeholder={Locale.Settings.Prompt.Modal.Search}
|
||||||
|
value={searchInput}
|
||||||
|
onInput={(e) => setSearchInput(e.currentTarget.value)}
|
||||||
|
></input>
|
||||||
|
|
||||||
|
<div className={styles["user-prompt-list"]}>
|
||||||
|
{prompts.map((v, _) => (
|
||||||
|
<div className={styles["user-prompt-item"]} key={v.id ?? v.title}>
|
||||||
|
<div className={styles["user-prompt-header"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles["user-prompt-title"]}
|
||||||
|
value={v.title}
|
||||||
|
readOnly={!v.isUser}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (v.isUser) {
|
||||||
|
promptStore.updateUserPrompts(
|
||||||
|
v.id!,
|
||||||
|
(prompt) => (prompt.title = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
|
||||||
|
<div className={styles["user-prompt-buttons"]}>
|
||||||
|
{v.isUser && (
|
||||||
|
<IconButton
|
||||||
|
icon={<ClearIcon />}
|
||||||
|
bordered
|
||||||
|
className={styles["user-prompt-button"]}
|
||||||
|
onClick={() => promptStore.remove(v.id!)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
className={styles["user-prompt-button"]}
|
||||||
|
onClick={() => copyToClipboard(v.content)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
rows={2}
|
||||||
|
value={v.content}
|
||||||
|
className={styles["user-prompt-content"]}
|
||||||
|
readOnly={!v.isUser}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (v.isUser) {
|
||||||
|
promptStore.updateUserPrompts(
|
||||||
|
v.id!,
|
||||||
|
(prompt) => (prompt.content = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SettingItem(props: {
|
function SettingItem(props: {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
@ -99,18 +200,16 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [usage, setUsage] = useState<{
|
const usage = {
|
||||||
used?: number;
|
used: updateStore.used,
|
||||||
subscription?: number;
|
subscription: updateStore.subscription,
|
||||||
}>();
|
};
|
||||||
const [loadingUsage, setLoadingUsage] = useState(false);
|
const [loadingUsage, setLoadingUsage] = useState(false);
|
||||||
function checkUsage() {
|
function checkUsage() {
|
||||||
setLoadingUsage(true);
|
setLoadingUsage(true);
|
||||||
requestUsage()
|
updateStore.updateUsage().finally(() => {
|
||||||
.then((res) => setUsage(res))
|
setLoadingUsage(false);
|
||||||
.finally(() => {
|
});
|
||||||
setLoadingUsage(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
@ -122,10 +221,12 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
|
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
const builtinCount = SearchService.count.builtin;
|
const builtinCount = SearchService.count.builtin;
|
||||||
const customCount = promptStore.prompts.size ?? 0;
|
const customCount = promptStore.getUserPrompts().length ?? 0;
|
||||||
|
const [shouldShowPromptModal, setShowPromptModal] = useState(false);
|
||||||
|
|
||||||
const showUsage = accessStore.isAuthorized();
|
const showUsage = accessStore.isAuthorized();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// checks per minutes
|
||||||
checkUpdate();
|
checkUpdate();
|
||||||
showUsage && checkUsage();
|
showUsage && checkUsage();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@ -470,7 +571,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<EditIcon />}
|
icon={<EditIcon />}
|
||||||
text={Locale.Settings.Prompt.Edit}
|
text={Locale.Settings.Prompt.Edit}
|
||||||
onClick={() => showToast(Locale.WIP)}
|
onClick={() => setShowPromptModal(true)}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</List>
|
</List>
|
||||||
@ -556,6 +657,10 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
></InputRange>
|
></InputRange>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
{shouldShowPromptModal && (
|
||||||
|
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
width: 50vw;
|
width: 60vw;
|
||||||
animation: slide-in ease 0.3s;
|
animation: slide-in ease 0.3s;
|
||||||
|
|
||||||
--modal-padding: 20px;
|
--modal-padding: 20px;
|
||||||
|
11
app/global.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
declare module "*.jpg";
|
||||||
|
declare module "*.png";
|
||||||
|
declare module "*.woff2";
|
||||||
|
declare module "*.woff";
|
||||||
|
declare module "*.ttf";
|
||||||
|
declare module "*.scss" {
|
||||||
|
const content: Record<string, string>;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.svg";
|
1
app/icons/auto.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="分组 1" style="stroke:#333333; stroke-width:1; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.333333333333333) rotate(0 2.333750009536743 2.6666666666666665)" d="M0 5.33667L0.73 3.66667 M4.6675 5.33667L3.9375 3.66667 M0.729167 3.67L2.32917 0L3.93917 3.67 M0.729167 3.66667L3.93917 3.66667 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.533316666666666 2.6666666666666665)" d="M13.07,5.33C12.45,2.29 9.76,0 6.53,0C3.31,0 0.62,2.29 0,5.33L2,4.67 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 9.333333333333332) rotate(0 6.533316666666666 2.6666666666666665)" d="M0,0C0.62,3.04 3.31,5.33 6.53,5.33C9.76,5.33 12.45,3.04 13.07,0L11.33,0.67 " /></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
app/icons/bottom.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 4) rotate(0 4 2)" d="M8,0L4,4L0,0 " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 8) rotate(0 4 2)" d="M8,0L4,4L0,0 " /></g></g></svg>
|
After Width: | Height: | Size: 736 B |
1
app/icons/dark.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.333333333333485) rotate(0 6.666666666666666 6.666666666666666)" d="M6.67,0L4.91,1.76L1.76,1.76L1.76,4.91L0,6.67L1.76,8.42L1.76,11.58L4.91,11.58L6.67,13.33L8.42,11.58L11.58,11.58L11.58,8.42L13.33,6.67L11.58,4.91L11.58,1.76L8.42,1.76L6.67,0Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(5.666666666666666 5.44771525016904) rotate(0 2.4732087011352872 2.442809041582063)" d="M4,0.55C2.17,-0.78 0,0.55 0,1.89C1.67,1.89 3.33,2.22 3.33,4.89C4.67,4.89 5.83,1.89 4,0.55Z " /></g></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
app/icons/light.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4.340166666666667 4.21550000000002) rotate(0 3.6666666666666665 3.666666666666666)" d="M0,3.67C0,5.69 1.64,7.33 3.67,7.33C5.69,7.33 7.33,5.69 7.33,3.67C7.33,1.64 5.69,0 3.67,0C1.64,0 0,1.64 0,3.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.166666666666666 12.1719333333333) rotate(0 0.4100499999999994 0.41240499999999997)" d="M0.82,0.82L0,0 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.0068366666666666 3.0654333333332033) rotate(0 0.3411483333333332 0.34309999999999974)" d="M0.68,0.69L0,0 " /><path id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 1.2155666666667457) rotate(0 0 0.5)" d="M0,1L0,0 " /><path id="路径 5" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(13.333266666666667 8.21550000000002) rotate(0 0.6666666666666666 0)" d="M1.33,0L0,0 " /><path id="路径 6" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(12.5108 3.065499999999929) rotate(0 0.41123333333333295 0.41123333333333295)" d="M0,0.82L0.82,0 " /><path id="路径 7" fill-rule="evenodd" style="fill:#333333" transform="translate(5.673499999999999 5.5488333333332776) rotate(0 1.1666666666666665 2.333333333333333)" opacity="1" d="M2.33,0C1.04,0 0,1.04 0,2.33C0,3.62 1.04,4.67 2.33,4.67L2.33,0Z " /><path id="路径 8" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333966666666666 7.8821666666667625) rotate(0 0.6666666666666666 0)" d="M0,0L1.33,0 " /><path id="路径 9" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(3.348133333333333 12.3125) rotate(0 0.3421333333333335 0.3421266666666665)" d="M0,0.68L0.68,0 " /><path id="路径 10" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(8 13.548763333333454) rotate(0 0 0.6666666666666666)" d="M0,1.33L0,0 " /></g></g></svg>
|
After Width: | Height: | Size: 2.7 KiB |
1
app/icons/pause.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)" d="M13.33,6.67C13.33,2.98 10.35,0 6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.333333333333333 6) rotate(0 0 2)" d="M0,0L0,4 " /><path id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.666666666666666 6) rotate(0 0 2)" d="M0,0L0,4 " /></g></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
1
app/icons/rename.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="white"><use xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(1.774903333333333 1.3006199999999999) rotate(0 6.599664999999999 6.599656666666666)" d="M2.83,13.2L13.2,2.83L10.37,0L0,10.37L0,13.2L2.83,13.2Z " /><path id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.317366666666667 4.129066666666667) rotate(0 1.4142166666666658 1.4142166666666665)" d="M0,0L2.83,2.83 " /></g></g></svg>
|
After Width: | Height: | Size: 911 B |
@ -18,6 +18,7 @@ const cn = {
|
|||||||
Copy: "复制",
|
Copy: "复制",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "重试",
|
Retry: "重试",
|
||||||
|
Delete: "删除",
|
||||||
},
|
},
|
||||||
Rename: "重命名对话",
|
Rename: "重命名对话",
|
||||||
Typing: "正在输入…",
|
Typing: "正在输入…",
|
||||||
@ -62,10 +63,10 @@ const cn = {
|
|||||||
ResetAll: "重置所有选项",
|
ResetAll: "重置所有选项",
|
||||||
Close: "关闭",
|
Close: "关闭",
|
||||||
ConfirmResetAll: {
|
ConfirmResetAll: {
|
||||||
Confirm: "Are you sure you want to reset all configurations?",
|
Confirm: "确认清除所有配置?",
|
||||||
},
|
},
|
||||||
ConfirmClearAll: {
|
ConfirmClearAll: {
|
||||||
Confirm: "Are you sure you want to reset all chat?",
|
Confirm: "确认清除所有聊天记录?",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Lang: {
|
Lang: {
|
||||||
@ -78,6 +79,7 @@ const cn = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "头像",
|
Avatar: "头像",
|
||||||
@ -107,6 +109,11 @@ const cn = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`内置 ${builtin} 条,用户定义 ${custom} 条`,
|
`内置 ${builtin} 条,用户定义 ${custom} 条`,
|
||||||
Edit: "编辑",
|
Edit: "编辑",
|
||||||
|
Modal: {
|
||||||
|
Title: "提示词列表",
|
||||||
|
Add: "增加一条",
|
||||||
|
Search: "搜尋提示詞",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "附带历史消息数",
|
Title: "附带历史消息数",
|
||||||
|
189
app/locales/de.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import { SubmitKey } from "../store/app";
|
||||||
|
import type { LocaleType } from "./index";
|
||||||
|
|
||||||
|
const de: LocaleType = {
|
||||||
|
WIP: "In Bearbeitung...",
|
||||||
|
Error: {
|
||||||
|
Unauthorized:
|
||||||
|
"Unbefugter Zugriff, bitte geben Sie den Zugangscode auf der Einstellungsseite ein.",
|
||||||
|
},
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} Nachrichten`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `${count} Nachrichten mit ChatGPT`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: "Zur Chat-Liste gehen",
|
||||||
|
CompressedHistory: "Komprimierter Gedächtnis-Prompt",
|
||||||
|
Export: "Alle Nachrichten als Markdown exportieren",
|
||||||
|
Copy: "Kopieren",
|
||||||
|
Stop: "Stop",
|
||||||
|
Retry: "Wiederholen",
|
||||||
|
Delete: "Delete",
|
||||||
|
},
|
||||||
|
Rename: "Chat umbenennen",
|
||||||
|
Typing: "Tippen...",
|
||||||
|
Input: (submitKey: string) => {
|
||||||
|
var inputHints = `${submitKey} um zu Senden`;
|
||||||
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
|
inputHints += ", Umschalt + Eingabe für Zeilenumbruch";
|
||||||
|
}
|
||||||
|
return inputHints + ", / zum Durchsuchen von Prompts";
|
||||||
|
},
|
||||||
|
Send: "Senden",
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: "Alle Nachrichten",
|
||||||
|
Copy: "Alles kopieren",
|
||||||
|
Download: "Herunterladen",
|
||||||
|
MessageFromYou: "Deine Nachricht",
|
||||||
|
MessageFromChatGPT: "Nachricht von ChatGPT",
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: "Gedächtnis-Prompt",
|
||||||
|
EmptyContent: "Noch nichts.",
|
||||||
|
Send: "Gedächtnis senden",
|
||||||
|
Copy: "Gedächtnis kopieren",
|
||||||
|
Reset: "Sitzung zurücksetzen",
|
||||||
|
ResetConfirm:
|
||||||
|
"Das Zurücksetzen löscht den aktuellen Gesprächsverlauf und das Langzeit-Gedächtnis. Möchten Sie wirklich zurücksetzen?",
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: "Neuer Chat",
|
||||||
|
DeleteChat: "Bestätigen Sie, um das ausgewählte Gespräch zu löschen?",
|
||||||
|
DeleteToast: "Chat gelöscht",
|
||||||
|
Revert: "Zurücksetzen",
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: "Einstellungen",
|
||||||
|
SubTitle: "Alle Einstellungen",
|
||||||
|
Actions: {
|
||||||
|
ClearAll: "Alle Daten löschen",
|
||||||
|
ResetAll: "Alle Einstellungen zurücksetzen",
|
||||||
|
Close: "Schließen",
|
||||||
|
ConfirmResetAll: {
|
||||||
|
Confirm: "Möchten Sie wirklich alle Konfigurationen zurücksetzen?",
|
||||||
|
},
|
||||||
|
ConfirmClearAll: {
|
||||||
|
Confirm: "Möchten Sie wirklich alle Chats zurücksetzen?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||||
|
Options: {
|
||||||
|
cn: "简体中文",
|
||||||
|
en: "English",
|
||||||
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
|
tr: "Türkçe",
|
||||||
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Avatar: "Avatar",
|
||||||
|
FontSize: {
|
||||||
|
Title: "Schriftgröße",
|
||||||
|
SubTitle: "Schriftgröße des Chat-Inhalts anpassen",
|
||||||
|
},
|
||||||
|
Update: {
|
||||||
|
Version: (x: string) => `Version: ${x}`,
|
||||||
|
IsLatest: "Neueste Version",
|
||||||
|
CheckUpdate: "Update prüfen",
|
||||||
|
IsChecking: "Update wird geprüft...",
|
||||||
|
FoundUpdate: (x: string) => `Neue Version gefunden: ${x}`,
|
||||||
|
GoToUpdate: "Aktualisieren",
|
||||||
|
},
|
||||||
|
SendKey: "Senden-Taste",
|
||||||
|
Theme: "Erscheinungsbild",
|
||||||
|
TightBorder: "Enger Rahmen",
|
||||||
|
SendPreviewBubble: "Vorschau-Bubble senden",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "Autovervollständigung deaktivieren",
|
||||||
|
SubTitle: "Autovervollständigung mit / starten",
|
||||||
|
},
|
||||||
|
List: "Prompt-Liste",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`${builtin} integriert, ${custom} benutzerdefiniert`,
|
||||||
|
Edit: "Bearbeiten",
|
||||||
|
Modal: {
|
||||||
|
Title: "Prompt List",
|
||||||
|
Add: "Add One",
|
||||||
|
Search: "Search Prompts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HistoryCount: {
|
||||||
|
Title: "Anzahl der angehängten Nachrichten",
|
||||||
|
SubTitle: "Anzahl der pro Anfrage angehängten gesendeten Nachrichten",
|
||||||
|
},
|
||||||
|
CompressThreshold: {
|
||||||
|
Title: "Schwellenwert für Verlaufskomprimierung",
|
||||||
|
SubTitle:
|
||||||
|
"Komprimierung, wenn die Länge der unkomprimierten Nachrichten den Wert überschreitet",
|
||||||
|
},
|
||||||
|
Token: {
|
||||||
|
Title: "API-Schlüssel",
|
||||||
|
SubTitle:
|
||||||
|
"Verwenden Sie Ihren Schlüssel, um das Zugangscode-Limit zu ignorieren",
|
||||||
|
Placeholder: "OpenAI API-Schlüssel",
|
||||||
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "Kontostand",
|
||||||
|
SubTitle(used: any, total: any) {
|
||||||
|
return `Diesen Monat ausgegeben $${used}, Abonnement $${total}`;
|
||||||
|
},
|
||||||
|
IsChecking: "Wird überprüft...",
|
||||||
|
Check: "Erneut prüfen",
|
||||||
|
NoAccess: "API-Schlüssel eingeben, um den Kontostand zu überprüfen",
|
||||||
|
},
|
||||||
|
AccessCode: {
|
||||||
|
Title: "Zugangscode",
|
||||||
|
SubTitle: "Zugangskontrolle aktiviert",
|
||||||
|
Placeholder: "Zugangscode erforderlich",
|
||||||
|
},
|
||||||
|
Model: "Modell",
|
||||||
|
Temperature: {
|
||||||
|
Title: "Temperature", //Temperatur
|
||||||
|
SubTitle: "Ein größerer Wert führt zu zufälligeren Antworten",
|
||||||
|
},
|
||||||
|
MaxTokens: {
|
||||||
|
Title: "Max Tokens", //Maximale Token
|
||||||
|
SubTitle: "Maximale Anzahl der Anfrage- plus Antwort-Token",
|
||||||
|
},
|
||||||
|
PresencePenlty: {
|
||||||
|
Title: "Presence Penalty", //Anwesenheitsstrafe
|
||||||
|
SubTitle:
|
||||||
|
"Ein größerer Wert erhöht die Wahrscheinlichkeit, dass über neue Themen gesprochen wird",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: "Neues Gespräch",
|
||||||
|
BotHello: "Hallo! Wie kann ich Ihnen heute helfen?",
|
||||||
|
Error:
|
||||||
|
"Etwas ist schief gelaufen, bitte versuchen Sie es später noch einmal.",
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) =>
|
||||||
|
"Dies ist eine Zusammenfassung des Chatverlaufs zwischen dem KI und dem Benutzer als Rückblick: " +
|
||||||
|
content,
|
||||||
|
Topic:
|
||||||
|
"Bitte erstellen Sie einen vier- bis fünfwörtigen Titel, der unser Gespräch zusammenfasst, ohne Einleitung, Zeichensetzung, Anführungszeichen, Punkte, Symbole oder zusätzlichen Text. Entfernen Sie Anführungszeichen.",
|
||||||
|
Summarize:
|
||||||
|
"Fassen Sie unsere Diskussion kurz in 200 Wörtern oder weniger zusammen, um sie als Pronpt für zukünftige Gespräche zu verwenden.",
|
||||||
|
},
|
||||||
|
ConfirmClearAll:
|
||||||
|
"Bestätigen Sie, um alle Chat- und Einstellungsdaten zu löschen?",
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: "In die Zwischenablage kopiert",
|
||||||
|
Failed:
|
||||||
|
"Kopieren fehlgeschlagen, bitte geben Sie die Berechtigung zum Zugriff auf die Zwischenablage frei",
|
||||||
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `Mit ${x} Kontext-Prompts`,
|
||||||
|
Edit: "Kontext- und Gedächtnis-Prompts",
|
||||||
|
Add: "Hinzufügen",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default de;
|
@ -19,6 +19,7 @@ const en: LocaleType = {
|
|||||||
Copy: "Copy",
|
Copy: "Copy",
|
||||||
Stop: "Stop",
|
Stop: "Stop",
|
||||||
Retry: "Retry",
|
Retry: "Retry",
|
||||||
|
Delete: "Delete",
|
||||||
},
|
},
|
||||||
Rename: "Rename Chat",
|
Rename: "Rename Chat",
|
||||||
Typing: "Typing…",
|
Typing: "Typing…",
|
||||||
@ -77,6 +78,7 @@ const en: LocaleType = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
@ -105,6 +107,11 @@ const en: LocaleType = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`${builtin} built-in, ${custom} user-defined`,
|
`${builtin} built-in, ${custom} user-defined`,
|
||||||
Edit: "Edit",
|
Edit: "Edit",
|
||||||
|
Modal: {
|
||||||
|
Title: "Prompt List",
|
||||||
|
Add: "Add One",
|
||||||
|
Search: "Search Prompts",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "Attached Messages Count",
|
Title: "Attached Messages Count",
|
||||||
@ -126,7 +133,7 @@ const en: LocaleType = {
|
|||||||
return `Used this month $${used}, subscription $${total}`;
|
return `Used this month $${used}, subscription $${total}`;
|
||||||
},
|
},
|
||||||
IsChecking: "Checking...",
|
IsChecking: "Checking...",
|
||||||
Check: "Check Again",
|
Check: "Check",
|
||||||
NoAccess: "Enter API Key to check balance",
|
NoAccess: "Enter API Key to check balance",
|
||||||
},
|
},
|
||||||
AccessCode: {
|
AccessCode: {
|
||||||
|
@ -19,6 +19,7 @@ const es: LocaleType = {
|
|||||||
Copy: "Copiar",
|
Copy: "Copiar",
|
||||||
Stop: "Detener",
|
Stop: "Detener",
|
||||||
Retry: "Reintentar",
|
Retry: "Reintentar",
|
||||||
|
Delete: "Delete",
|
||||||
},
|
},
|
||||||
Rename: "Renombrar chat",
|
Rename: "Renombrar chat",
|
||||||
Typing: "Escribiendo...",
|
Typing: "Escribiendo...",
|
||||||
@ -77,6 +78,7 @@ const es: LocaleType = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
@ -105,6 +107,11 @@ const es: LocaleType = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`${builtin} incorporado, ${custom} definido por el usuario`,
|
`${builtin} incorporado, ${custom} definido por el usuario`,
|
||||||
Edit: "Editar",
|
Edit: "Editar",
|
||||||
|
Modal: {
|
||||||
|
Title: "Prompt List",
|
||||||
|
Add: "Add One",
|
||||||
|
Search: "Search Prompts",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "Cantidad de mensajes adjuntos",
|
Title: "Cantidad de mensajes adjuntos",
|
||||||
|
@ -5,10 +5,20 @@ import ES from "./es";
|
|||||||
import IT from "./it";
|
import IT from "./it";
|
||||||
import TR from "./tr";
|
import TR from "./tr";
|
||||||
import JP from "./jp";
|
import JP from "./jp";
|
||||||
|
import DE from "./de";
|
||||||
|
|
||||||
export type { LocaleType } from "./cn";
|
export type { LocaleType } from "./cn";
|
||||||
|
|
||||||
export const AllLangs = ["en", "cn", "tw", "es", "it", "tr", "jp"] as const;
|
export const AllLangs = [
|
||||||
|
"en",
|
||||||
|
"cn",
|
||||||
|
"tw",
|
||||||
|
"es",
|
||||||
|
"it",
|
||||||
|
"tr",
|
||||||
|
"jp",
|
||||||
|
"de",
|
||||||
|
] as const;
|
||||||
type Lang = (typeof AllLangs)[number];
|
type Lang = (typeof AllLangs)[number];
|
||||||
|
|
||||||
const LANG_KEY = "lang";
|
const LANG_KEY = "lang";
|
||||||
@ -44,21 +54,13 @@ export function getLang(): Lang {
|
|||||||
|
|
||||||
const lang = getLanguage();
|
const lang = getLanguage();
|
||||||
|
|
||||||
if (lang.includes("zh") || lang.includes("cn")) {
|
for (const option of AllLangs) {
|
||||||
return "cn";
|
if (lang.includes(option)) {
|
||||||
} else if (lang.includes("tw")) {
|
return option;
|
||||||
return "tw";
|
}
|
||||||
} else if (lang.includes("es")) {
|
|
||||||
return "es";
|
|
||||||
} else if (lang.includes("it")) {
|
|
||||||
return "it";
|
|
||||||
} else if (lang.includes("tr")) {
|
|
||||||
return "tr";
|
|
||||||
} else if (lang.includes("jp")) {
|
|
||||||
return "jp";
|
|
||||||
} else {
|
|
||||||
return "en";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return "en";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeLang(lang: Lang) {
|
export function changeLang(lang: Lang) {
|
||||||
@ -66,6 +68,13 @@ export function changeLang(lang: Lang) {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { en: EN, cn: CN, tw: TW, es: ES, it: IT, tr: TR, jp: JP }[
|
export default {
|
||||||
getLang()
|
en: EN,
|
||||||
];
|
cn: CN,
|
||||||
|
tw: TW,
|
||||||
|
es: ES,
|
||||||
|
it: IT,
|
||||||
|
tr: TR,
|
||||||
|
jp: JP,
|
||||||
|
de: DE,
|
||||||
|
}[getLang()] as typeof CN;
|
||||||
|
@ -19,6 +19,7 @@ const it: LocaleType = {
|
|||||||
Copy: "Copia",
|
Copy: "Copia",
|
||||||
Stop: "Stop",
|
Stop: "Stop",
|
||||||
Retry: "Riprova",
|
Retry: "Riprova",
|
||||||
|
Delete: "Delete",
|
||||||
},
|
},
|
||||||
Rename: "Rinomina Chat",
|
Rename: "Rinomina Chat",
|
||||||
Typing: "Typing…",
|
Typing: "Typing…",
|
||||||
@ -45,12 +46,12 @@ const it: LocaleType = {
|
|||||||
Send: "Send Memory",
|
Send: "Send Memory",
|
||||||
Reset: "Reset Session",
|
Reset: "Reset Session",
|
||||||
ResetConfirm:
|
ResetConfirm:
|
||||||
"Resetting will clear the current conversation history and historical memory. Are you sure you want to reset?",
|
"Ripristinare cancellerà la conversazione corrente e la cronologia di memoria. Sei sicuro che vuoi riavviare?",
|
||||||
},
|
},
|
||||||
Home: {
|
Home: {
|
||||||
NewChat: "Nuova Chat",
|
NewChat: "Nuova Chat",
|
||||||
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
||||||
DeleteToast: "Chat Deleted",
|
DeleteToast: "Chat Cancellata",
|
||||||
Revert: "Revert",
|
Revert: "Revert",
|
||||||
},
|
},
|
||||||
Settings: {
|
Settings: {
|
||||||
@ -77,6 +78,7 @@ const it: LocaleType = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
@ -93,9 +95,9 @@ const it: LocaleType = {
|
|||||||
GoToUpdate: "Aggiorna",
|
GoToUpdate: "Aggiorna",
|
||||||
},
|
},
|
||||||
SendKey: "Tasto invia",
|
SendKey: "Tasto invia",
|
||||||
Theme: "tema",
|
Theme: "Tema",
|
||||||
TightBorder: "Bordi stretti",
|
TightBorder: "Schermo intero",
|
||||||
SendPreviewBubble: "Invia l'anteprima della bolla",
|
SendPreviewBubble: "Anteprima di digitazione",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "Disabilita l'auto completamento",
|
Title: "Disabilita l'auto completamento",
|
||||||
@ -105,6 +107,11 @@ const it: LocaleType = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`${builtin} built-in, ${custom} user-defined`,
|
`${builtin} built-in, ${custom} user-defined`,
|
||||||
Edit: "Modifica",
|
Edit: "Modifica",
|
||||||
|
Modal: {
|
||||||
|
Title: "Prompt List",
|
||||||
|
Add: "Add One",
|
||||||
|
Search: "Search Prompts",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "Conteggio dei messaggi allegati",
|
Title: "Conteggio dei messaggi allegati",
|
||||||
@ -116,7 +123,7 @@ const it: LocaleType = {
|
|||||||
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
|
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
|
||||||
},
|
},
|
||||||
Token: {
|
Token: {
|
||||||
Title: "Chiave API",
|
Title: "API Key",
|
||||||
SubTitle:
|
SubTitle:
|
||||||
"Utilizzare la chiave per ignorare il limite del codice di accesso",
|
"Utilizzare la chiave per ignorare il limite del codice di accesso",
|
||||||
Placeholder: "OpenAI API Key",
|
Placeholder: "OpenAI API Key",
|
||||||
@ -124,7 +131,7 @@ const it: LocaleType = {
|
|||||||
Usage: {
|
Usage: {
|
||||||
Title: "Bilancio Account",
|
Title: "Bilancio Account",
|
||||||
SubTitle(used: any, total: any) {
|
SubTitle(used: any, total: any) {
|
||||||
return `Usato in questo mese $${used}, subscription $${total}`;
|
return `Attualmente usato in questo mese $${used}, soglia massima $${total}`;
|
||||||
},
|
},
|
||||||
IsChecking: "Controllando...",
|
IsChecking: "Controllando...",
|
||||||
Check: "Controlla ancora",
|
Check: "Controlla ancora",
|
||||||
|
@ -18,6 +18,7 @@ const jp = {
|
|||||||
Copy: "コピー",
|
Copy: "コピー",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "リトライ",
|
Retry: "リトライ",
|
||||||
|
Delete: "Delete",
|
||||||
},
|
},
|
||||||
Rename: "チャットの名前を変更",
|
Rename: "チャットの名前を変更",
|
||||||
Typing: "入力中…",
|
Typing: "入力中…",
|
||||||
@ -76,6 +77,7 @@ const jp = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "アバター",
|
Avatar: "アバター",
|
||||||
@ -106,6 +108,11 @@ const jp = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`組み込み ${builtin} 件、ユーザー定義 ${custom} 件`,
|
`組み込み ${builtin} 件、ユーザー定義 ${custom} 件`,
|
||||||
Edit: "編集",
|
Edit: "編集",
|
||||||
|
Modal: {
|
||||||
|
Title: "提示词列表",
|
||||||
|
Add: "增加一条",
|
||||||
|
Search: "搜尋提示詞",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "履歴メッセージ数を添付",
|
Title: "履歴メッセージ数を添付",
|
||||||
@ -177,6 +184,4 @@ const jp = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocaleType = typeof jp;
|
|
||||||
|
|
||||||
export default jp;
|
export default jp;
|
||||||
|
@ -19,6 +19,7 @@ const tr: LocaleType = {
|
|||||||
Copy: "Kopyala",
|
Copy: "Kopyala",
|
||||||
Stop: "Durdur",
|
Stop: "Durdur",
|
||||||
Retry: "Tekrar Dene",
|
Retry: "Tekrar Dene",
|
||||||
|
Delete: "Delete",
|
||||||
},
|
},
|
||||||
Rename: "Sohbeti Yeniden Adlandır",
|
Rename: "Sohbeti Yeniden Adlandır",
|
||||||
Typing: "Yazıyor…",
|
Typing: "Yazıyor…",
|
||||||
@ -77,6 +78,7 @@ const tr: LocaleType = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
@ -105,6 +107,11 @@ const tr: LocaleType = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
|
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
|
||||||
Edit: "Düzenle",
|
Edit: "Düzenle",
|
||||||
|
Modal: {
|
||||||
|
Title: "Prompt List",
|
||||||
|
Add: "Add One",
|
||||||
|
Search: "Search Prompts",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "Ekli Mesaj Sayısı",
|
Title: "Ekli Mesaj Sayısı",
|
||||||
|
@ -18,6 +18,7 @@ const tw: LocaleType = {
|
|||||||
Copy: "複製",
|
Copy: "複製",
|
||||||
Stop: "停止",
|
Stop: "停止",
|
||||||
Retry: "重試",
|
Retry: "重試",
|
||||||
|
Delete: "刪除",
|
||||||
},
|
},
|
||||||
Rename: "重命名對話",
|
Rename: "重命名對話",
|
||||||
Typing: "正在輸入…",
|
Typing: "正在輸入…",
|
||||||
@ -75,6 +76,7 @@ const tw: LocaleType = {
|
|||||||
it: "Italiano",
|
it: "Italiano",
|
||||||
tr: "Türkçe",
|
tr: "Türkçe",
|
||||||
jp: "日本語",
|
jp: "日本語",
|
||||||
|
de: "Deutsch",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "大頭貼",
|
Avatar: "大頭貼",
|
||||||
@ -103,6 +105,11 @@ const tw: LocaleType = {
|
|||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`內置 ${builtin} 條,用戶定義 ${custom} 條`,
|
`內置 ${builtin} 條,用戶定義 ${custom} 條`,
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
|
Modal: {
|
||||||
|
Title: "提示詞列表",
|
||||||
|
Add: "增加一條",
|
||||||
|
Search: "搜索提示词",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
HistoryCount: {
|
HistoryCount: {
|
||||||
Title: "附帶歷史訊息數",
|
Title: "附帶歷史訊息數",
|
||||||
@ -152,9 +159,10 @@ const tw: LocaleType = {
|
|||||||
Prompt: {
|
Prompt: {
|
||||||
History: (content: string) =>
|
History: (content: string) =>
|
||||||
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
||||||
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
|
Topic:
|
||||||
|
"Summarise the conversation in a short and concise eye-catching title that instantly conveys the main topic. Use as few words as possible. Use the language used in the enquiry, e.g. use English for English enquiry, use zh-hant for traditional chinese enquiry. Don't use quotation marks at the beginning and the end.",
|
||||||
Summarize:
|
Summarize:
|
||||||
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
|
"Summarise the conversation in at most 250 tokens for continuing the conversation in future. Use the language used in the conversation, e.g. use English for English conversation, use zh-hant for traditional chinese conversation.",
|
||||||
},
|
},
|
||||||
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@ import type { ChatRequest, ChatResponse } from "./api/openai/typing";
|
|||||||
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
|
import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
|
||||||
import { showToast } from "./components/ui-lib";
|
import { showToast } from "./components/ui-lib";
|
||||||
|
|
||||||
const TIME_OUT_MS = 30000;
|
const TIME_OUT_MS = 60000;
|
||||||
|
|
||||||
const makeRequestParam = (
|
const makeRequestParam = (
|
||||||
messages: Message[],
|
messages: Message[],
|
||||||
@ -114,6 +114,10 @@ export async function requestUsage() {
|
|||||||
response.total_usage = Math.round(response.total_usage) / 100;
|
response.total_usage = Math.round(response.total_usage) / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (total.hard_limit_usd) {
|
||||||
|
total.hard_limit_usd = Math.round(total.hard_limit_usd * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
used: response.total_usage,
|
used: response.total_usage,
|
||||||
subscription: total.hard_limit_usd,
|
subscription: total.hard_limit_usd,
|
||||||
@ -167,7 +171,6 @@ export async function requestChatStream(
|
|||||||
options?.onController?.(controller);
|
options?.onController?.(controller);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// handle time out, will stop if no response in 10 secs
|
|
||||||
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
|
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
|
||||||
const content = await reader?.read();
|
const content = await reader?.read();
|
||||||
clearTimeout(resTimeoutId);
|
clearTimeout(resTimeoutId);
|
||||||
@ -189,8 +192,8 @@ export async function requestChatStream(
|
|||||||
|
|
||||||
finish();
|
finish();
|
||||||
} else if (res.status === 401) {
|
} else if (res.status === 401) {
|
||||||
console.error("Anauthorized");
|
console.error("Unauthorized");
|
||||||
options?.onError(new Error("Anauthorized"), res.status);
|
options?.onError(new Error("Unauthorized"), res.status);
|
||||||
} else {
|
} else {
|
||||||
console.error("Stream Error", res.body);
|
console.error("Stream Error", res.body);
|
||||||
options?.onError(new Error("Stream Error"), res.status);
|
options?.onError(new Error("Stream Error"), res.status);
|
||||||
@ -235,6 +238,14 @@ export const ControllerPool = {
|
|||||||
controller?.abort();
|
controller?.abort();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
stopAll() {
|
||||||
|
Object.values(this.controllers).forEach((v) => v.abort());
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPending() {
|
||||||
|
return Object.values(this.controllers).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
remove(sessionIndex: number, messageId: number) {
|
remove(sessionIndex: number, messageId: number) {
|
||||||
const key = this.key(sessionIndex, messageId);
|
const key = this.key(sessionIndex, messageId);
|
||||||
delete this.controllers[key];
|
delete this.controllers[key];
|
||||||
|
@ -386,6 +386,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
const botMessage: Message = createMessage({
|
const botMessage: Message = createMessage({
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
streaming: true,
|
streaming: true,
|
||||||
|
id: userMessage.id! + 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
// get recent messages
|
// get recent messages
|
||||||
@ -421,7 +422,7 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
onError(error, statusCode) {
|
onError(error, statusCode) {
|
||||||
if (statusCode === 401) {
|
if (statusCode === 401) {
|
||||||
botMessage.content = Locale.Error.Unauthorized;
|
botMessage.content = Locale.Error.Unauthorized;
|
||||||
} else {
|
} else if (!error.message.includes("aborted")) {
|
||||||
botMessage.content += "\n\n" + Locale.Store.Error;
|
botMessage.content += "\n\n" + Locale.Store.Error;
|
||||||
}
|
}
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
|
@ -5,62 +5,74 @@ import { getLang } from "../locales";
|
|||||||
|
|
||||||
export interface Prompt {
|
export interface Prompt {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
isUser?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PromptStore {
|
export interface PromptStore {
|
||||||
|
counter: number;
|
||||||
latestId: number;
|
latestId: number;
|
||||||
prompts: Map<number, Prompt>;
|
prompts: Record<number, Prompt>;
|
||||||
|
|
||||||
add: (prompt: Prompt) => number;
|
add: (prompt: Prompt) => number;
|
||||||
remove: (id: number) => void;
|
remove: (id: number) => void;
|
||||||
search: (text: string) => Prompt[];
|
search: (text: string) => Prompt[];
|
||||||
|
|
||||||
|
getUserPrompts: () => Prompt[];
|
||||||
|
updateUserPrompts: (id: number, updater: (prompt: Prompt) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PROMPT_KEY = "prompt-store";
|
export const PROMPT_KEY = "prompt-store";
|
||||||
|
|
||||||
export const SearchService = {
|
export const SearchService = {
|
||||||
ready: false,
|
ready: false,
|
||||||
engine: new Fuse<Prompt>([], { keys: ["title"] }),
|
builtinEngine: new Fuse<Prompt>([], { keys: ["title"] }),
|
||||||
|
userEngine: new Fuse<Prompt>([], { keys: ["title"] }),
|
||||||
count: {
|
count: {
|
||||||
builtin: 0,
|
builtin: 0,
|
||||||
},
|
},
|
||||||
allBuiltInPrompts: [] as Prompt[],
|
allPrompts: [] as Prompt[],
|
||||||
|
builtinPrompts: [] as Prompt[],
|
||||||
|
|
||||||
init(prompts: Prompt[]) {
|
init(builtinPrompts: Prompt[], userPrompts: Prompt[]) {
|
||||||
if (this.ready) {
|
if (this.ready) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.allBuiltInPrompts = prompts;
|
this.allPrompts = userPrompts.concat(builtinPrompts);
|
||||||
this.engine.setCollection(prompts);
|
this.builtinPrompts = builtinPrompts.slice();
|
||||||
|
this.builtinEngine.setCollection(builtinPrompts);
|
||||||
|
this.userEngine.setCollection(userPrompts);
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(id: number) {
|
remove(id: number) {
|
||||||
this.engine.remove((doc) => doc.id === id);
|
this.userEngine.remove((doc) => doc.id === id);
|
||||||
},
|
},
|
||||||
|
|
||||||
add(prompt: Prompt) {
|
add(prompt: Prompt) {
|
||||||
this.engine.add(prompt);
|
this.userEngine.add(prompt);
|
||||||
},
|
},
|
||||||
|
|
||||||
search(text: string) {
|
search(text: string) {
|
||||||
const results = this.engine.search(text);
|
const userResults = this.userEngine.search(text);
|
||||||
return results.map((v) => v.item);
|
const builtinResults = this.builtinEngine.search(text);
|
||||||
|
return userResults.concat(builtinResults).map((v) => v.item);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePromptStore = create<PromptStore>()(
|
export const usePromptStore = create<PromptStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
counter: 0,
|
||||||
latestId: 0,
|
latestId: 0,
|
||||||
prompts: new Map(),
|
prompts: {},
|
||||||
|
|
||||||
add(prompt) {
|
add(prompt) {
|
||||||
const prompts = get().prompts;
|
const prompts = get().prompts;
|
||||||
prompt.id = get().latestId + 1;
|
prompt.id = get().latestId + 1;
|
||||||
prompts.set(prompt.id, prompt);
|
prompt.isUser = true;
|
||||||
|
prompts[prompt.id] = prompt;
|
||||||
|
|
||||||
set(() => ({
|
set(() => ({
|
||||||
latestId: prompt.id!,
|
latestId: prompt.id!,
|
||||||
@ -72,19 +84,40 @@ export const usePromptStore = create<PromptStore>()(
|
|||||||
|
|
||||||
remove(id) {
|
remove(id) {
|
||||||
const prompts = get().prompts;
|
const prompts = get().prompts;
|
||||||
prompts.delete(id);
|
delete prompts[id];
|
||||||
SearchService.remove(id);
|
SearchService.remove(id);
|
||||||
|
|
||||||
set(() => ({
|
set(() => ({
|
||||||
prompts,
|
prompts,
|
||||||
|
counter: get().counter + 1,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getUserPrompts() {
|
||||||
|
const userPrompts = Object.values(get().prompts ?? {});
|
||||||
|
userPrompts.sort((a, b) => (b.id && a.id ? b.id - a.id : 0));
|
||||||
|
return userPrompts;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUserPrompts(id: number, updater) {
|
||||||
|
const prompt = get().prompts[id] ?? {
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
|
||||||
|
SearchService.remove(id);
|
||||||
|
updater(prompt);
|
||||||
|
const prompts = get().prompts;
|
||||||
|
prompts[id] = prompt;
|
||||||
|
set(() => ({ prompts }));
|
||||||
|
SearchService.add(prompt);
|
||||||
|
},
|
||||||
|
|
||||||
search(text) {
|
search(text) {
|
||||||
if (text.length === 0) {
|
if (text.length === 0) {
|
||||||
// return all prompts
|
// return all rompts
|
||||||
const userPrompts = get().prompts?.values?.() ?? [];
|
return SearchService.allPrompts.concat([...get().getUserPrompts()]);
|
||||||
return SearchService.allBuiltInPrompts.concat([...userPrompts]);
|
|
||||||
}
|
}
|
||||||
return SearchService.search(text) as Prompt[];
|
return SearchService.search(text) as Prompt[];
|
||||||
},
|
},
|
||||||
@ -104,24 +137,27 @@ export const usePromptStore = create<PromptStore>()(
|
|||||||
if (getLang() === "cn") {
|
if (getLang() === "cn") {
|
||||||
fetchPrompts = fetchPrompts.reverse();
|
fetchPrompts = fetchPrompts.reverse();
|
||||||
}
|
}
|
||||||
const builtinPrompts = fetchPrompts
|
const builtinPrompts = fetchPrompts.map(
|
||||||
.map((promptList: PromptList) => {
|
(promptList: PromptList) => {
|
||||||
return promptList.map(
|
return promptList.map(
|
||||||
([title, content]) =>
|
([title, content]) =>
|
||||||
({
|
({
|
||||||
|
id: Math.random(),
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
} as Prompt),
|
} as Prompt),
|
||||||
);
|
);
|
||||||
})
|
},
|
||||||
.concat([...(state?.prompts?.values() ?? [])]);
|
|
||||||
|
|
||||||
const allPromptsForSearch = builtinPrompts.reduce(
|
|
||||||
(pre, cur) => pre.concat(cur),
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userPrompts =
|
||||||
|
usePromptStore.getState().getUserPrompts() ?? [];
|
||||||
|
|
||||||
|
const allPromptsForSearch = builtinPrompts
|
||||||
|
.reduce((pre, cur) => pre.concat(cur), [])
|
||||||
|
.filter((v) => !!v.title && !!v.content);
|
||||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||||
SearchService.init(allPromptsForSearch);
|
SearchService.init(allPromptsForSearch, userPrompts);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
|
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
|
||||||
|
import { requestUsage } from "../requests";
|
||||||
|
|
||||||
export interface UpdateStore {
|
export interface UpdateStore {
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
remoteVersion: string;
|
remoteVersion: string;
|
||||||
|
|
||||||
|
used?: number;
|
||||||
|
subscription?: number;
|
||||||
|
lastUpdateUsage: number;
|
||||||
|
|
||||||
version: string;
|
version: string;
|
||||||
getLatestVersion: (force: boolean) => Promise<string>;
|
getLatestVersion: (force?: boolean) => Promise<void>;
|
||||||
|
updateUsage: (force?: boolean) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UPDATE_KEY = "chat-update";
|
export const UPDATE_KEY = "chat-update";
|
||||||
@ -26,22 +32,27 @@ function queryMeta(key: string, defaultValue?: string): string {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ONE_MINUTE = 60 * 1000;
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateStore>()(
|
export const useUpdateStore = create<UpdateStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
lastUpdate: 0,
|
lastUpdate: 0,
|
||||||
remoteVersion: "",
|
remoteVersion: "",
|
||||||
|
|
||||||
|
lastUpdateUsage: 0,
|
||||||
|
|
||||||
version: "unknown",
|
version: "unknown",
|
||||||
|
|
||||||
async getLatestVersion(force = false) {
|
async getLatestVersion(force = false) {
|
||||||
set(() => ({ version: queryMeta("version") }));
|
set(() => ({ version: queryMeta("version") ?? "unknown" }));
|
||||||
|
|
||||||
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
|
const overTenMins = Date.now() - get().lastUpdate > 10 * ONE_MINUTE;
|
||||||
const shouldFetch = force || overTenMins;
|
if (!force && !overTenMins) return;
|
||||||
if (!shouldFetch) {
|
|
||||||
return get().version ?? "unknown";
|
set(() => ({
|
||||||
}
|
lastUpdate: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// const data = await (await fetch(FETCH_TAG_URL)).json();
|
// const data = await (await fetch(FETCH_TAG_URL)).json();
|
||||||
@ -49,14 +60,26 @@ export const useUpdateStore = create<UpdateStore>()(
|
|||||||
const data = await (await fetch(FETCH_COMMIT_URL)).json();
|
const data = await (await fetch(FETCH_COMMIT_URL)).json();
|
||||||
const remoteId = (data[0].sha as string).substring(0, 7);
|
const remoteId = (data[0].sha as string).substring(0, 7);
|
||||||
set(() => ({
|
set(() => ({
|
||||||
lastUpdate: Date.now(),
|
|
||||||
remoteVersion: remoteId,
|
remoteVersion: remoteId,
|
||||||
}));
|
}));
|
||||||
console.log("[Got Upstream] ", remoteId);
|
console.log("[Got Upstream] ", remoteId);
|
||||||
return remoteId;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Fetch Upstream Commit Id]", error);
|
console.error("[Fetch Upstream Commit Id]", error);
|
||||||
return get().version ?? "";
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUsage(force = false) {
|
||||||
|
const overOneMinute = Date.now() - get().lastUpdateUsage >= ONE_MINUTE;
|
||||||
|
if (!overOneMinute && !force) return;
|
||||||
|
|
||||||
|
set(() => ({
|
||||||
|
lastUpdateUsage: Date.now(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const usage = await requestUsage();
|
||||||
|
|
||||||
|
if (usage) {
|
||||||
|
set(() => usage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
@mixin light {
|
@mixin light {
|
||||||
|
--theme: light;
|
||||||
|
|
||||||
/* color */
|
/* color */
|
||||||
--white: white;
|
--white: white;
|
||||||
--black: rgb(48, 48, 48);
|
--black: rgb(48, 48, 48);
|
||||||
@ -18,6 +20,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark {
|
@mixin dark {
|
||||||
|
--theme: dark;
|
||||||
|
|
||||||
/* color */
|
/* color */
|
||||||
--white: rgb(30, 30, 30);
|
--white: rgb(30, 30, 30);
|
||||||
--black: rgb(187, 187, 187);
|
--black: rgb(187, 187, 187);
|
||||||
@ -31,6 +35,10 @@
|
|||||||
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
|
--border-in-light: 1px solid rgba(255, 255, 255, 0.192);
|
||||||
|
|
||||||
--theme-color: var(--gray);
|
--theme-color: var(--gray);
|
||||||
|
|
||||||
|
div:not(.no-dark) > svg {
|
||||||
|
filter: invert(0.5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.light {
|
.light {
|
||||||
@ -132,6 +140,7 @@ label {
|
|||||||
|
|
||||||
input {
|
input {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
@ -216,6 +225,7 @@ input[type="password"] {
|
|||||||
color: var(--black);
|
color: var(--black);
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.math {
|
div.math {
|
||||||
@ -282,10 +292,6 @@ pre {
|
|||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
div:not(.no-dark) > svg {
|
|
||||||
filter: invert(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(0.9);
|
filter: brightness(0.9);
|
||||||
}
|
}
|
||||||
|
@ -120,3 +120,7 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
|||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCSSVar(varName: string) {
|
||||||
|
return getComputedStyle(document.body).getPropertyValue(varName).trim();
|
||||||
|
}
|
||||||
|
BIN
docs/images/enable-actions-sync.jpg
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
docs/images/enable-actions.jpg
Normal file
After Width: | Height: | Size: 130 KiB |
BIN
docs/images/vercel/vercel-create-1.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
docs/images/vercel/vercel-create-2.jpg
Normal file
After Width: | Height: | Size: 131 KiB |
BIN
docs/images/vercel/vercel-create-3.jpg
Normal file
After Width: | Height: | Size: 106 KiB |
BIN
docs/images/vercel/vercel-env-edit.jpg
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
docs/images/vercel/vercel-redeploy.jpg
Normal file
After Width: | Height: | Size: 102 KiB |
39
docs/vercel-cn.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Vercel 的使用说明
|
||||||
|
|
||||||
|
## 如何新建项目
|
||||||
|
当你从 Github fork 本项目之后,需要重新在 Vercel 创建一个全新的 Vercel 项目来重新部署,你需要按照下列步骤进行。
|
||||||
|
|
||||||
|

|
||||||
|
1. 进入 Vercel 控制台首页;
|
||||||
|
2. 点击 Add New;
|
||||||
|
3. 选择 Project。
|
||||||
|
|
||||||
|

|
||||||
|
1. 在 Import Git Repository 处,搜索 chatgpt-next-web;
|
||||||
|
2. 选中新 fork 的项目,点击 Import。
|
||||||
|
|
||||||
|

|
||||||
|
1. 在项目配置页,点开 Environmane Variables 开始配置环境变量;
|
||||||
|
2. 依次新增名为 OPENAI_API_KEY 和 CODE 的环境变量;
|
||||||
|
3. 填入环境变量对应的值;
|
||||||
|
4. 点击 Add 确认增加环境变量;
|
||||||
|
5. 请确保你添加了 OPENAI_API_KEY,否则无法使用;
|
||||||
|
6. 点击 Deploy,创建完成,耐心等待 5 分钟左右部署完成。
|
||||||
|
|
||||||
|
## 如何增加自定义域名
|
||||||
|
[TODO]
|
||||||
|
|
||||||
|
## 如何更改环境变量
|
||||||
|

|
||||||
|
1. 进去 Vercel 项目内部控制台,点击顶部的 Settings 按钮;
|
||||||
|
2. 点击左侧的 Environment Variables;
|
||||||
|
3. 点击已有条目的右侧按钮;
|
||||||
|
4. 选择 Edit 进行编辑,然后保存即可。
|
||||||
|
|
||||||
|
⚠️️ 注意:每次修改完环境变量,你都需要[重新部署项目](#如何重新部署)来让改动生效!
|
||||||
|
|
||||||
|
## 如何重新部署
|
||||||
|

|
||||||
|
1. 进入 Vercel 项目内部控制台,点击顶部的 Deployments 按钮;
|
||||||
|
2. 选择列表最顶部一条的右侧按钮;
|
||||||
|
3. 点击 Redeploy 即可重新部署。
|
@ -8,6 +8,17 @@ export const config = {
|
|||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
function getIP(req: NextRequest) {
|
||||||
|
let ip = req.ip ?? req.headers.get("x-real-ip");
|
||||||
|
const forwardedFor = req.headers.get("x-forwarded-for");
|
||||||
|
|
||||||
|
if (!ip && forwardedFor) {
|
||||||
|
ip = forwardedFor.split(",").at(0) ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
export function middleware(req: NextRequest) {
|
export function middleware(req: NextRequest) {
|
||||||
//console.log("req:",req)
|
//console.log("req:",req)
|
||||||
const accessCode = req.headers.get("access-code");
|
const accessCode = req.headers.get("access-code");
|
||||||
@ -17,6 +28,8 @@ export function middleware(req: NextRequest) {
|
|||||||
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
|
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
|
||||||
console.log("[Auth] got access code:", accessCode);
|
console.log("[Auth] got access code:", accessCode);
|
||||||
console.log("[Auth] hashed access code:", hashedCode);
|
console.log("[Auth] hashed access code:", hashedCode);
|
||||||
|
console.log("[User IP] ", getIP(req));
|
||||||
|
console.log("[Time] ", new Date().toLocaleString());
|
||||||
|
|
||||||
if (!accessCode) {
|
if (!accessCode) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"fetch": "node ./scripts/fetch-prompts.mjs",
|
"fetch": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install",
|
||||||
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^16.2.0",
|
"@hello-pangea/dnd": "^16.2.0",
|
||||||
@ -19,7 +20,7 @@
|
|||||||
"emoji-picker-react": "^4.4.7",
|
"emoji-picker-react": "^4.4.7",
|
||||||
"eventsource-parser": "^0.1.0",
|
"eventsource-parser": "^0.1.0",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"next": "^13.2.3",
|
"next": "^13.3.1-canary.8",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"openai": "^3.2.1",
|
"openai": "^3.2.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
1
scripts/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
proxychains.conf
|
5
scripts/init-proxy.sh
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dir="$(dirname "$0")"
|
||||||
|
config=$dir/proxychains.conf
|
||||||
|
host_ip=$(grep nameserver /etc/resolv.conf | sed 's/nameserver //')
|
||||||
|
cp $dir/proxychains.template.conf $config
|
||||||
|
sed -i "\$s/.*/http $host_ip 7890/" $config
|
12
scripts/proxychains.template.conf
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
strict_chain
|
||||||
|
proxy_dns
|
||||||
|
|
||||||
|
remote_dns_subnet 224
|
||||||
|
|
||||||
|
tcp_read_time_out 15000
|
||||||
|
tcp_connect_time_out 8000
|
||||||
|
|
||||||
|
localnet 127.0.0.0/255.0.0.0
|
||||||
|
|
||||||
|
[ProxyList]
|
||||||
|
socks4 127.0.0.1 9050
|
145
yarn.lock
@ -1099,10 +1099,10 @@
|
|||||||
"@jridgewell/resolve-uri" "3.1.0"
|
"@jridgewell/resolve-uri" "3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||||
|
|
||||||
"@next/env@13.2.4":
|
"@next/env@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.2.4.tgz#8b763700262b2445140a44a8c8d088cef676dbae"
|
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1-canary.8.tgz#9f5cf57999e4f4b59ef6407924803a247cc4e451"
|
||||||
integrity sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==
|
integrity sha512-xZfNu7yq3OfiC4rkGuGMcqb25se+ZHRqajSdny8dp+nZzkNSK1SHuNT3W8faI+KGk6dqzO/zAdHR9YrqnQlCAg==
|
||||||
|
|
||||||
"@next/eslint-plugin-next@13.2.3":
|
"@next/eslint-plugin-next@13.2.3":
|
||||||
version "13.2.3"
|
version "13.2.3"
|
||||||
@ -1111,70 +1111,50 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "7.1.7"
|
glob "7.1.7"
|
||||||
|
|
||||||
"@next/swc-android-arm-eabi@13.2.4":
|
"@next/swc-darwin-arm64@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.2.4.tgz#758d0403771e549f9cee71cbabc0cb16a6c947c0"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.1-canary.8.tgz#66786ba76d37c210c184739624c6f84eaf2dc52b"
|
||||||
integrity sha512-DWlalTSkLjDU11MY11jg17O1gGQzpRccM9Oes2yTqj2DpHndajrXHGxj9HGtJ+idq2k7ImUdJVWS2h2l/EDJOw==
|
integrity sha512-BLbvhcaSzwuXbREOmJiqAdXVD7Jl9830hDY5ZTTNg7hXqEZgoMg2LxAEmtaaBMVZRfDQjd5bH3QPBV8fbG4UKg==
|
||||||
|
|
||||||
"@next/swc-android-arm64@13.2.4":
|
"@next/swc-darwin-x64@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.2.4.tgz#834d586523045110d5602e0c8aae9028835ac427"
|
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.1-canary.8.tgz#289296bd3cc55db7fef42037eb89ce4a6260ba31"
|
||||||
integrity sha512-sRavmUImUCf332Gy+PjIfLkMhiRX1Ez4SI+3vFDRs1N5eXp+uNzjFUK/oLMMOzk6KFSkbiK/3Wt8+dHQR/flNg==
|
integrity sha512-n4tJKPIvFTZshS1TVWrsqaW7h9VW+BmguO/AlZ3Q3NJ9hWxC5L4lxn2T6CTQ4M30Gf+t5u+dPzYLQ5IDtJFnFQ==
|
||||||
|
|
||||||
"@next/swc-darwin-arm64@13.2.4":
|
"@next/swc-linux-arm64-gnu@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.2.4.tgz#5006fca179a36ef3a24d293abadec7438dbb48c6"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.1-canary.8.tgz#dc79e8005849b6482241b460abdce9334665c766"
|
||||||
integrity sha512-S6vBl+OrInP47TM3LlYx65betocKUUlTZDDKzTiRDbsRESeyIkBtZ6Qi5uT2zQs4imqllJznVjFd1bXLx3Aa6A==
|
integrity sha512-AxnsgZ56whwVAeejyEZMk8xc8Vapwzb3Zn0YdZzPCR42WKfkcSkM+AWfq33zUOZnjvCmQBDyfHIo4CURVweR6g==
|
||||||
|
|
||||||
"@next/swc-darwin-x64@13.2.4":
|
"@next/swc-linux-arm64-musl@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.2.4.tgz#6549c7c04322766acc3264ccdb3e1b43fcaf7946"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.1-canary.8.tgz#f70873add4aad7ced36f760d1640adc008b7dc03"
|
||||||
integrity sha512-a6LBuoYGcFOPGd4o8TPo7wmv5FnMr+Prz+vYHopEDuhDoMSHOnC+v+Ab4D7F0NMZkvQjEJQdJS3rqgFhlZmKlw==
|
integrity sha512-zc7rzhtrHMWZ/phvjCNplHGo+ZLembjtluI5J8Xl4iwQQCyZwAtnmQhs37/zkdi6dHZou+wcFBZWRz14awRDBw==
|
||||||
|
|
||||||
"@next/swc-freebsd-x64@13.2.4":
|
"@next/swc-linux-x64-gnu@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.2.4.tgz#0bbe28979e3e868debc2cc06e45e186ce195b7f4"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.3.1-canary.8.tgz#fe81b8033628c6cf74e154f2db8c8c7f1593008f"
|
||||||
integrity sha512-kkbzKVZGPaXRBPisoAQkh3xh22r+TD+5HwoC5bOkALraJ0dsOQgSMAvzMXKsN3tMzJUPS0tjtRf1cTzrQ0I5vQ==
|
integrity sha512-vNbFDiuZ9fWmcznlilDbflZLb04evWPUQlyDT7Tqjd964PlSIaaX3tr64pdYjJOljDaqTr2Kbx0YW74mWF/PEw==
|
||||||
|
|
||||||
"@next/swc-linux-arm-gnueabihf@13.2.4":
|
"@next/swc-linux-x64-musl@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.2.4.tgz#1d28d2203f5a7427d6e7119d7bcb5fc40959fb3e"
|
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.3.1-canary.8.tgz#ada4585046a7937f96f2d39fc4aaca12826dde5f"
|
||||||
integrity sha512-7qA1++UY0fjprqtjBZaOA6cas/7GekpjVsZn/0uHvquuITFCdKGFCsKNBx3S0Rpxmx6WYo0GcmhNRM9ru08BGg==
|
integrity sha512-/FVBPJEBDZYCNraocRWtd5ObAgNi9VFnzJYGYDYIj4jKkFRWWm/CaWu9A7toQACC/JDy262uPyDPathXT9BAqQ==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-gnu@13.2.4":
|
"@next/swc-win32-arm64-msvc@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.2.4.tgz#eb26448190948cdf4c44b8f34110a3ecea32f1d0"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.1-canary.8.tgz#21b4f6c4be61845759753df9313bd9bcbb241969"
|
||||||
integrity sha512-xzYZdAeq883MwXgcwc72hqo/F/dwUxCukpDOkx/j1HTq/J0wJthMGjinN9wH5bPR98Mfeh1MZJ91WWPnZOedOg==
|
integrity sha512-8jMwRCeI26yVZLPwG0AjOi4b1yqSeqYmbHA7r+dqiV0OgFdYjnbyHU1FmiKDaC5SnnJN6LWV2Qjer9GDD0Kcuw==
|
||||||
|
|
||||||
"@next/swc-linux-arm64-musl@13.2.4":
|
"@next/swc-win32-ia32-msvc@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.2.4.tgz#c4227c0acd94a420bb14924820710e6284d234d3"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.1-canary.8.tgz#e23192e1d1b1a32b0eb805363b02360c5b523a77"
|
||||||
integrity sha512-8rXr3WfmqSiYkb71qzuDP6I6R2T2tpkmf83elDN8z783N9nvTJf2E7eLx86wu2OJCi4T05nuxCsh4IOU3LQ5xw==
|
integrity sha512-kcYB9iSEikFhv0I9uQDdgQ2lm8i3O8LA+GhnED9e5VtURBwOSwED7c6ZpaRQBYSPgnEA9/xiJVChICE/I7Ig1g==
|
||||||
|
|
||||||
"@next/swc-linux-x64-gnu@13.2.4":
|
"@next/swc-win32-x64-msvc@13.3.1-canary.8":
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.2.4.tgz#6bcb540944ee9b0209b33bfc23b240c2044dfc3e"
|
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.1-canary.8.tgz#a3f29404955cba2193de5e74fd5d9fcfdcb0ab51"
|
||||||
integrity sha512-Ngxh51zGSlYJ4EfpKG4LI6WfquulNdtmHg1yuOYlaAr33KyPJp4HeN/tivBnAHcZkoNy0hh/SbwDyCnz5PFJQQ==
|
integrity sha512-UKrGHonKVWBNg+HI4J8pXE6Jjjl8GwjhygFau71s8M0+jSy99y5Y+nGH9EmMNWKNvrObukyYvrs6OsAusKdCqw==
|
||||||
|
|
||||||
"@next/swc-linux-x64-musl@13.2.4":
|
|
||||||
version "13.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.2.4.tgz#ce21e43251eaf09a09df39372b2c3e38028c30ff"
|
|
||||||
integrity sha512-gOvwIYoSxd+j14LOcvJr+ekd9fwYT1RyMAHOp7znA10+l40wkFiMONPLWiZuHxfRk+Dy7YdNdDh3ImumvL6VwA==
|
|
||||||
|
|
||||||
"@next/swc-win32-arm64-msvc@13.2.4":
|
|
||||||
version "13.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.2.4.tgz#68220063d8e5e082f5465498675640dedb670ff1"
|
|
||||||
integrity sha512-q3NJzcfClgBm4HvdcnoEncmztxrA5GXqKeiZ/hADvC56pwNALt3ngDC6t6qr1YW9V/EPDxCYeaX4zYxHciW4Dw==
|
|
||||||
|
|
||||||
"@next/swc-win32-ia32-msvc@13.2.4":
|
|
||||||
version "13.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.2.4.tgz#7c120ab54a081be9566df310bed834f168252990"
|
|
||||||
integrity sha512-/eZ5ncmHUYtD2fc6EUmAIZlAJnVT2YmxDsKs1Ourx0ttTtvtma/WKlMV5NoUsyOez0f9ExLyOpeCoz5aj+MPXw==
|
|
||||||
|
|
||||||
"@next/swc-win32-x64-msvc@13.2.4":
|
|
||||||
version "13.2.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.2.4.tgz#5abda92fe12b9829bf7951c4a221282c56041144"
|
|
||||||
integrity sha512-0MffFmyv7tBLlji01qc0IaPP/LVExzvj7/R5x1Jph1bTAIj4Vu81yFQWHHQAP6r4ff9Ukj1mBK6MDNVXm7Tcvw==
|
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
@ -1737,6 +1717,13 @@ browserslist@^4.21.3, browserslist@^4.21.5:
|
|||||||
node-releases "^2.0.8"
|
node-releases "^2.0.8"
|
||||||
update-browserslist-db "^1.0.10"
|
update-browserslist-db "^1.0.10"
|
||||||
|
|
||||||
|
busboy@1.6.0:
|
||||||
|
version "1.6.0"
|
||||||
|
resolved "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
|
||||||
|
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
|
||||||
|
dependencies:
|
||||||
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
|
||||||
@ -3944,30 +3931,27 @@ natural-compare@^1.4.0:
|
|||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||||
|
|
||||||
next@^13.2.3:
|
next@^13.3.1-canary.8:
|
||||||
version "13.2.4"
|
version "13.3.1-canary.8"
|
||||||
resolved "https://registry.yarnpkg.com/next/-/next-13.2.4.tgz#2363330392b0f7da02ab41301f60857ffa7f67d6"
|
resolved "https://registry.yarnpkg.com/next/-/next-13.3.1-canary.8.tgz#f0846e5eada1491884326786a0749d5adc04c24d"
|
||||||
integrity sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==
|
integrity sha512-z4QUgyAN+hSWSEqb4pvGvC3iRktE6NH2DVLU4AvfqNYpzP+prePiJC8HN/cJpFhGW9YbhyRLi5FliDC631OOag==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@next/env" "13.2.4"
|
"@next/env" "13.3.1-canary.8"
|
||||||
"@swc/helpers" "0.4.14"
|
"@swc/helpers" "0.4.14"
|
||||||
|
busboy "1.6.0"
|
||||||
caniuse-lite "^1.0.30001406"
|
caniuse-lite "^1.0.30001406"
|
||||||
postcss "8.4.14"
|
postcss "8.4.14"
|
||||||
styled-jsx "5.1.1"
|
styled-jsx "5.1.1"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@next/swc-android-arm-eabi" "13.2.4"
|
"@next/swc-darwin-arm64" "13.3.1-canary.8"
|
||||||
"@next/swc-android-arm64" "13.2.4"
|
"@next/swc-darwin-x64" "13.3.1-canary.8"
|
||||||
"@next/swc-darwin-arm64" "13.2.4"
|
"@next/swc-linux-arm64-gnu" "13.3.1-canary.8"
|
||||||
"@next/swc-darwin-x64" "13.2.4"
|
"@next/swc-linux-arm64-musl" "13.3.1-canary.8"
|
||||||
"@next/swc-freebsd-x64" "13.2.4"
|
"@next/swc-linux-x64-gnu" "13.3.1-canary.8"
|
||||||
"@next/swc-linux-arm-gnueabihf" "13.2.4"
|
"@next/swc-linux-x64-musl" "13.3.1-canary.8"
|
||||||
"@next/swc-linux-arm64-gnu" "13.2.4"
|
"@next/swc-win32-arm64-msvc" "13.3.1-canary.8"
|
||||||
"@next/swc-linux-arm64-musl" "13.2.4"
|
"@next/swc-win32-ia32-msvc" "13.3.1-canary.8"
|
||||||
"@next/swc-linux-x64-gnu" "13.2.4"
|
"@next/swc-win32-x64-msvc" "13.3.1-canary.8"
|
||||||
"@next/swc-linux-x64-musl" "13.2.4"
|
|
||||||
"@next/swc-win32-arm64-msvc" "13.2.4"
|
|
||||||
"@next/swc-win32-ia32-msvc" "13.2.4"
|
|
||||||
"@next/swc-win32-x64-msvc" "13.2.4"
|
|
||||||
|
|
||||||
node-domexception@^1.0.0:
|
node-domexception@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
@ -4682,6 +4666,11 @@ stop-iteration-iterator@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
internal-slot "^1.0.4"
|
internal-slot "^1.0.4"
|
||||||
|
|
||||||
|
streamsearch@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||||
|
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
|
||||||
|
|
||||||
string-argv@^0.3.1:
|
string-argv@^0.3.1:
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
|
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
|
||||||
|