mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-30 18:46:58 +08:00
Compare commits
109 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b5b8593e7f | ||
|
03163d6a61 | ||
|
cada0aa70b | ||
|
75488e4bd5 | ||
|
6da3aab046 | ||
|
ff2589c97f | ||
|
a9f000e7ef | ||
|
d9be63e6cb | ||
|
1a626a68f0 | ||
|
330504b91e | ||
|
6bb0166055 | ||
|
b22988e6b8 | ||
|
f7edac961f | ||
|
5b9b120fa6 | ||
|
f07e4fc87f | ||
|
a0e192b6e4 | ||
|
dc3fa6c780 | ||
|
dd5604f5d9 | ||
|
170936a96e | ||
|
93c9974019 | ||
|
377579e802 | ||
|
c49dbab127 | ||
|
36adfe87fb | ||
|
cdfcf0f068 | ||
|
c3676091ee | ||
|
ec19b86ade | ||
|
7bf74c6a5d | ||
|
cbb50c14e1 | ||
|
d42622a5c1 | ||
|
f9bee1485b | ||
|
c87eee1fda | ||
|
960aa90c32 | ||
|
c4210be3c7 | ||
|
28a49827ff | ||
|
736869454b | ||
|
db9084b0dc | ||
|
c39d75b448 | ||
|
71a546dd44 | ||
|
96e3d3a22c | ||
|
0ad91101a4 | ||
|
e9df58709e | ||
|
a80dcaa1c3 | ||
|
89551774db | ||
|
be58d3afb6 | ||
|
652d803739 | ||
|
2688914125 | ||
|
1d489cfcea | ||
|
cb55ce084c | ||
|
dae7da0e4e | ||
|
e4630e6a0f | ||
|
6d9abf11b8 | ||
|
7e8def50aa | ||
|
2b7f72deec | ||
|
9b1f25140e | ||
|
f4caa0029e | ||
|
15e046b3ce | ||
|
eeef92b068 | ||
|
a1418fe33c | ||
|
8f21e736dd | ||
|
222301307f | ||
|
1b19fdfe11 | ||
|
1f2ef1cdb7 | ||
|
c394b21423 | ||
|
696e84ea88 | ||
|
4a17dca7b9 | ||
|
9bcfe6461d | ||
|
4a82a91f2d | ||
|
24cd905911 | ||
|
6d62ab4257 | ||
|
f6ff32f339 | ||
|
bc523d302b | ||
|
1facbb2906 | ||
|
8265436437 | ||
|
96545bd523 | ||
|
e773ed2912 | ||
|
9949fc4646 | ||
|
d88da1f6ab | ||
|
fe8e3f2bcf | ||
|
4b9d753254 | ||
|
5ba385b74d | ||
|
ec655f5182 | ||
|
093df395c7 | ||
|
92d2373d77 | ||
|
319959ad6e | ||
|
de1e4b4c6c | ||
|
c2e79d22d2 | ||
|
596c9b1d27 | ||
|
40223e6b3f | ||
|
eec1dd6448 | ||
|
6c1261d28d | ||
|
4697ea1c1a | ||
|
6e20031dce | ||
|
a5fe9bc6d6 | ||
|
637660df2a | ||
|
e1549d109e | ||
|
5f8fc3d155 | ||
|
fce3b3ce7b | ||
|
074bd9f045 | ||
|
b1ea26467d | ||
|
48ebd74859 | ||
|
ef5b7ce853 | ||
|
e0053d57f7 | ||
|
06268543d0 | ||
|
58eadd6d7b | ||
|
f250594e97 | ||
|
c1b6828ed4 | ||
|
328ecd1cfb | ||
|
6a74d62e98 | ||
|
35ccfb14c2 |
2
.github/workflows/sync.yml
vendored
2
.github/workflows/sync.yml
vendored
@@ -5,7 +5,7 @@ permissions:
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 * * * *" # every hour
|
||||
- cron: "0 0 * * *" # every day
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
@@ -8,7 +8,7 @@ WORKDIR /app
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
RUN yarn config set registry 'https://registry.npm.taobao.org'
|
||||
RUN yarn config set registry 'https://registry.npmmirror.com/'
|
||||
RUN yarn install
|
||||
|
||||
FROM base AS builder
|
||||
|
35
README.md
35
README.md
@@ -9,10 +9,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
|
||||
一键免费部署你的私人 ChatGPT 网页应用。
|
||||
|
||||
[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/234462588-e8eff256-f5ca-46ef-8f5f-d7db6d28735a.jpg) / [打赏开发者](https://user-images.githubusercontent.com/16968934/227772541-5bcd52d8-61b7-488c-a203-0330d8006e2b.jpg)
|
||||
[Demo](https://chatgpt.nextweb.fun/) / [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://chatgpt.nextweb.fun/) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [QQ 群](https://user-images.githubusercontent.com/16968934/236402186-fa76e930-64f5-47ae-b967-b0f04b1fbf56.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)
|
||||
|
||||
@@ -26,13 +25,13 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
|
||||
- **Deploy for free with one-click** on Vercel in under 1 minute
|
||||
- Privacy first, all data stored locally in the browser
|
||||
- Markdown support: LaTex, mermaid, code highlight, etc.
|
||||
- Responsive design, dark mode and PWA
|
||||
- Fast first screen loading speed (~100kb), support streaming response
|
||||
- New in v2: create, share and debug your chat tools with prompt templates (mask)
|
||||
- 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
|
||||
- One-click export all chat history with full Markdown support
|
||||
- I18n supported
|
||||
- I18n: English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -50,18 +49,20 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
- UI text customize
|
||||
|
||||
## What's New
|
||||
|
||||
- 🚀 v2.0 is released, now you can create prompt templates, turn your ideas into reality! Read this: [ChatGPT Prompt Engineering Tips: Zero, One and Few Shot Prompting](https://www.allabtai.com/prompt-engineering-tips-zero-one-and-few-shot-prompting/).
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 在 1 分钟内使用 Vercel **免费一键部署**
|
||||
- 完整的 Markdown 支持:LaTex 公式、Mermaid 流程图、代码高亮等等
|
||||
- 精心设计的 UI,响应式设计,支持深色模式,支持 PWA
|
||||
- 极快的首屏加载速度(~100kb),支持流式响应
|
||||
- 隐私安全,所有数据保存在用户浏览器本地
|
||||
- 预制角色功能(面具),方便地创建、分享和调试你的个性化对话
|
||||
- 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts)
|
||||
- 自动压缩上下文聊天记录,在节省 Token 的同时支持超长对话
|
||||
- 一键导出聊天记录,完整的 Markdown 支持
|
||||
- 多国语言支持:English, 简体中文, 繁体中文, 日本語, Español, Italiano, Türkçe, Deutsch, Tiếng Việt, Русский, Čeština
|
||||
- 拥有自己的域名?好上加好,绑定后即可在任何地方**无障碍**快速访问
|
||||
|
||||
## 开发计划
|
||||
@@ -80,9 +81,9 @@ One-Click to deploy well-designed ChatGPT web UI on Vercel.
|
||||
- 用户登录、账号管理、消息云同步
|
||||
|
||||
## 最新动态
|
||||
|
||||
- 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。
|
||||
|
||||
|
||||
- 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com
|
||||
|
||||
## Get Started
|
||||
|
||||
@@ -163,6 +164,18 @@ Override openai api request base url.
|
||||
|
||||
Specify OpenAI organization ID.
|
||||
|
||||
### `HIDE_USER_API_KEY` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do not want users to input their own API key, set this value to 1.
|
||||
|
||||
### `DISABLE_GPT4` (optional)
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
## Development
|
||||
|
||||
> [简体中文 > 如何进行二次开发](./README_CN.md#开发)
|
||||
@@ -248,7 +261,11 @@ bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/s
|
||||
[@hotic](https://github.com/hotic)
|
||||
[@WingCH](https://github.com/WingCH)
|
||||
[@jtung4](https://github.com/jtung4)
|
||||
|
||||
[@micozhu](https://github.com/micozhu)
|
||||
[@jhansion](https://github.com/jhansion)
|
||||
[@Sha1rholder](https://github.com/Sha1rholder)
|
||||
[@AnsonHyq](https://github.com/AnsonHyq)
|
||||
[@synwith](https://github.com/synwith)
|
||||
|
||||
### Contributor
|
||||
|
||||
|
18
README_CN.md
18
README_CN.md
@@ -33,6 +33,7 @@
|
||||
- 在 Vercel 重新选择并部署,[请查看详细教程](./docs/vercel-cn.md#如何新建项目)。
|
||||
|
||||
### 打开自动更新
|
||||
|
||||
> 如果你遇到了 Upstream Sync 执行错误,请手动 Sync Fork 一次!
|
||||
|
||||
当你 fork 项目之后,由于 Github 的限制,需要手动去你 fork 后的项目的 Actions 页面启用 Workflows,并启用 Upstream Sync Action,启用之后即可开启每小时定时自动更新:
|
||||
@@ -63,7 +64,7 @@ code1,code2,code3
|
||||
|
||||
## 环境变量
|
||||
|
||||
> 本项目大多数配置项都通过环境变量来设置。
|
||||
> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)。
|
||||
|
||||
### `OPENAI_API_KEY` (必填项)
|
||||
|
||||
@@ -89,6 +90,14 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
|
||||
|
||||
指定 OpenAI 中的组织 ID。
|
||||
|
||||
### `HIDE_USER_API_KEY` (可选)
|
||||
|
||||
如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
|
||||
|
||||
### `DISABLE_GPT4` (可选)
|
||||
|
||||
如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
|
||||
|
||||
## 开发
|
||||
|
||||
> 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。
|
||||
@@ -106,15 +115,16 @@ OPENAI_API_KEY=<your api key here>
|
||||
### 本地开发
|
||||
|
||||
1. 安装 nodejs 18 和 yarn,具体细节请询问 ChatGPT;
|
||||
2. 执行 `yarn install && yarn dev` 即可。⚠️注意:此命令仅用于本地开发,不要用于部署!
|
||||
2. 执行 `yarn install && yarn dev` 即可。⚠️ 注意:此命令仅用于本地开发,不要用于部署!
|
||||
3. 如果你想本地部署,请使用 `yarn install && yarn start` 命令,你可以配合 pm2 来守护进程,防止被杀死,详情询问 ChatGPT。
|
||||
|
||||
## 部署
|
||||
|
||||
### 容器部署 (推荐)
|
||||
|
||||
> Docker 版本需要在 20 及其以上,否则会提示找不到镜像。
|
||||
|
||||
> ⚠️注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
|
||||
> ⚠️ 注意:docker 版本在大多数时间都会落后最新的版本 1 到 2 天,所以部署后会持续出现“存在更新”的提示,属于正常现象。
|
||||
|
||||
```shell
|
||||
docker pull yidadaa/chatgpt-next-web
|
||||
@@ -146,7 +156,7 @@ docker run -d -p 3000:3000 \
|
||||
bash <(curl -s https://raw.githubusercontent.com/Yidadaa/ChatGPT-Next-Web/main/scripts/setup.sh)
|
||||
```
|
||||
|
||||
⚠️注意:如果你安装过程中遇到了问题,请使用 docker 部署。
|
||||
⚠️ 注意:如果你安装过程中遇到了问题,请使用 docker 部署。
|
||||
|
||||
## 鸣谢
|
||||
|
||||
|
71
app/api/auth.ts
Normal file
71
app/api/auth.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import md5 from "spark-md5";
|
||||
import { ACCESS_CODE_PREFIX } from "../constant";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function parseApiKey(bearToken: string) {
|
||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||
const isOpenAiKey = !token.startsWith(ACCESS_CODE_PREFIX);
|
||||
|
||||
return {
|
||||
accessCode: isOpenAiKey ? "" : token.slice(ACCESS_CODE_PREFIX.length),
|
||||
apiKey: isOpenAiKey ? token : "",
|
||||
};
|
||||
}
|
||||
|
||||
export function auth(req: NextRequest) {
|
||||
const authToken = req.headers.get("Authorization") ?? "";
|
||||
|
||||
// check if it is openai api key or user token
|
||||
const { accessCode, apiKey: token } = parseApiKey(authToken);
|
||||
|
||||
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||
|
||||
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
|
||||
console.log("[Auth] got access code:", accessCode);
|
||||
console.log("[Auth] hashed access code:", hashedCode);
|
||||
console.log("[User IP] ", getIP(req));
|
||||
console.log("[Time] ", new Date().toLocaleString());
|
||||
|
||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
|
||||
return {
|
||||
error: true,
|
||||
needAccessCode: true,
|
||||
msg: "Please go settings page and fill your access code.",
|
||||
};
|
||||
}
|
||||
|
||||
// if user does not provide an api key, inject system api key
|
||||
if (!token) {
|
||||
const apiKey = serverConfig.apiKey;
|
||||
if (apiKey) {
|
||||
console.log("[Auth] use system api key");
|
||||
req.headers.set("Authorization", `Bearer ${apiKey}`);
|
||||
} else {
|
||||
console.log("[Auth] admin did not provide an api key");
|
||||
return {
|
||||
error: true,
|
||||
msg: "Empty Api Key",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
console.log("[Auth] use user api key");
|
||||
}
|
||||
|
||||
return {
|
||||
error: false,
|
||||
};
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
import { createParser } from "eventsource-parser";
|
||||
import { NextRequest } from "next/server";
|
||||
import { requestOpenai } from "../common";
|
||||
|
||||
async function createStream(req: NextRequest) {
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const res = await requestOpenai(req);
|
||||
|
||||
const contentType = res.headers.get("Content-Type") ?? "";
|
||||
if (!contentType.includes("stream")) {
|
||||
const content = await (
|
||||
await res.text()
|
||||
).replace(/provided:.*. You/, "provided: ***. You");
|
||||
console.log("[Stream] error ", content);
|
||||
return "```json\n" + content + "```";
|
||||
}
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
function onParse(event: any) {
|
||||
if (event.type === "event") {
|
||||
const data = event.data;
|
||||
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
|
||||
if (data === "[DONE]") {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const text = json.choices[0].delta.content;
|
||||
const queue = encoder.encode(text);
|
||||
controller.enqueue(queue);
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parser = createParser(onParse);
|
||||
for await (const chunk of res.body as any) {
|
||||
parser.feed(decoder.decode(chunk, { stream: true }));
|
||||
}
|
||||
},
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const stream = await createStream(req);
|
||||
return new Response(stream);
|
||||
} catch (error) {
|
||||
console.error("[Chat Stream]", error);
|
||||
return new Response(
|
||||
["```json\n", JSON.stringify(error, null, " "), "\n```"].join(""),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const runtime = "edge";
|
@@ -6,8 +6,11 @@ const PROTOCOL = process.env.PROTOCOL ?? DEFAULT_PROTOCOL;
|
||||
const BASE_URL = process.env.BASE_URL ?? OPENAI_URL;
|
||||
|
||||
export async function requestOpenai(req: NextRequest) {
|
||||
const apiKey = req.headers.get("token");
|
||||
const openaiPath = req.headers.get("path");
|
||||
const authValue = req.headers.get("Authorization") ?? "";
|
||||
const openaiPath = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll(
|
||||
"/api/openai/",
|
||||
"",
|
||||
);
|
||||
|
||||
let baseUrl = BASE_URL;
|
||||
|
||||
@@ -22,10 +25,14 @@ export async function requestOpenai(req: NextRequest) {
|
||||
console.log("[Org ID]", process.env.OPENAI_ORG_ID);
|
||||
}
|
||||
|
||||
if (!authValue || !authValue.startsWith("Bearer sk-")) {
|
||||
console.error("[OpenAI Request] invalid api key provided", authValue);
|
||||
}
|
||||
|
||||
return fetch(`${baseUrl}/${openaiPath}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Authorization: authValue,
|
||||
...(process.env.OPENAI_ORG_ID && {
|
||||
"OpenAI-Organization": process.env.OPENAI_ORG_ID,
|
||||
}),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import { getServerSideConfig } from "../../config/server";
|
||||
|
||||
@@ -8,16 +8,19 @@ const serverConfig = getServerSideConfig();
|
||||
// 警告!不要在这里写入任何敏感信息!
|
||||
const DANGER_CONFIG = {
|
||||
needCode: serverConfig.needCode,
|
||||
hideUserApiKey: serverConfig.hideUserApiKey,
|
||||
enableGPT4: serverConfig.enableGPT4,
|
||||
};
|
||||
|
||||
declare global {
|
||||
type DangerConfig = typeof DANGER_CONFIG;
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({
|
||||
needCode: serverConfig.needCode,
|
||||
});
|
||||
async function handle() {
|
||||
return NextResponse.json(DANGER_CONFIG);
|
||||
}
|
||||
|
||||
export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
||||
|
101
app/api/openai/[...path]/route.ts
Normal file
101
app/api/openai/[...path]/route.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { createParser } from "eventsource-parser";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { requestOpenai } from "../../common";
|
||||
|
||||
async function createStream(res: Response) {
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
function onParse(event: any) {
|
||||
if (event.type === "event") {
|
||||
const data = event.data;
|
||||
// https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream
|
||||
if (data === "[DONE]") {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const text = json.choices[0].delta.content;
|
||||
const queue = encoder.encode(text);
|
||||
controller.enqueue(queue);
|
||||
} catch (e) {
|
||||
controller.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parser = createParser(onParse);
|
||||
for await (const chunk of res.body as any) {
|
||||
parser.feed(decoder.decode(chunk, { stream: true }));
|
||||
}
|
||||
},
|
||||
});
|
||||
return stream;
|
||||
}
|
||||
|
||||
function formatResponse(msg: any) {
|
||||
const jsonMsg = ["```json\n", JSON.stringify(msg, null, " "), "\n```"].join(
|
||||
"",
|
||||
);
|
||||
return new Response(jsonMsg);
|
||||
}
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[OpenAI Route] params ", params);
|
||||
|
||||
const authResult = auth(req);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const api = await requestOpenai(req);
|
||||
|
||||
const contentType = api.headers.get("Content-Type") ?? "";
|
||||
|
||||
// streaming response
|
||||
if (contentType.includes("stream")) {
|
||||
const stream = await createStream(api);
|
||||
const res = new Response(stream);
|
||||
res.headers.set("Content-Type", contentType);
|
||||
return res;
|
||||
}
|
||||
|
||||
// try to parse error msg
|
||||
try {
|
||||
const mayBeErrorBody = await api.json();
|
||||
if (mayBeErrorBody.error) {
|
||||
console.error("[OpenAI Response] ", mayBeErrorBody);
|
||||
return formatResponse(mayBeErrorBody);
|
||||
} else {
|
||||
const res = new Response(JSON.stringify(mayBeErrorBody));
|
||||
res.headers.set("Content-Type", "application/json");
|
||||
res.headers.set("Cache-Control", "no-cache");
|
||||
return res;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[OpenAI Parse] ", e);
|
||||
return formatResponse({
|
||||
msg: "invalid response from openai server",
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] ", e);
|
||||
return formatResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
@@ -1,33 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requestOpenai } from "../common";
|
||||
|
||||
async function makeRequest(req: NextRequest) {
|
||||
try {
|
||||
const api = await requestOpenai(req);
|
||||
const res = new NextResponse(api.body);
|
||||
res.headers.set("Content-Type", "application/json");
|
||||
res.headers.set("Cache-Control", "no-cache");
|
||||
return res;
|
||||
} catch (e) {
|
||||
console.error("[OpenAI] ", req.body, e);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: JSON.stringify(e),
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
return makeRequest(req);
|
||||
}
|
||||
|
||||
export const runtime = "edge";
|
28
app/command.ts
Normal file
28
app/command.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
type Command = (param: string) => void;
|
||||
interface Commands {
|
||||
fill?: Command;
|
||||
submit?: Command;
|
||||
mask?: Command;
|
||||
}
|
||||
|
||||
export function useCommand(commands: Commands = {}) {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
if (commands === undefined) return;
|
||||
|
||||
let shouldUpdate = false;
|
||||
searchParams.forEach((param, name) => {
|
||||
const commandName = name as keyof Commands;
|
||||
if (typeof commands[commandName] === "function") {
|
||||
commands[commandName]!(param);
|
||||
searchParams.delete(name);
|
||||
shouldUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldUpdate) {
|
||||
setSearchParams(searchParams);
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { Mask } from "../store/mask";
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
@@ -29,6 +30,14 @@ export function ChatItem(props: {
|
||||
narrow?: boolean;
|
||||
mask: Mask;
|
||||
}) {
|
||||
const draggableRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (props.selected && draggableRef.current) {
|
||||
draggableRef.current?.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}, [props.selected]);
|
||||
return (
|
||||
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||
{(provided) => (
|
||||
@@ -37,7 +46,10 @@ export function ChatItem(props: {
|
||||
props.selected && styles["chat-item-selected"]
|
||||
}`}
|
||||
onClick={props.onClick}
|
||||
ref={provided.innerRef}
|
||||
ref={(ele) => {
|
||||
draggableRef.current = ele;
|
||||
provided.innerRef(ele);
|
||||
}}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
title={`${props.title}\n${Locale.ChatItem.ChatItemCount(
|
||||
|
@@ -26,12 +26,10 @@ import {
|
||||
SubmitKey,
|
||||
useChatStore,
|
||||
BOT_HELLO,
|
||||
ROLES,
|
||||
createMessage,
|
||||
useAccessStore,
|
||||
Theme,
|
||||
useAppConfig,
|
||||
ModelConfig,
|
||||
DEFAULT_TOPIC,
|
||||
} from "../store";
|
||||
|
||||
@@ -54,15 +52,12 @@ import styles from "./home.module.scss";
|
||||
import chatStyle from "./chat.module.scss";
|
||||
|
||||
import { ListItem, Modal, showModal } from "./ui-lib";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { LAST_INPUT_KEY, Path } from "../constant";
|
||||
import { Avatar } from "./emoji";
|
||||
import { MaskAvatar, MaskConfig } from "./mask";
|
||||
import {
|
||||
DEFAULT_MASK_AVATAR,
|
||||
DEFAULT_MASK_ID,
|
||||
useMaskStore,
|
||||
} from "../store/mask";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { useCommand } from "../command";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -224,15 +219,65 @@ export function PromptHints(props: {
|
||||
prompts: Prompt[];
|
||||
onPromptSelect: (prompt: Prompt) => void;
|
||||
}) {
|
||||
if (props.prompts.length === 0) return null;
|
||||
const noPrompts = props.prompts.length === 0;
|
||||
const [selectIndex, setSelectIndex] = useState(0);
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectIndex(0);
|
||||
}, [props.prompts.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (noPrompts) return;
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
// arrow up / down to select prompt
|
||||
const changeIndex = (delta: number) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const nextIndex = Math.max(
|
||||
0,
|
||||
Math.min(props.prompts.length - 1, selectIndex + delta),
|
||||
);
|
||||
setSelectIndex(nextIndex);
|
||||
selectedRef.current?.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
changeIndex(1);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
changeIndex(-1);
|
||||
} else if (e.key === "Enter") {
|
||||
const selectedPrompt = props.prompts.at(selectIndex);
|
||||
if (selectedPrompt) {
|
||||
props.onPromptSelect(selectedPrompt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [noPrompts, selectIndex]);
|
||||
|
||||
if (noPrompts) return null;
|
||||
return (
|
||||
<div className={styles["prompt-hints"]}>
|
||||
{props.prompts.map((prompt, i) => (
|
||||
<div
|
||||
className={styles["prompt-hint"]}
|
||||
ref={i === selectIndex ? selectedRef : null}
|
||||
className={
|
||||
styles["prompt-hint"] +
|
||||
` ${i === selectIndex ? styles["prompt-hint-selected"] : ""}`
|
||||
}
|
||||
key={prompt.title + i.toString()}
|
||||
onClick={() => props.onPromptSelect(prompt)}
|
||||
onMouseEnter={() => setSelectIndex(i)}
|
||||
>
|
||||
<div className={styles["hint-title"]}>{prompt.title}</div>
|
||||
<div className={styles["hint-content"]}>{prompt.content}</div>
|
||||
@@ -361,7 +406,6 @@ export function Chat() {
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [userInput, setUserInput] = useState("");
|
||||
const [beforeInput, setBeforeInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||
const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
|
||||
@@ -370,7 +414,7 @@ export function Chat() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onChatBodyScroll = (e: HTMLElement) => {
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 100;
|
||||
setHitBottom(isTouchBottom);
|
||||
};
|
||||
|
||||
@@ -397,7 +441,7 @@ export function Chat() {
|
||||
() => {
|
||||
const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
|
||||
const inputRows = Math.min(
|
||||
5,
|
||||
20,
|
||||
Math.max(2 + Number(!isMobileScreen), rows),
|
||||
);
|
||||
setInputRows(inputRows);
|
||||
@@ -430,12 +474,11 @@ export function Chat() {
|
||||
}
|
||||
};
|
||||
|
||||
// submit user input
|
||||
const onUserSubmit = () => {
|
||||
if (userInput.length <= 0) return;
|
||||
const doSubmit = (userInput: string) => {
|
||||
if (userInput.trim() === "") return;
|
||||
setIsLoading(true);
|
||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||
setBeforeInput(userInput);
|
||||
localStorage.setItem(LAST_INPUT_KEY, userInput);
|
||||
setUserInput("");
|
||||
setPromptHints([]);
|
||||
if (!isMobileScreen) inputRef.current?.focus();
|
||||
@@ -449,23 +492,22 @@ export function Chat() {
|
||||
|
||||
// check if should send message
|
||||
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// if ArrowUp and no userInput
|
||||
if (e.key === "ArrowUp" && userInput.length <= 0) {
|
||||
setUserInput(beforeInput);
|
||||
// if ArrowUp and no userInput, fill with last input
|
||||
if (
|
||||
e.key === "ArrowUp" &&
|
||||
userInput.length <= 0 &&
|
||||
!(e.metaKey || e.altKey || e.ctrlKey)
|
||||
) {
|
||||
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (shouldSubmit(e)) {
|
||||
onUserSubmit();
|
||||
doSubmit(userInput);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
const onRightClick = (e: any, message: Message) => {
|
||||
// auto fill user input
|
||||
if (message.role === "user") {
|
||||
setUserInput(message.content);
|
||||
}
|
||||
|
||||
// copy to clipboard
|
||||
if (selectOrCopy(e.currentTarget, message.content)) {
|
||||
e.preventDefault();
|
||||
@@ -566,12 +608,16 @@ export function Chat() {
|
||||
}
|
||||
};
|
||||
|
||||
// Auto focus
|
||||
useEffect(() => {
|
||||
if (isMobileScreen) return;
|
||||
inputRef.current?.focus();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const location = useLocation();
|
||||
const isChat = location.pathname === Path.Chat;
|
||||
const autoFocus = !isMobileScreen || isChat; // only focus in chat page
|
||||
|
||||
useCommand({
|
||||
fill: setUserInput,
|
||||
submit: (text) => {
|
||||
doSubmit(text);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
@@ -749,7 +795,14 @@ export function Chat() {
|
||||
scrollToBottom={scrollToBottom}
|
||||
hitBottom={hitBottom}
|
||||
showPromptHints={() => {
|
||||
// Click again to close
|
||||
if (promptHints.length > 0) {
|
||||
setPromptHints([]);
|
||||
return;
|
||||
}
|
||||
|
||||
inputRef.current?.focus();
|
||||
setUserInput("/");
|
||||
onSearch("");
|
||||
}}
|
||||
/>
|
||||
@@ -762,23 +815,16 @@ export function Chat() {
|
||||
value={userInput}
|
||||
onKeyDown={onInputKeyDown}
|
||||
onFocus={() => setAutoScroll(true)}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
if (document.activeElement !== inputRef.current) {
|
||||
setAutoScroll(false);
|
||||
setPromptHints([]);
|
||||
}
|
||||
}, 100);
|
||||
}}
|
||||
autoFocus
|
||||
onBlur={() => setAutoScroll(false)}
|
||||
rows={inputRows}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={onUserSubmit}
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -186,7 +186,7 @@
|
||||
.chat-item-delete {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: -20px;
|
||||
right: 0;
|
||||
transition: all ease 0.3s;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
.chat-item:hover > .chat-item-delete {
|
||||
opacity: 0.5;
|
||||
right: 10px;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.chat-item:hover > .chat-item-delete:hover {
|
||||
@@ -335,6 +335,7 @@
|
||||
padding: 20px;
|
||||
padding-bottom: 40px;
|
||||
position: relative;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.chat-body-title {
|
||||
|
@@ -63,17 +63,17 @@ export function useSwitchTheme() {
|
||||
}
|
||||
|
||||
const metaDescriptionDark = document.querySelector(
|
||||
'meta[name="theme-color"][media]',
|
||||
'meta[name="theme-color"][media*="dark"]',
|
||||
);
|
||||
const metaDescriptionLight = document.querySelector(
|
||||
'meta[name="theme-color"]:not([media])',
|
||||
'meta[name="theme-color"][media*="light"]',
|
||||
);
|
||||
|
||||
if (config.theme === "auto") {
|
||||
metaDescriptionDark?.setAttribute("content", "#151515");
|
||||
metaDescriptionLight?.setAttribute("content", "#fafafa");
|
||||
} else {
|
||||
const themeColor = getCSSVar("--themeColor");
|
||||
const themeColor = getCSSVar("--theme-color");
|
||||
metaDescriptionDark?.setAttribute("content", themeColor);
|
||||
metaDescriptionLight?.setAttribute("content", themeColor);
|
||||
}
|
||||
@@ -90,12 +90,24 @@ const useHasHydrated = () => {
|
||||
return hasHydrated;
|
||||
};
|
||||
|
||||
const loadAsyncGoogleFont = () => {
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.rel = "stylesheet";
|
||||
linkEl.href =
|
||||
"/google-fonts/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap";
|
||||
document.head.appendChild(linkEl);
|
||||
};
|
||||
|
||||
function Screen() {
|
||||
const config = useAppConfig();
|
||||
const location = useLocation();
|
||||
const isHome = location.pathname === Path.Home;
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
useEffect(() => {
|
||||
loadAsyncGoogleFont();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
@@ -7,12 +7,67 @@ import RemarkGfm from "remark-gfm";
|
||||
import RehypeHighlight from "rehype-highlight";
|
||||
import { useRef, useState, RefObject, useEffect } from "react";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import mermaid from "mermaid";
|
||||
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import React from "react";
|
||||
|
||||
export function Mermaid(props: { code: string; onError: () => void }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.code && ref.current) {
|
||||
mermaid
|
||||
.run({
|
||||
nodes: [ref.current],
|
||||
})
|
||||
.catch((e) => {
|
||||
props.onError();
|
||||
console.error("[Mermaid] ", e.message);
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.code]);
|
||||
|
||||
function viewSvgInNewWindow() {
|
||||
const svg = ref.current?.querySelector("svg");
|
||||
if (!svg) return;
|
||||
const text = new XMLSerializer().serializeToString(svg);
|
||||
const blob = new Blob([text], { type: "image/svg+xml" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const win = window.open(url);
|
||||
if (win) {
|
||||
win.onload = () => URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="no-dark"
|
||||
style={{ cursor: "pointer", overflow: "auto" }}
|
||||
ref={ref}
|
||||
onClick={() => viewSvgInNewWindow()}
|
||||
>
|
||||
{props.code}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const [mermaidCode, setMermaidCode] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const mermaidDom = ref.current.querySelector("code.language-mermaid");
|
||||
if (mermaidDom) {
|
||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||
}
|
||||
}, [props.children]);
|
||||
|
||||
if (mermaidCode) {
|
||||
return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<pre ref={ref}>
|
||||
@@ -82,10 +137,12 @@ export function Markdown(
|
||||
const parentBounds = parent.getBoundingClientRect();
|
||||
const twoScreenHeight = Math.max(500, parentBounds.height * 2);
|
||||
const mdBounds = md.getBoundingClientRect();
|
||||
const isInRange = (x: number) =>
|
||||
x <= parentBounds.bottom + twoScreenHeight &&
|
||||
x >= parentBounds.top - twoScreenHeight;
|
||||
inView.current = isInRange(mdBounds.top) || isInRange(mdBounds.bottom);
|
||||
const parentTop = parentBounds.top - twoScreenHeight;
|
||||
const parentBottom = parentBounds.bottom + twoScreenHeight;
|
||||
const isOverlap =
|
||||
Math.max(parentTop, mdBounds.top) <=
|
||||
Math.min(parentBottom, mdBounds.bottom);
|
||||
inView.current = isOverlap;
|
||||
}
|
||||
|
||||
if (inView.current && md) {
|
||||
@@ -96,7 +153,7 @@ export function Markdown(
|
||||
}
|
||||
};
|
||||
|
||||
checkInView();
|
||||
setTimeout(() => checkInView(), 1);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@@ -14,14 +14,14 @@ import CopyIcon from "../icons/copy.svg";
|
||||
|
||||
import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
|
||||
import { Message, ModelConfig, ROLES, useChatStore } from "../store";
|
||||
import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
|
||||
import { Input, List, ListItem, Modal, Popover, Select } from "./ui-lib";
|
||||
import { Avatar, AvatarPicker } from "./emoji";
|
||||
import Locale, { AllLangs, Lang } from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import chatStyle from "./chat.module.scss";
|
||||
import { useEffect, useState } from "react";
|
||||
import { downloadAs } from "../utils";
|
||||
import { useState } from "react";
|
||||
import { downloadAs, readFromFile } from "../utils";
|
||||
import { Updater } from "../api/openai/typing";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
import { FileName, Path } from "../constant";
|
||||
@@ -106,6 +106,59 @@ export function MaskConfig(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function ContextPromptItem(props: {
|
||||
prompt: Message;
|
||||
update: (prompt: Message) => void;
|
||||
remove: () => void;
|
||||
}) {
|
||||
const [focusingInput, setFocusingInput] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
{!focusingInput && (
|
||||
<Select
|
||||
value={props.prompt.role}
|
||||
className={chatStyle["context-role"]}
|
||||
onChange={(e) =>
|
||||
props.update({
|
||||
...props.prompt,
|
||||
role: e.target.value as any,
|
||||
})
|
||||
}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<Input
|
||||
value={props.prompt.content}
|
||||
type="text"
|
||||
className={chatStyle["context-content"]}
|
||||
rows={focusingInput ? 5 : 1}
|
||||
onFocus={() => setFocusingInput(true)}
|
||||
onBlur={() => setFocusingInput(false)}
|
||||
onInput={(e) =>
|
||||
props.update({
|
||||
...props.prompt,
|
||||
content: e.currentTarget.value as any,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{!focusingInput && (
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
className={chatStyle["context-delete-button"]}
|
||||
onClick={() => props.remove()}
|
||||
bordered
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContextPrompts(props: {
|
||||
context: Message[];
|
||||
updateContext: (updater: (context: Message[]) => void) => void;
|
||||
@@ -128,42 +181,12 @@ export function ContextPrompts(props: {
|
||||
<>
|
||||
<div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
|
||||
{context.map((c, i) => (
|
||||
<div className={chatStyle["context-prompt-row"]} key={i}>
|
||||
<select
|
||||
value={c.role}
|
||||
className={chatStyle["context-role"]}
|
||||
onChange={(e) =>
|
||||
updateContextPrompt(i, {
|
||||
...c,
|
||||
role: e.target.value as any,
|
||||
})
|
||||
}
|
||||
>
|
||||
{ROLES.map((r) => (
|
||||
<option key={r} value={r}>
|
||||
{r}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Input
|
||||
value={c.content}
|
||||
type="text"
|
||||
className={chatStyle["context-content"]}
|
||||
rows={1}
|
||||
onInput={(e) =>
|
||||
updateContextPrompt(i, {
|
||||
...c,
|
||||
content: e.currentTarget.value as any,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
className={chatStyle["context-delete-button"]}
|
||||
onClick={() => removeContextPrompt(i)}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
<ContextPromptItem
|
||||
key={i}
|
||||
prompt={c}
|
||||
update={(prompt) => updateContextPrompt(i, prompt)}
|
||||
remove={() => removeContextPrompt(i)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className={chatStyle["context-prompt-row"]}>
|
||||
@@ -174,7 +197,7 @@ export function ContextPrompts(props: {
|
||||
className={chatStyle["context-prompt-button"]}
|
||||
onClick={() =>
|
||||
addContextPrompt({
|
||||
role: "system",
|
||||
role: "user",
|
||||
content: "",
|
||||
date: "",
|
||||
})
|
||||
@@ -222,6 +245,21 @@ export function MaskPage() {
|
||||
downloadAs(JSON.stringify(masks), FileName.Masks);
|
||||
};
|
||||
|
||||
const importFromFile = () => {
|
||||
readFromFile().then((content) => {
|
||||
try {
|
||||
const importMasks = JSON.parse(content);
|
||||
if (Array.isArray(importMasks)) {
|
||||
for (const mask of importMasks) {
|
||||
if (mask.name) {
|
||||
maskStore.create(mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className={styles["mask-page"]}>
|
||||
@@ -247,7 +285,7 @@ export function MaskPage() {
|
||||
<IconButton
|
||||
icon={<UploadIcon />}
|
||||
bordered
|
||||
onClick={() => showToast(Locale.WIP)}
|
||||
onClick={() => importFromFile()}
|
||||
/>
|
||||
</div>
|
||||
<div className="window-action-button">
|
||||
@@ -269,7 +307,7 @@ export function MaskPage() {
|
||||
autoFocus
|
||||
onInput={(e) => onSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<select
|
||||
<Select
|
||||
className={styles["mask-filter-lang"]}
|
||||
value={filterLang ?? Locale.Settings.Lang.All}
|
||||
onChange={(e) => {
|
||||
@@ -289,7 +327,7 @@ export function MaskPage() {
|
||||
{Locale.Settings.Lang.Options[lang]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
|
||||
<IconButton
|
||||
className={styles["mask-create"]}
|
||||
@@ -371,7 +409,10 @@ export function MaskPage() {
|
||||
key="export"
|
||||
bordered
|
||||
onClick={() =>
|
||||
downloadAs(JSON.stringify(editingMask), "mask.json")
|
||||
downloadAs(
|
||||
JSON.stringify(editingMask),
|
||||
`${editingMask.name}.json`,
|
||||
)
|
||||
}
|
||||
/>,
|
||||
<IconButton
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import styles from "./settings.module.scss";
|
||||
import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { InputRange } from "./input-range";
|
||||
import { List, ListItem } from "./ui-lib";
|
||||
import { List, ListItem, Select } from "./ui-lib";
|
||||
|
||||
export function ModelConfigList(props: {
|
||||
modelConfig: ModelConfig;
|
||||
@@ -12,7 +11,7 @@ export function ModelConfigList(props: {
|
||||
return (
|
||||
<>
|
||||
<ListItem title={Locale.Settings.Model}>
|
||||
<select
|
||||
<Select
|
||||
value={props.modelConfig.model}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
@@ -28,7 +27,7 @@ export function ModelConfigList(props: {
|
||||
{v.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Temperature.Title}
|
||||
|
@@ -13,6 +13,7 @@ import { Mask, useMaskStore } from "../store/mask";
|
||||
import Locale from "../locales";
|
||||
import { useAppConfig, useChatStore } from "../store";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { useCommand } from "../command";
|
||||
|
||||
function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
|
||||
const xmin = Math.max(aRect.x, bRect.x);
|
||||
@@ -108,9 +109,20 @@ export function NewChat() {
|
||||
|
||||
const startChat = (mask?: Mask) => {
|
||||
chatStore.newSession(mask);
|
||||
navigate(Path.Chat);
|
||||
setTimeout(() => navigate(Path.Chat), 1);
|
||||
};
|
||||
|
||||
useCommand({
|
||||
mask: (id) => {
|
||||
try {
|
||||
const mask = maskStore.get(parseInt(id));
|
||||
startChat(mask ?? undefined);
|
||||
} catch {
|
||||
console.error("[New Chat] failed to create chat from mask id=", id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles["new-chat"]}>
|
||||
<div className={styles["mask-header"]}>
|
||||
|
@@ -60,18 +60,13 @@
|
||||
.user-prompt-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
|
||||
.user-prompt-button {
|
||||
height: 100%;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
//height: 100%;
|
||||
padding: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-prompt-actions {
|
||||
}
|
||||
}
|
||||
|
@@ -7,9 +7,18 @@ import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import ClearIcon from "../icons/clear.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import EditIcon from "../icons/edit.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import { Input, List, ListItem, Modal, PasswordInput, Popover } from "./ui-lib";
|
||||
import {
|
||||
Input,
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
PasswordInput,
|
||||
Popover,
|
||||
Select,
|
||||
} from "./ui-lib";
|
||||
import { ModelConfigList } from "./model-config";
|
||||
|
||||
import { IconButton } from "./button";
|
||||
@@ -183,6 +192,19 @@ function UserPromptModal(props: { onClose?: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatVersionDate(t: string) {
|
||||
const d = new Date(+t);
|
||||
const year = d.getUTCFullYear();
|
||||
const month = d.getUTCMonth() + 1;
|
||||
const day = d.getUTCDate();
|
||||
|
||||
return [
|
||||
year.toString(),
|
||||
month.toString().padStart(2, "0"),
|
||||
day.toString().padStart(2, "0"),
|
||||
].join("");
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const navigate = useNavigate();
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
@@ -193,8 +215,8 @@ export function Settings() {
|
||||
|
||||
const updateStore = useUpdateStore();
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentVersion = updateStore.version;
|
||||
const remoteId = updateStore.remoteVersion;
|
||||
const currentVersion = formatVersionDate(updateStore.version);
|
||||
const remoteId = formatVersionDate(updateStore.remoteVersion);
|
||||
const hasNewVersion = currentVersion !== remoteId;
|
||||
|
||||
function checkUpdate(force = false) {
|
||||
@@ -202,6 +224,15 @@ export function Settings() {
|
||||
updateStore.getLatestVersion(force).then(() => {
|
||||
setCheckingUpdate(false);
|
||||
});
|
||||
|
||||
console.log(
|
||||
"[Update] local version ",
|
||||
new Date(+updateStore.version).toLocaleString(),
|
||||
);
|
||||
console.log(
|
||||
"[Update] remote version ",
|
||||
new Date(+updateStore.remoteVersion).toLocaleString(),
|
||||
);
|
||||
}
|
||||
|
||||
const usage = {
|
||||
@@ -330,7 +361,7 @@ export function Settings() {
|
||||
}
|
||||
>
|
||||
{checkingUpdate ? (
|
||||
<div />
|
||||
<LoadingIcon />
|
||||
) : hasNewVersion ? (
|
||||
<Link href={UPDATE_URL} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
@@ -345,7 +376,7 @@ export function Settings() {
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={Locale.Settings.SendKey}>
|
||||
<select
|
||||
<Select
|
||||
value={config.submitKey}
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
@@ -359,11 +390,11 @@ export function Settings() {
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={Locale.Settings.Theme}>
|
||||
<select
|
||||
<Select
|
||||
value={config.theme}
|
||||
onChange={(e) => {
|
||||
updateConfig(
|
||||
@@ -376,11 +407,11 @@ export function Settings() {
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem title={Locale.Settings.Lang.Name}>
|
||||
<select
|
||||
<Select
|
||||
value={getLang()}
|
||||
onChange={(e) => {
|
||||
changeLang(e.target.value as any);
|
||||
@@ -391,7 +422,7 @@ export function Settings() {
|
||||
{Locale.Settings.Lang.Options[lang]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
@@ -466,19 +497,21 @@ export function Settings() {
|
||||
<></>
|
||||
)}
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Token.Title}
|
||||
subTitle={Locale.Settings.Token.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.token}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Token.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateToken(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<ListItem
|
||||
title={Locale.Settings.Token.Title}
|
||||
subTitle={Locale.Settings.Token.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.token}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Token.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.updateToken(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
) : null}
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Usage.Title}
|
||||
@@ -540,9 +573,9 @@ export function Settings() {
|
||||
<List>
|
||||
<ModelConfigList
|
||||
modelConfig={config.modelConfig}
|
||||
updateConfig={(upater) => {
|
||||
updateConfig={(updater) => {
|
||||
const modelConfig = { ...config.modelConfig };
|
||||
upater(modelConfig);
|
||||
updater(modelConfig);
|
||||
config.update((config) => (config.modelConfig = modelConfig));
|
||||
}}
|
||||
/>
|
||||
|
@@ -32,6 +32,28 @@ const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => null,
|
||||
});
|
||||
|
||||
function useHotKey() {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.metaKey || e.altKey || e.ctrlKey) {
|
||||
const n = chatStore.sessions.length;
|
||||
const limit = (x: number) => (x + n) % n;
|
||||
const i = chatStore.currentSessionIndex;
|
||||
if (e.key === "ArrowUp") {
|
||||
chatStore.selectSession(limit(i - 1));
|
||||
} else if (e.key === "ArrowDown") {
|
||||
chatStore.selectSession(limit(i + 1));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
});
|
||||
}
|
||||
|
||||
function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
||||
|
||||
@@ -86,9 +108,10 @@ export function SideBar(props: { className?: string }) {
|
||||
// drag side bar
|
||||
const { onDragMouseDown, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const config = useAppConfig();
|
||||
|
||||
useHotKey();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
@@ -163,6 +186,7 @@ export function SideBar(props: { className?: string }) {
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen) {
|
||||
chatStore.newSession();
|
||||
navigate(Path.Chat);
|
||||
} else {
|
||||
navigate(Path.NewChat);
|
||||
}
|
||||
|
@@ -124,6 +124,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.modal-container {
|
||||
width: 100vw;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.modal-content {
|
||||
max-height: 50vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.show {
|
||||
opacity: 1;
|
||||
transition: all ease 0.3s;
|
||||
@@ -192,12 +204,27 @@
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.modal-container {
|
||||
width: 90vw;
|
||||
|
||||
.modal-content {
|
||||
max-height: 50vh;
|
||||
}
|
||||
.select-with-icon {
|
||||
position: relative;
|
||||
max-width: fit-content;
|
||||
|
||||
.select-with-icon-select {
|
||||
height: 100%;
|
||||
border: var(--border-in-light);
|
||||
padding: 10px 25px 10px 10px;
|
||||
border-radius: 10px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
background-color: var(--white);
|
||||
color: var(--black);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.select-with-icon-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import EyeOffIcon from "../icons/eye-off.svg";
|
||||
import DownIcon from "../icons/down.svg";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
import React, { HTMLProps, useEffect, useState } from "react";
|
||||
@@ -244,3 +245,20 @@ export function PasswordInput(props: HTMLProps<HTMLInputElement>) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Select(
|
||||
props: React.DetailedHTMLProps<
|
||||
React.SelectHTMLAttributes<HTMLSelectElement>,
|
||||
HTMLSelectElement
|
||||
>,
|
||||
) {
|
||||
const { className, children, ...otherProps } = props;
|
||||
return (
|
||||
<div className={`${styles["select-with-icon"]} ${className}`}>
|
||||
<select className={styles["select-with-icon-select"]} {...otherProps}>
|
||||
{children}
|
||||
</select>
|
||||
<DownIcon className={styles["select-with-icon-icon"]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -1,13 +1,10 @@
|
||||
const COMMIT_ID: string = (() => {
|
||||
try {
|
||||
const childProcess = require("child_process");
|
||||
return (
|
||||
childProcess
|
||||
// .execSync("git describe --tags --abbrev=0")
|
||||
.execSync("git rev-parse --short HEAD")
|
||||
.toString()
|
||||
.trim()
|
||||
);
|
||||
return childProcess
|
||||
.execSync('git log -1 --format="%at000" --date=unix')
|
||||
.toString()
|
||||
.trim();
|
||||
} catch (e) {
|
||||
console.error("[Build Config] No git or not from git repo.");
|
||||
return "unknown";
|
||||
|
@@ -7,6 +7,8 @@ declare global {
|
||||
CODE?: string;
|
||||
PROXY_URL?: string;
|
||||
VERCEL?: string;
|
||||
HIDE_USER_API_KEY?: string; // disable user's api key input
|
||||
DISABLE_GPT4?: string; // allow user to use gpt-4 or not
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,5 +40,7 @@ export const getServerSideConfig = () => {
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
proxyUrl: process.env.PROXY_URL,
|
||||
isVercel: !!process.env.VERCEL,
|
||||
hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
|
||||
enableGPT4: !process.env.DISABLE_GPT4,
|
||||
};
|
||||
};
|
||||
|
@@ -36,3 +36,7 @@ export enum StoreKey {
|
||||
export const MAX_SIDEBAR_WIDTH = 500;
|
||||
export const MIN_SIDEBAR_WIDTH = 230;
|
||||
export const NARROW_SIDEBAR_WIDTH = 100;
|
||||
|
||||
export const ACCESS_CODE_PREFIX = "ak-";
|
||||
|
||||
export const LAST_INPUT_KEY = "last-input";
|
||||
|
1
app/icons/down.svg
Normal file
1
app/icons/down.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(-90 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(6.333333333333333 4) rotate(0 2 4)" d="M4,8L0,4L4,0 " /></g></g></svg>
|
After Width: | Height: | Size: 575 B |
@@ -9,11 +9,19 @@ const buildConfig = getBuildConfig();
|
||||
export const metadata = {
|
||||
title: "ChatGPT Next Web",
|
||||
description: "Your personal ChatGPT Chat Bot.",
|
||||
viewport: {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
},
|
||||
themeColor: [
|
||||
{ media: "(prefers-color-scheme: light)", color: "#fafafa" },
|
||||
{ media: "(prefers-color-scheme: dark)", color: "#151515" },
|
||||
],
|
||||
appleWebApp: {
|
||||
title: "ChatGPT Next Web",
|
||||
statusBarStyle: "default",
|
||||
},
|
||||
themeColor: "#fafafa",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -24,23 +32,8 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#151515"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<meta name="version" content={buildConfig.commitId} />
|
||||
<link rel="manifest" href="/site.webmanifest"></link>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"></link>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com"></link>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"
|
||||
rel="stylesheet"
|
||||
></link>
|
||||
<script src="/serviceWorkerRegister.js" defer></script>
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
|
@@ -4,7 +4,7 @@ const cn = {
|
||||
WIP: "该功能仍在开发中……",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。",
|
||||
"访问密码不正确或为空,请前往[设置](/#/settings)页输入正确的访问密码,或者填入你自己的 OpenAI API Key。",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 条对话`,
|
||||
@@ -67,7 +67,7 @@ const cn = {
|
||||
ConfirmClearAll: "确认清除所有数据?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language",
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "所有语言",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
@@ -78,6 +78,9 @@ const cn = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "头像",
|
||||
@@ -148,7 +151,7 @@ const cn = {
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "访问密码",
|
||||
SubTitle: "已开启加密访问",
|
||||
SubTitle: "管理员已开启加密访问",
|
||||
Placeholder: "请输入访问密码",
|
||||
},
|
||||
Model: "模型 (model)",
|
||||
|
244
app/locales/cs.ts
Normal file
244
app/locales/cs.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const cs: LocaleType = {
|
||||
WIP: "V přípravě...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Neoprávněný přístup, zadejte přístupový kód na stránce nastavení.",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} zpráv`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} zpráv s ChatGPT`,
|
||||
Actions: {
|
||||
ChatList: "Přejít na seznam chatů",
|
||||
CompressedHistory: "Pokyn z komprimované paměti historie",
|
||||
Export: "Exportovat všechny zprávy jako Markdown",
|
||||
Copy: "Kopírovat",
|
||||
Stop: "Zastavit",
|
||||
Retry: "Zopakovat",
|
||||
Delete: "Smazat",
|
||||
},
|
||||
Rename: "Přejmenovat chat",
|
||||
Typing: "Píše...",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} pro odeslání`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter pro řádkování";
|
||||
}
|
||||
return inputHints + ", / pro vyhledávání pokynů";
|
||||
},
|
||||
Send: "Odeslat",
|
||||
Config: {
|
||||
Reset: "Obnovit výchozí",
|
||||
SaveAs: "Uložit jako Masku",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Všechny zprávy",
|
||||
Copy: "Kopírovat vše",
|
||||
Download: "Stáhnout",
|
||||
MessageFromYou: "Zpráva od vás",
|
||||
MessageFromChatGPT: "Zpráva z ChatGPT",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Pokyn z paměti",
|
||||
EmptyContent: "Zatím nic.",
|
||||
Send: "Odeslat paměť",
|
||||
Copy: "Kopírovat paměť",
|
||||
Reset: "Obnovit relaci",
|
||||
ResetConfirm:
|
||||
"Resetováním se vymaže historie aktuálních konverzací i paměť historie pokynů. Opravdu chcete provést obnovu?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Nový chat",
|
||||
DeleteChat: "Potvrzujete smazání vybrané konverzace?",
|
||||
DeleteToast: "Chat smazán",
|
||||
Revert: "Zvrátit",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Nastavení",
|
||||
SubTitle: "Všechna nastavení",
|
||||
Actions: {
|
||||
ClearAll: "Vymazat všechna data",
|
||||
ResetAll: "Obnovit veškeré nastavení",
|
||||
Close: "Zavřít",
|
||||
ConfirmResetAll: "Jste si jisti, že chcete obnovit všechna nastavení?",
|
||||
ConfirmClearAll: "Jste si jisti, že chcete smazat všechna data?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Všechny jazyky",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
it: "Italiano",
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
FontSize: {
|
||||
Title: "Velikost písma",
|
||||
SubTitle: "Nastavení velikosti písma obsahu chatu",
|
||||
},
|
||||
Update: {
|
||||
Version: (x: string) => `Verze: ${x}`,
|
||||
IsLatest: "Aktuální verze",
|
||||
CheckUpdate: "Zkontrolovat aktualizace",
|
||||
IsChecking: "Kontrola aktualizace...",
|
||||
FoundUpdate: (x: string) => `Nalezena nová verze: ${x}`,
|
||||
GoToUpdate: "Aktualizovat",
|
||||
},
|
||||
SendKey: "Odeslat klíč",
|
||||
Theme: "Téma",
|
||||
TightBorder: "Těsné ohraničení",
|
||||
SendPreviewBubble: {
|
||||
Title: "Odesílat chatovací bublinu s náhledem",
|
||||
SubTitle: "Zobrazit v náhledu bubliny",
|
||||
},
|
||||
Mask: {
|
||||
Title: "Úvodní obrazovka Masek",
|
||||
SubTitle: "Před zahájením nového chatu zobrazte úvodní obrazovku Masek",
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Deaktivovat automatické dokončování",
|
||||
SubTitle: "Zadejte / pro spuštění automatického dokončování",
|
||||
},
|
||||
List: "Seznam pokynů",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} vestavěných, ${custom} uživatelských`,
|
||||
Edit: "Upravit",
|
||||
Modal: {
|
||||
Title: "Seznam pokynů",
|
||||
Add: "Přidat pokyn",
|
||||
Search: "Hledat pokyny",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Editovat pokyn",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Počet připojených zpráv",
|
||||
SubTitle: "Počet odeslaných připojených zpráv na žádost",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Práh pro kompresi historie",
|
||||
SubTitle:
|
||||
"Komprese proběhne, pokud délka nekomprimovaných zpráv přesáhne tuto hodnotu",
|
||||
},
|
||||
Token: {
|
||||
Title: "API klíč",
|
||||
SubTitle: "Použitím klíče ignorujete omezení přístupového kódu",
|
||||
Placeholder: "Klíč API OpenAI",
|
||||
},
|
||||
Usage: {
|
||||
Title: "Stav účtu",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Použito tento měsíc $${used}, předplaceno $${total}`;
|
||||
},
|
||||
IsChecking: "Kontroluji...",
|
||||
Check: "Zkontrolovat",
|
||||
NoAccess: "Pro kontrolu zůstatku zadejte klíč API",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Přístupový kód",
|
||||
SubTitle: "Kontrola přístupu povolena",
|
||||
Placeholder: "Potřebujete přístupový kód",
|
||||
},
|
||||
Model: "Model",
|
||||
Temperature: {
|
||||
Title: "Teplota",
|
||||
SubTitle: "Větší hodnota činí výstup náhodnějším",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Max. počet tokenů",
|
||||
SubTitle: "Maximální délka vstupního tokenu a generovaných tokenů",
|
||||
},
|
||||
PresencePenlty: {
|
||||
Title: "Přítomnostní korekce",
|
||||
SubTitle: "Větší hodnota zvyšuje pravděpodobnost nových témat.",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Nová konverzace",
|
||||
BotHello: "Ahoj! Jak mohu dnes pomoci?",
|
||||
Error: "Něco se pokazilo, zkuste to prosím později.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Toto je shrnutí historie chatu mezi umělou inteligencí a uživatelem v podobě rekapitulace: " +
|
||||
content,
|
||||
Topic:
|
||||
"Vytvořte prosím název o čtyřech až pěti slovech vystihující průběh našeho rozhovoru bez jakýchkoli úvodních slov, interpunkčních znamének, uvozovek, teček, symbolů nebo dalšího textu. Odstraňte uvozovky.",
|
||||
Summarize:
|
||||
"Krátce shrň naši diskusi v rozsahu do 200 slov a použij ji jako podnět pro budoucí kontext.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Zkopírováno do schránky",
|
||||
Failed: "Kopírování selhalo, prosím, povolte přístup ke schránce",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Použití ${x} kontextových pokynů`,
|
||||
Edit: "Kontextové a paměťové pokyny",
|
||||
Add: "Přidat pokyn",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Maska",
|
||||
Page: {
|
||||
Title: "Šablona pokynu",
|
||||
SubTitle: (count: number) => `${count} šablon pokynů`,
|
||||
Search: "Hledat v šablonách",
|
||||
Create: "Vytvořit",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} pokynů`,
|
||||
Chat: "Chat",
|
||||
View: "Zobrazit",
|
||||
Edit: "Upravit",
|
||||
Delete: "Smazat",
|
||||
DeleteConfirm: "Potvrdit smazání?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Editovat šablonu pokynu ${readonly ? "(pouze ke čtení)" : ""}`,
|
||||
Download: "Stáhnout",
|
||||
Clone: "Duplikovat",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Avatar Bota",
|
||||
Name: "Jméno Bota",
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Zpět",
|
||||
Skip: "Přeskočit",
|
||||
Title: "Vyberte Masku",
|
||||
SubTitle: "Chatovat s duší za Maskou",
|
||||
More: "Najít více",
|
||||
NotShow: "Nezobrazovat znovu",
|
||||
ConfirmNoShow: "Potvrdit zakázání?Můžete jej povolit později v nastavení.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Potvrdit",
|
||||
Cancel: "Zrušit",
|
||||
Close: "Zavřít",
|
||||
Create: "Vytvořit",
|
||||
Edit: "Upravit",
|
||||
},
|
||||
};
|
||||
|
||||
export default cs;
|
@@ -71,7 +71,7 @@ const de: LocaleType = {
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "All Languages",
|
||||
All: "Alle Sprachen",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
@@ -81,6 +81,9 @@ const de: LocaleType = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
|
@@ -80,6 +80,9 @@ const en: LocaleType = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
|
@@ -69,17 +69,20 @@ const es: LocaleType = {
|
||||
ConfirmClearAll: "Are you sure you want to reset all chat?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language",
|
||||
All: "All Languages",
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Todos los idiomas",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "Inglés",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
it: "Italiano",
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
|
@@ -6,6 +6,9 @@ import IT from "./it";
|
||||
import TR from "./tr";
|
||||
import JP from "./jp";
|
||||
import DE from "./de";
|
||||
import VI from "./vi";
|
||||
import RU from "./ru";
|
||||
import CS from "./cs";
|
||||
|
||||
export type { LocaleType } from "./cn";
|
||||
|
||||
@@ -18,10 +21,14 @@ export const AllLangs = [
|
||||
"tr",
|
||||
"jp",
|
||||
"de",
|
||||
"vi",
|
||||
"ru",
|
||||
"cs",
|
||||
] as const;
|
||||
export type Lang = (typeof AllLangs)[number];
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
const DEFAULT_LANG = "en";
|
||||
|
||||
function getItem(key: string) {
|
||||
try {
|
||||
@@ -41,7 +48,8 @@ function getLanguage() {
|
||||
try {
|
||||
return navigator.language.toLowerCase();
|
||||
} catch {
|
||||
return "cn";
|
||||
console.log("[Lang] failed to detect user lang.");
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +68,7 @@ export function getLang(): Lang {
|
||||
}
|
||||
}
|
||||
|
||||
return "en";
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
|
||||
export function changeLang(lang: Lang) {
|
||||
@@ -77,4 +85,7 @@ export default {
|
||||
tr: TR,
|
||||
jp: JP,
|
||||
de: DE,
|
||||
vi: VI,
|
||||
ru: RU,
|
||||
cs: CS,
|
||||
}[getLang()] as typeof CN;
|
||||
|
@@ -69,8 +69,8 @@ const it: LocaleType = {
|
||||
ConfirmClearAll: "Sei sicuro vuoi cancellare tutte le chat?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Lingue",
|
||||
All: "All Languages",
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Tutte le lingue",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
@@ -80,6 +80,9 @@ const it: LocaleType = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
|
@@ -69,7 +69,7 @@ const jp: LocaleType = {
|
||||
ConfirmClearAll: "すべてのチャットをリセットしてもよろしいですか?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language",
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "所有语言",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
@@ -80,6 +80,9 @@ const jp: LocaleType = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "アバター",
|
||||
|
250
app/locales/ru.ts
Normal file
250
app/locales/ru.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const ru: LocaleType = {
|
||||
WIP: "Скоро...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Несанкционированный доступ. Пожалуйста, введите код доступа на странице настроек.",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} сообщений`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} сообщений с ChatGPT`,
|
||||
Actions: {
|
||||
ChatList: "Перейти к списку чатов",
|
||||
CompressedHistory: "Сжатая история памяти",
|
||||
Export: "Экспортировать все сообщения в формате Markdown",
|
||||
Copy: "Копировать",
|
||||
Stop: "Остановить",
|
||||
Retry: "Повторить",
|
||||
Delete: "Удалить",
|
||||
},
|
||||
Rename: "Переименовать чат",
|
||||
Typing: "Печатает…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} для отправки сообщения`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter для переноса строки";
|
||||
}
|
||||
return inputHints + ", / для поиска подсказок";
|
||||
},
|
||||
Send: "Отправить",
|
||||
Config: {
|
||||
Reset: "Сбросить настройки",
|
||||
SaveAs: "Сохранить как маску",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Все сообщения",
|
||||
Copy: "Копировать все",
|
||||
Download: "Скачать",
|
||||
MessageFromYou: "Сообщение от вас",
|
||||
MessageFromChatGPT: "Сообщение от ChatGPT",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Память",
|
||||
EmptyContent: "Пусто.",
|
||||
Send: "Отправить память",
|
||||
Copy: "Копировать память",
|
||||
Reset: "Сбросить сессию",
|
||||
ResetConfirm:
|
||||
"При сбросе текущая история переписки и историческая память будут удалены. Вы уверены, что хотите сбросить?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Новый чат",
|
||||
DeleteChat: "Вы действительно хотите удалить выбранный разговор?",
|
||||
DeleteToast: "Чат удален",
|
||||
Revert: "Отмена",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Настройки",
|
||||
SubTitle: "Все настройки",
|
||||
Actions: {
|
||||
ClearAll: "Очистить все данные",
|
||||
ResetAll: "Сбросить все настройки",
|
||||
Close: "Закрыть",
|
||||
ConfirmResetAll: "Вы уверены, что хотите сбросить все настройки?",
|
||||
ConfirmClearAll: "Вы уверены, что хотите очистить все данные?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Все языки",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
it: "Italiano",
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Аватар",
|
||||
FontSize: {
|
||||
Title: "Размер шрифта",
|
||||
SubTitle: "Настроить размер шрифта контента чата",
|
||||
},
|
||||
Update: {
|
||||
Version: (x: string) => `Версия: ${x}`,
|
||||
IsLatest: "Последняя версия",
|
||||
CheckUpdate: "Проверить обновление",
|
||||
IsChecking: "Проверка обновления...",
|
||||
FoundUpdate: (x: string) => `Найдена новая версия: ${x}`,
|
||||
GoToUpdate: "Обновить",
|
||||
},
|
||||
SendKey: "Клавиша отправки",
|
||||
Theme: "Тема",
|
||||
TightBorder: "Узкая граница",
|
||||
SendPreviewBubble: {
|
||||
Title: "Отправить предпросмотр",
|
||||
SubTitle: "Предварительный просмотр markdown в пузыре",
|
||||
},
|
||||
Mask: {
|
||||
Title: "Экран заставки маски",
|
||||
SubTitle: "Показывать экран заставки маски перед началом нового чата",
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Отключить автозаполнение",
|
||||
SubTitle: "Ввод / для запуска автозаполнения",
|
||||
},
|
||||
List: "Список подсказок",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} встроенных, ${custom} пользовательских`,
|
||||
Edit: "Редактировать",
|
||||
Modal: {
|
||||
Title: "Список подсказок",
|
||||
Add: "Добавить",
|
||||
Search: "Поиск подсказок",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Редактировать подсказку",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Количество прикрепляемых сообщений",
|
||||
SubTitle:
|
||||
"Количество отправляемых сообщений, прикрепляемых к каждому запросу",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Порог сжатия истории",
|
||||
SubTitle:
|
||||
"Будет сжимать, если длина несжатых сообщений превышает указанное значение",
|
||||
},
|
||||
Token: {
|
||||
Title: "API ключ",
|
||||
SubTitle: "Используйте свой ключ, чтобы игнорировать лимит доступа",
|
||||
Placeholder: "API ключ OpenAI",
|
||||
},
|
||||
Usage: {
|
||||
Title: "Баланс аккаунта",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Использовано в этом месяце $${used}, подписка $${total}`;
|
||||
},
|
||||
IsChecking: "Проверка...",
|
||||
Check: "Проверить",
|
||||
NoAccess: "Введите API ключ, чтобы проверить баланс",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Код доступа",
|
||||
SubTitle: "Контроль доступа включен",
|
||||
Placeholder: "Требуется код доступа",
|
||||
},
|
||||
Model: "Модель",
|
||||
Temperature: {
|
||||
Title: "Температура",
|
||||
SubTitle: "Чем выше значение, тем более случайный вывод",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Максимальное количество токенов",
|
||||
SubTitle: "Максимальная длина вводных и генерируемых токенов",
|
||||
},
|
||||
PresencePenlty: {
|
||||
Title: "Штраф за повторения",
|
||||
SubTitle:
|
||||
"Чем выше значение, тем больше вероятность общения на новые темы",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Новый разговор",
|
||||
BotHello: "Здравствуйте! Как я могу вам помочь сегодня?",
|
||||
Error: "Что-то пошло не так. Пожалуйста, попробуйте еще раз позже.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Это краткое содержание истории чата между ИИ и пользователем: " +
|
||||
content,
|
||||
Topic:
|
||||
"Пожалуйста, создайте заголовок из четырех или пяти слов, который кратко описывает нашу беседу, без введения, знаков пунктуации, кавычек, точек, символов или дополнительного текста. Удалите кавычки.",
|
||||
Summarize:
|
||||
"Кратко изложите нашу дискуссию в 200 словах или менее для использования в будущем контексте.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Скопировано в буфер обмена",
|
||||
Failed:
|
||||
"Не удалось скопировать, пожалуйста, предоставьте разрешение на доступ к буферу обмена",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `С ${x} контекстными подсказками`,
|
||||
Edit: "Контекстные и памятные подсказки",
|
||||
Add: "Добавить подсказку",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Плагин",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Маска",
|
||||
Page: {
|
||||
Title: "Шаблон подсказки",
|
||||
SubTitle: (count: number) => `${count} шаблонов подсказок`,
|
||||
Search: "Поиск шаблонов",
|
||||
Create: "Создать",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} подсказок`,
|
||||
Chat: "Чат",
|
||||
View: "Просмотр",
|
||||
Edit: "Редактировать",
|
||||
Delete: "Удалить",
|
||||
DeleteConfirm: "Подтвердить удаление?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Редактирование шаблона подсказки ${
|
||||
readonly ? "(только для чтения)" : ""
|
||||
}`,
|
||||
Download: "Скачать",
|
||||
Clone: "Клонировать",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Аватар бота",
|
||||
Name: "Имя бота",
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Вернуться",
|
||||
Skip: "Пропустить",
|
||||
Title: "Выберите маску",
|
||||
SubTitle: "Общайтесь с душой за маской",
|
||||
More: "Найти еще",
|
||||
NotShow: "Не показывать снова",
|
||||
ConfirmNoShow:
|
||||
"Подтвердите отключение? Вы можете включить это позже в настройках.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Подтвердить",
|
||||
Cancel: "Отмена",
|
||||
Close: "Закрыть",
|
||||
Create: "Создать",
|
||||
Edit: "Редактировать",
|
||||
},
|
||||
};
|
||||
|
||||
export default ru;
|
@@ -70,7 +70,7 @@ const tr: LocaleType = {
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "All Languages",
|
||||
All: "Tüm Diller",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
@@ -80,6 +80,9 @@ const tr: LocaleType = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Avatar",
|
||||
|
@@ -67,7 +67,7 @@ const tw: LocaleType = {
|
||||
ConfirmClearAll: "您確定要清除所有数据嗎?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language",
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "所有语言",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
@@ -78,6 +78,9 @@ const tw: LocaleType = {
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "大頭貼",
|
||||
|
243
app/locales/vi.ts
Normal file
243
app/locales/vi.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { LocaleType } from "./index";
|
||||
|
||||
const vi: LocaleType = {
|
||||
WIP: "Sắp ra mắt...",
|
||||
Error: {
|
||||
Unauthorized:
|
||||
"Truy cập chưa xác thực, vui lòng nhập mã truy cập trong trang cài đặt.",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} tin nhắn`,
|
||||
},
|
||||
Chat: {
|
||||
SubTitle: (count: number) => `${count} tin nhắn với ChatGPT`,
|
||||
Actions: {
|
||||
ChatList: "Xem danh sách chat",
|
||||
CompressedHistory: "Nén tin nhắn trong quá khứ",
|
||||
Export: "Xuất tất cả tin nhắn dưới dạng Markdown",
|
||||
Copy: "Sao chép",
|
||||
Stop: "Dừng",
|
||||
Retry: "Thử lại",
|
||||
Delete: "Xóa",
|
||||
},
|
||||
Rename: "Đổi tên",
|
||||
Typing: "Đang nhập…",
|
||||
Input: (submitKey: string) => {
|
||||
var inputHints = `${submitKey} để gửi`;
|
||||
if (submitKey === String(SubmitKey.Enter)) {
|
||||
inputHints += ", Shift + Enter để xuống dòng";
|
||||
}
|
||||
return inputHints + ", / để tìm kiếm mẫu gợi ý";
|
||||
},
|
||||
Send: "Gửi",
|
||||
Config: {
|
||||
Reset: "Khôi phục cài đặt gốc",
|
||||
SaveAs: "Lưu dưới dạng Mẫu",
|
||||
},
|
||||
},
|
||||
Export: {
|
||||
Title: "Tất cả tin nhắn",
|
||||
Copy: "Sao chép tất cả",
|
||||
Download: "Tải xuống",
|
||||
MessageFromYou: "Tin nhắn của bạn",
|
||||
MessageFromChatGPT: "Tin nhắn từ ChatGPT",
|
||||
},
|
||||
Memory: {
|
||||
Title: "Lịch sử tin nhắn",
|
||||
EmptyContent: "Chưa có tin nhắn",
|
||||
Send: "Gửi tin nhắn trong quá khứ",
|
||||
Copy: "Sao chép tin nhắn trong quá khứ",
|
||||
Reset: "Đặt lại phiên",
|
||||
ResetConfirm:
|
||||
"Đặt lại sẽ xóa toàn bộ lịch sử trò chuyện hiện tại và bộ nhớ. Bạn có chắc chắn muốn đặt lại không?",
|
||||
},
|
||||
Home: {
|
||||
NewChat: "Cuộc trò chuyện mới",
|
||||
DeleteChat: "Xác nhận xóa các cuộc trò chuyện đã chọn?",
|
||||
DeleteToast: "Đã xóa cuộc trò chuyện",
|
||||
Revert: "Khôi phục",
|
||||
},
|
||||
Settings: {
|
||||
Title: "Cài đặt",
|
||||
SubTitle: "Tất cả cài đặt",
|
||||
Actions: {
|
||||
ClearAll: "Xóa toàn bộ dữ liệu",
|
||||
ResetAll: "Khôi phục cài đặt gốc",
|
||||
Close: "Đóng",
|
||||
ConfirmResetAll: "Bạn chắc chắn muốn thiết lập lại tất cả cài đặt?",
|
||||
ConfirmClearAll: "Bạn chắc chắn muốn thiết lập lại tất cả dữ liệu?",
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
All: "Tất cả ngôn ngữ",
|
||||
Options: {
|
||||
cn: "简体中文",
|
||||
en: "English",
|
||||
tw: "繁體中文",
|
||||
es: "Español",
|
||||
it: "Italiano",
|
||||
tr: "Türkçe",
|
||||
jp: "日本語",
|
||||
de: "Deutsch",
|
||||
vi: "Tiếng Việt",
|
||||
ru: "Русский",
|
||||
cs: "Čeština",
|
||||
},
|
||||
},
|
||||
Avatar: "Ảnh đại diện",
|
||||
FontSize: {
|
||||
Title: "Font chữ",
|
||||
SubTitle: "Thay đổi font chữ của nội dung trò chuyện",
|
||||
},
|
||||
Update: {
|
||||
Version: (x: string) => `Phiên bản: ${x}`,
|
||||
IsLatest: "Phiên bản mới nhất",
|
||||
CheckUpdate: "Kiểm tra bản cập nhật",
|
||||
IsChecking: "Kiểm tra bản cập nhật...",
|
||||
FoundUpdate: (x: string) => `Phát hiện phiên bản mới: ${x}`,
|
||||
GoToUpdate: "Cập nhật",
|
||||
},
|
||||
SendKey: "Phím gửi",
|
||||
Theme: "Theme",
|
||||
TightBorder: "Chế độ không viền",
|
||||
SendPreviewBubble: {
|
||||
Title: "Gửi bong bóng xem trước",
|
||||
SubTitle: "Xem trước nội dung markdown bằng bong bóng",
|
||||
},
|
||||
Mask: {
|
||||
Title: "Mask Splash Screen",
|
||||
SubTitle: "Chớp màn hình khi bắt đầu cuộc trò chuyện mới",
|
||||
},
|
||||
Prompt: {
|
||||
Disable: {
|
||||
Title: "Vô hiệu hóa chức năng tự động hoàn thành",
|
||||
SubTitle: "Nhập / để kích hoạt chức năng tự động hoàn thành",
|
||||
},
|
||||
List: "Danh sách mẫu gợi ý",
|
||||
ListCount: (builtin: number, custom: number) =>
|
||||
`${builtin} có sẵn, ${custom} do người dùng xác định`,
|
||||
Edit: "Chỉnh sửa",
|
||||
Modal: {
|
||||
Title: "Danh sách mẫu gợi ý",
|
||||
Add: "Thêm",
|
||||
Search: "Tìm kiếm mẫu",
|
||||
},
|
||||
EditModal: {
|
||||
Title: "Chỉnh sửa mẫu",
|
||||
},
|
||||
},
|
||||
HistoryCount: {
|
||||
Title: "Số lượng tin nhắn đính kèm",
|
||||
SubTitle: "Số lượng tin nhắn trong quá khứ được gửi kèm theo mỗi yêu cầu",
|
||||
},
|
||||
CompressThreshold: {
|
||||
Title: "Ngưỡng nén lịch sử tin nhắn",
|
||||
SubTitle: "Thực hiện nén nếu số lượng tin nhắn chưa nén vượt quá ngưỡng",
|
||||
},
|
||||
Token: {
|
||||
Title: "API Key",
|
||||
SubTitle: "Sử dụng khóa của bạn để bỏ qua giới hạn mã truy cập",
|
||||
Placeholder: "OpenAI API Key",
|
||||
},
|
||||
Usage: {
|
||||
Title: "Hạn mức tài khoản",
|
||||
SubTitle(used: any, total: any) {
|
||||
return `Đã sử dụng $${used} trong tháng này, hạn mức $${total}`;
|
||||
},
|
||||
IsChecking: "Đang kiểm tra...",
|
||||
Check: "Kiểm tra",
|
||||
NoAccess: "Nhập API Key để kiểm tra hạn mức",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Mã truy cập",
|
||||
SubTitle: "Đã bật kiểm soát truy cập",
|
||||
Placeholder: "Nhập mã truy cập",
|
||||
},
|
||||
Model: "Mô hình",
|
||||
Temperature: {
|
||||
Title: "Tính ngẫu nhiên (temperature)",
|
||||
SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên",
|
||||
},
|
||||
MaxTokens: {
|
||||
Title: "Giới hạn số lượng token (max_tokens)",
|
||||
SubTitle: "Số lượng token tối đa được sử dụng trong mỗi lần tương tác",
|
||||
},
|
||||
PresencePenlty: {
|
||||
Title: "Chủ đề mới (presence_penalty)",
|
||||
SubTitle: "Giá trị càng lớn tăng khả năng mở rộng sang các chủ đề mới",
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "Cuộc trò chuyện mới",
|
||||
BotHello: "Xin chào! Mình có thể giúp gì cho bạn?",
|
||||
Error: "Có lỗi xảy ra, vui lòng thử lại sau.",
|
||||
Prompt: {
|
||||
History: (content: string) =>
|
||||
"Tóm tắt ngắn gọn cuộc trò chuyện giữa người dùng và AI: " + content,
|
||||
Topic:
|
||||
"Sử dụng 4 đến 5 từ tóm tắt cuộc trò chuyện này mà không có phần mở đầu, dấu chấm câu, dấu ngoặc kép, dấu chấm, ký hiệu hoặc văn bản bổ sung nào. Loại bỏ các dấu ngoặc kép kèm theo.",
|
||||
Summarize:
|
||||
"Tóm tắt cuộc trò chuyện này một cách ngắn gọn trong 200 từ hoặc ít hơn để sử dụng làm gợi ý cho ngữ cảnh tiếp theo.",
|
||||
},
|
||||
},
|
||||
Copy: {
|
||||
Success: "Sao chép vào bộ nhớ tạm",
|
||||
Failed:
|
||||
"Sao chép không thành công, vui lòng cấp quyền truy cập vào bộ nhớ tạm",
|
||||
},
|
||||
Context: {
|
||||
Toast: (x: any) => `Sử dụng ${x} tin nhắn chứa ngữ cảnh`,
|
||||
Edit: "Thiết lập ngữ cảnh và bộ nhớ",
|
||||
Add: "Thêm tin nhắn",
|
||||
},
|
||||
Plugin: {
|
||||
Name: "Plugin",
|
||||
},
|
||||
Mask: {
|
||||
Name: "Mẫu",
|
||||
Page: {
|
||||
Title: "Mẫu trò chuyện",
|
||||
SubTitle: (count: number) => `${count} mẫu`,
|
||||
Search: "Tìm kiếm mẫu",
|
||||
Create: "Tạo",
|
||||
},
|
||||
Item: {
|
||||
Info: (count: number) => `${count} tin nhắn`,
|
||||
Chat: "Chat",
|
||||
View: "Xem trước",
|
||||
Edit: "Chỉnh sửa",
|
||||
Delete: "Xóa",
|
||||
DeleteConfirm: "Xác nhận xóa?",
|
||||
},
|
||||
EditModal: {
|
||||
Title: (readonly: boolean) =>
|
||||
`Chỉnh sửa mẫu ${readonly ? "(chỉ xem)" : ""}`,
|
||||
Download: "Tải xuống",
|
||||
Clone: "Tạo bản sao",
|
||||
},
|
||||
Config: {
|
||||
Avatar: "Ảnh đại diện bot",
|
||||
Name: "Tên bot",
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
Return: "Quay lại",
|
||||
Skip: "Bỏ qua",
|
||||
Title: "Chọn 1 biểu tượng",
|
||||
SubTitle: "Bắt đầu trò chuyện ẩn sau lớp mặt nạ",
|
||||
More: "Tìm thêm",
|
||||
NotShow: "Không hiển thị lại",
|
||||
ConfirmNoShow: "Xác nhận tắt? Bạn có thể bật lại trong phần cài đặt.",
|
||||
},
|
||||
|
||||
UI: {
|
||||
Confirm: "Xác nhận",
|
||||
Cancel: "Hủy",
|
||||
Close: "Đóng",
|
||||
Create: "Tạo",
|
||||
Edit: "Chỉnh sửa",
|
||||
},
|
||||
};
|
||||
|
||||
export default vi;
|
@@ -1,6 +1,38 @@
|
||||
import { BuiltinMask } from "./typing";
|
||||
|
||||
export const CN_MASKS: BuiltinMask[] = [
|
||||
export const CN_MASKS: BuiltinMask[] = [{
|
||||
avatar:"gpt-bot",
|
||||
name:"文生图",
|
||||
context: [
|
||||
{role:"system",
|
||||
content:"You are a useful Assistant that won't refuse to draw picture, instead, you will output specific markdown code for picture description, but don't use code block.",
|
||||
date:"",
|
||||
},
|
||||
{role:"user",
|
||||
content:"Can you draw some pictures for me?",
|
||||
date:"",
|
||||
},
|
||||
{role:"assistant",
|
||||
content:"Sure, what do you want me to draw?",
|
||||
date:"",
|
||||
},
|
||||
{role:"system",
|
||||
content:"助手善于判断用户意图,当确定需要提供图片时,助手会变得沉默寡言,只使用以下格式输出markdown图片:,因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足,助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示,以大幅提高生成图片质量和丰富程度,比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记,因为那样只会渲染出代码块或原始块而不是图片。",
|
||||
date:"",
|
||||
},
|
||||
],
|
||||
modelConfig:{
|
||||
model:"gpt-3.5-turbo",
|
||||
temperature:1,
|
||||
max_tokens:2000,
|
||||
presence_penalty:0,
|
||||
sendMemory:true,
|
||||
historyMessageCount:32,
|
||||
compressMessageLengthThreshold:1000,
|
||||
},
|
||||
lang:"cn",
|
||||
builtin:true,
|
||||
},
|
||||
{
|
||||
avatar: "1f638",
|
||||
name: "文案写手",
|
||||
|
@@ -31,7 +31,7 @@ export const EN_MASKS: BuiltinMask[] = [
|
||||
],
|
||||
modelConfig: {
|
||||
model: "gpt-4",
|
||||
temperature: 1,
|
||||
temperature: 0.5,
|
||||
max_tokens: 2000,
|
||||
presence_penalty: 0,
|
||||
sendMemory: true,
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
useChatStore,
|
||||
} from "./store";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import { ACCESS_CODE_PREFIX } from "./constant";
|
||||
|
||||
const TIME_OUT_MS = 60000;
|
||||
|
||||
@@ -42,31 +43,35 @@ const makeRequestParam = (
|
||||
};
|
||||
};
|
||||
|
||||
function getHeaders() {
|
||||
export function getHeaders() {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let headers: Record<string, string> = {};
|
||||
|
||||
if (accessStore.enabledAccessControl()) {
|
||||
headers["access-code"] = accessStore.accessCode;
|
||||
}
|
||||
const makeBearer = (token: string) => `Bearer ${token.trim()}`;
|
||||
const validString = (x: string) => x && x.length > 0;
|
||||
|
||||
if (accessStore.token && accessStore.token.length > 0) {
|
||||
headers["token"] = accessStore.token;
|
||||
// use user's api key first
|
||||
if (validString(accessStore.token)) {
|
||||
headers.Authorization = makeBearer(accessStore.token);
|
||||
} else if (
|
||||
accessStore.enabledAccessControl() &&
|
||||
validString(accessStore.accessCode)
|
||||
) {
|
||||
headers.Authorization = makeBearer(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function requestOpenaiClient(path: string) {
|
||||
const openaiUrl = useAccessStore.getState().openaiUrl;
|
||||
return (body: any, method = "POST") =>
|
||||
fetch("/api/openai", {
|
||||
fetch(openaiUrl + path, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
path,
|
||||
...getHeaders(),
|
||||
},
|
||||
body: body && JSON.stringify(body),
|
||||
headers: getHeaders(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,16 +166,17 @@ export async function requestChatStream(
|
||||
const reqTimeoutId = setTimeout(() => controller.abort(), TIME_OUT_MS);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/chat-stream", {
|
||||
const openaiUrl = useAccessStore.getState().openaiUrl;
|
||||
const res = await fetch(openaiUrl + "v1/chat/completions", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
path: "v1/chat/completions",
|
||||
...getHeaders(),
|
||||
},
|
||||
body: JSON.stringify(req),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
clearTimeout(reqTimeoutId);
|
||||
|
||||
let responseText = "";
|
||||
|
@@ -1,12 +1,17 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { StoreKey } from "../constant";
|
||||
import { getHeaders } from "../requests";
|
||||
import { BOT_HELLO } from "./chat";
|
||||
import { ALL_MODELS } from "./config";
|
||||
|
||||
export interface AccessControlStore {
|
||||
accessCode: string;
|
||||
token: string;
|
||||
|
||||
needCode: boolean;
|
||||
hideUserApiKey: boolean;
|
||||
openaiUrl: string;
|
||||
|
||||
updateToken: (_: string) => void;
|
||||
updateCode: (_: string) => void;
|
||||
@@ -23,18 +28,23 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
token: "",
|
||||
accessCode: "",
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
openaiUrl: "/api/openai/",
|
||||
|
||||
enabledAccessControl() {
|
||||
get().fetch();
|
||||
|
||||
return get().needCode;
|
||||
},
|
||||
updateCode(code: string) {
|
||||
set((state) => ({ accessCode: code }));
|
||||
set(() => ({ accessCode: code }));
|
||||
},
|
||||
updateToken(token: string) {
|
||||
set((state) => ({ token }));
|
||||
set(() => ({ token }));
|
||||
},
|
||||
isAuthorized() {
|
||||
get().fetch();
|
||||
|
||||
// has token or has code or disabled access control
|
||||
return (
|
||||
!!get().token || !!get().accessCode || !get().enabledAccessControl()
|
||||
@@ -46,11 +56,26 @@ export const useAccessStore = create<AccessControlStore>()(
|
||||
fetch("/api/config", {
|
||||
method: "post",
|
||||
body: null,
|
||||
headers: {
|
||||
...getHeaders(),
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ ...res }));
|
||||
|
||||
if (!res.enableGPT4) {
|
||||
ALL_MODELS.forEach((model) => {
|
||||
if (model.name.startsWith("gpt-4")) {
|
||||
(model as any).available = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ((res as any).botHello) {
|
||||
BOT_HELLO.content = (res as any).botHello;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("[Config] failed to fetch config");
|
||||
|
@@ -7,11 +7,11 @@ import {
|
||||
requestChatStream,
|
||||
requestWithPrompt,
|
||||
} from "../requests";
|
||||
import { isMobileScreen, trimTopic } from "../utils";
|
||||
import { trimTopic } from "../utils";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import { DEFAULT_CONFIG, ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { ModelType } from "./config";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import { StoreKey } from "../constant";
|
||||
|
||||
@@ -180,8 +180,9 @@ export const useChatStore = create<ChatStore>()(
|
||||
const sessions = get().sessions.slice();
|
||||
sessions.splice(index, 1);
|
||||
|
||||
const currentIndex = get().currentSessionIndex;
|
||||
let nextIndex = Math.min(
|
||||
get().currentSessionIndex,
|
||||
currentIndex - Number(index < currentIndex),
|
||||
sessions.length - 1,
|
||||
);
|
||||
|
||||
@@ -251,9 +252,20 @@ export const useChatStore = create<ChatStore>()(
|
||||
model: modelConfig.model,
|
||||
});
|
||||
|
||||
const systemInfo = createMessage({
|
||||
role: "system",
|
||||
content: `IMPRTANT: You are a virtual assistant powered by the ${
|
||||
modelConfig.model
|
||||
} model, now time is ${new Date().toLocaleString()}}`,
|
||||
id: botMessage.id! + 1,
|
||||
});
|
||||
|
||||
// get recent messages
|
||||
const systemMessages = [systemInfo];
|
||||
const recentMessages = get().getMessagesWithMemory();
|
||||
const sendMessages = recentMessages.concat(userMessage);
|
||||
const sendMessages = systemMessages.concat(
|
||||
recentMessages.concat(userMessage),
|
||||
);
|
||||
const sessionIndex = get().currentSessionIndex;
|
||||
const messageIndex = get().currentSession().messages.length + 1;
|
||||
|
||||
@@ -390,14 +402,17 @@ export const useChatStore = create<ChatStore>()(
|
||||
|
||||
summarizeSession() {
|
||||
const session = get().currentSession();
|
||||
|
||||
// remove error messages if any
|
||||
const cleanMessages = session.messages.filter((msg) => !msg.isError);
|
||||
|
||||
// should summarize topic after chating more than 50 words
|
||||
const SUMMARIZE_MIN_LEN = 50;
|
||||
if (
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(session.messages) >= SUMMARIZE_MIN_LEN
|
||||
countMessages(cleanMessages) >= SUMMARIZE_MIN_LEN
|
||||
) {
|
||||
requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, {
|
||||
requestWithPrompt(cleanMessages, Locale.Store.Prompt.Topic, {
|
||||
model: "gpt-3.5-turbo",
|
||||
}).then((res) => {
|
||||
get().updateCurrentSession(
|
||||
@@ -408,10 +423,10 @@ export const useChatStore = create<ChatStore>()(
|
||||
}
|
||||
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
let toBeSummarizedMsgs = session.messages.slice(
|
||||
let toBeSummarizedMsgs = cleanMessages.slice(
|
||||
session.lastSummarizeIndex,
|
||||
);
|
||||
|
||||
|
||||
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||
|
||||
if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
|
||||
|
@@ -31,7 +31,7 @@ export const DEFAULT_CONFIG = {
|
||||
|
||||
modelConfig: {
|
||||
model: "gpt-3.5-turbo" as ModelType,
|
||||
temperature: 1,
|
||||
temperature: 0.5,
|
||||
max_tokens: 2000,
|
||||
presence_penalty: 0,
|
||||
sendMemory: true,
|
||||
@@ -76,6 +76,26 @@ export const ALL_MODELS = [
|
||||
name: "gpt-3.5-turbo-0301",
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
name: "qwen-v1", // 通义千问
|
||||
available: false,
|
||||
},
|
||||
{
|
||||
name: "ernie", // 文心一言
|
||||
available: false,
|
||||
},
|
||||
{
|
||||
name: "spark", // 讯飞星火
|
||||
available: false,
|
||||
},
|
||||
{
|
||||
name: "llama", // llama
|
||||
available: false,
|
||||
},
|
||||
{
|
||||
name: "chatglm", // chatglm-6b
|
||||
available: false,
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ModelType = (typeof ALL_MODELS)[number]["name"];
|
||||
|
@@ -57,6 +57,7 @@ export const useMaskStore = create<MaskStore>()(
|
||||
...createEmptyMask(),
|
||||
...mask,
|
||||
id,
|
||||
builtin: false,
|
||||
};
|
||||
|
||||
set(() => ({ masks }));
|
||||
|
@@ -53,10 +53,9 @@ export const useUpdateStore = create<UpdateStore>()(
|
||||
}));
|
||||
|
||||
try {
|
||||
// const data = await (await fetch(FETCH_TAG_URL)).json();
|
||||
// const remoteId = data[0].name as string;
|
||||
const data = await (await fetch(FETCH_COMMIT_URL)).json();
|
||||
const remoteId = (data[0].sha as string).substring(0, 7);
|
||||
const remoteCommitTime = data[0].commit.committer.date;
|
||||
const remoteId = new Date(remoteCommitTime).getTime().toString();
|
||||
set(() => ({
|
||||
remoteVersion: remoteId,
|
||||
}));
|
||||
|
@@ -88,6 +88,9 @@
|
||||
}
|
||||
html {
|
||||
height: var(--full-height);
|
||||
|
||||
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
|
||||
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -102,8 +105,6 @@ body {
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
touch-action: pan-x pan-y;
|
||||
font-family: "Noto Sans SC", "SF Pro SC", "SF Pro Text", "SF Pro Icons",
|
||||
"PingFang SC", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
background-color: var(--second);
|
||||
@@ -247,6 +248,10 @@ div.math {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
|
28
app/utils.ts
28
app/utils.ts
@@ -42,6 +42,26 @@ export function downloadAs(text: string, filename: string) {
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
export function readFromFile() {
|
||||
return new Promise<string>((res, rej) => {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = "application/json";
|
||||
|
||||
fileInput.onchange = (event: any) => {
|
||||
const file = event.target.files[0];
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e: any) => {
|
||||
res(e.target.result);
|
||||
};
|
||||
fileReader.onerror = (e) => rej(e);
|
||||
fileReader.readAsText(file);
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
export function isIOS() {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return /iphone|ipad|ipod/.test(userAgent);
|
||||
@@ -138,15 +158,15 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
|
||||
|
||||
const width = getDomContentWidth(dom);
|
||||
measureDom.style.width = width + "px";
|
||||
measureDom.innerText = dom.value.trim().length > 0 ? dom.value : "1";
|
||||
|
||||
const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1);
|
||||
measureDom.innerText = dom.value !== "" ? dom.value : "1";
|
||||
const endWithEmptyLine = dom.value.endsWith("\n");
|
||||
const height = parseFloat(window.getComputedStyle(measureDom).height);
|
||||
const singleLineHeight = parseFloat(
|
||||
window.getComputedStyle(singleLineDom).height,
|
||||
);
|
||||
|
||||
const rows = Math.round(height / singleLineHeight) + lineWrapCount;
|
||||
const rows =
|
||||
Math.round(height / singleLineHeight) + (endWithEmptyLine ? 1 : 0);
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
39
docs/cloudflare-pages-cn.md
Normal file
39
docs/cloudflare-pages-cn.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Cloudflare Pages 部署指南
|
||||
|
||||
## 如何新建项目
|
||||
在 Github 上 fork 本项目,然后登录到 dash.cloudflare.com 并进入 Pages。
|
||||
|
||||
1. 点击 "Create a project"。
|
||||
2. 选择 "Connect to Git"。
|
||||
3. 关联 Cloudflare Pages 和你的 GitHub 账号。
|
||||
4. 选中你 fork 的此项目。
|
||||
5. 点击 "Begin setup"。
|
||||
6. 对于 "Project name" 和 "Production branch",可以使用默认值,也可以根据需要进行更改。
|
||||
7. 在 "Build Settings" 中,选择 "Framework presets" 选项并选择 "Next.js"。
|
||||
8. 由于 node:buffer 的 bug,暂时不要使用默认的 "Build command"。请使用以下命令:
|
||||
```
|
||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
||||
```
|
||||
9. 对于 "Build output directory",使用默认值并且不要修改。
|
||||
10. 不要修改 "Root Directory"。
|
||||
11. 对于 "Environment variables",点击 ">" 然后点击 "Add variable"。按照以下信息填写:
|
||||
|
||||
- `NODE_VERSION=20.1`
|
||||
- `NEXT_TELEMETRY_DISABLE=1`
|
||||
- `OPENAI_API_KEY=你自己的API Key`
|
||||
- `YARN_VERSION=1.22.19`
|
||||
- `PHP_VERSION=7.4`
|
||||
|
||||
根据实际需要,可以选择填写以下选项:
|
||||
|
||||
- `CODE= 可选填,访问密码,可以使用逗号隔开多个密码`
|
||||
- `OPENAI_ORG_ID= 可选填,指定 OpenAI 中的组织 ID`
|
||||
- `HIDE_USER_API_KEY=1 可选,不让用户自行填入 API Key`
|
||||
- `DISABLE_GPT4=1 可选,不让用户使用 GPT-4`
|
||||
|
||||
12. 点击 "Save and Deploy"。
|
||||
13. 点击 "Cancel deployment",因为需要填写 Compatibility flags。
|
||||
14. 前往 "Build settings"、"Functions",找到 "Compatibility flags"。
|
||||
15. 在 "Configure Production compatibility flag" 和 "Configure Preview compatibility flag" 中填写 "nodejs_compat"。
|
||||
16. 前往 "Deployments",点击 "Retry deployment"。
|
||||
17. Enjoy.
|
38
docs/cloudflare-pages-en.md
Normal file
38
docs/cloudflare-pages-en.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Cloudflare Pages Deployment Guide
|
||||
|
||||
## How to create a new project
|
||||
Fork this project on GitHub, then log in to dash.cloudflare.com and go to Pages.
|
||||
|
||||
1. Click "Create a project".
|
||||
2. Choose "Connect to Git".
|
||||
3. Connect Cloudflare Pages to your GitHub account.
|
||||
4. Select the forked project.
|
||||
5. Click "Begin setup".
|
||||
6. For "Project name" and "Production branch", use the default values or change them as needed.
|
||||
7. In "Build Settings", choose the "Framework presets" option and select "Next.js".
|
||||
8. Do not use the default "Build command" due to a node:buffer bug. Instead, use the following command:
|
||||
```
|
||||
npx https://prerelease-registry.devprod.cloudflare.dev/next-on-pages/runs/4930842298/npm-package-next-on-pages-230 --experimental-minify
|
||||
```
|
||||
9. For "Build output directory", use the default value and do not modify it.
|
||||
10. Do not modify "Root Directory".
|
||||
11. For "Environment variables", click ">" and then "Add variable". Fill in the following information:
|
||||
- `NODE_VERSION=20.1`
|
||||
- `NEXT_TELEMETRY_DISABLE=1`
|
||||
- `OPENAI_API_KEY=your_own_API_key`
|
||||
- `YARN_VERSION=1.22.19`
|
||||
- `PHP_VERSION=7.4`
|
||||
|
||||
Optionally fill in the following based on your needs:
|
||||
|
||||
- `CODE= Optional, access passwords, multiple passwords can be separated by commas`
|
||||
- `OPENAI_ORG_ID= Optional, specify the organization ID in OpenAI`
|
||||
- `HIDE_USER_API_KEY=1 Optional, do not allow users to enter their own API key`
|
||||
- `DISABLE_GPT4=1 Optional, do not allow users to use GPT-4`
|
||||
|
||||
12. Click "Save and Deploy".
|
||||
13. Click "Cancel deployment" because you need to fill in Compatibility flags.
|
||||
14. Go to "Build settings", "Functions", and find "Compatibility flags".
|
||||
15. Fill in "nodejs_compat" for both "Configure Production compatibility flag" and "Configure Preview compatibility flag".
|
||||
16. Go to "Deployments" and click "Retry deployment".
|
||||
17. Enjoy.
|
@@ -1,72 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "./app/config/server";
|
||||
import md5 from "spark-md5";
|
||||
|
||||
export const config = {
|
||||
matcher: ["/api/openai", "/api/chat-stream"],
|
||||
};
|
||||
|
||||
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) {
|
||||
const accessCode = req.headers.get("access-code");
|
||||
const token = req.headers.get("token");
|
||||
const hashedCode = md5.hash(accessCode ?? "").trim();
|
||||
|
||||
console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
|
||||
console.log("[Auth] got access code:", accessCode);
|
||||
console.log("[Auth] hashed access code:", hashedCode);
|
||||
console.log("[User IP] ", getIP(req));
|
||||
console.log("[Time] ", new Date().toLocaleString());
|
||||
|
||||
if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
needAccessCode: true,
|
||||
msg: "Please go settings page and fill your access code.",
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// inject api key
|
||||
if (!token) {
|
||||
const apiKey = serverConfig.apiKey;
|
||||
if (apiKey) {
|
||||
console.log("[Auth] set system token");
|
||||
req.headers.set("token", apiKey);
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "Empty Api Key",
|
||||
},
|
||||
{
|
||||
status: 401,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log("[Auth] set user token");
|
||||
}
|
||||
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: req.headers,
|
||||
},
|
||||
});
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ["@svgr/webpack"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
43
next.config.mjs
Normal file
43
next.config.mjs
Normal file
@@ -0,0 +1,43 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
appDir: true,
|
||||
},
|
||||
async rewrites() {
|
||||
const ret = [
|
||||
{
|
||||
source: "/api/proxy/:path*",
|
||||
destination: "https://api.openai.com/:path*",
|
||||
},
|
||||
{
|
||||
source: "/google-fonts/:path*",
|
||||
destination: "https://fonts.googleapis.com/:path*",
|
||||
},
|
||||
];
|
||||
|
||||
const apiUrl = process.env.API_URL;
|
||||
if (apiUrl) {
|
||||
console.log("[Next] using api url ", apiUrl);
|
||||
ret.push({
|
||||
source: "/api/:path*",
|
||||
destination: `${apiUrl}/:path*`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
beforeFiles: ret,
|
||||
};
|
||||
},
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ["@svgr/webpack"],
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
@@ -19,6 +19,7 @@
|
||||
"emoji-picker-react": "^4.4.7",
|
||||
"eventsource-parser": "^0.1.0",
|
||||
"fuse.js": "^6.6.2",
|
||||
"mermaid": "^10.1.0",
|
||||
"next": "^13.3.1-canary.8",
|
||||
"node-fetch": "^3.3.1",
|
||||
"openai": "^3.2.1",
|
||||
|
@@ -40,7 +40,7 @@ async function fetchEN() {
|
||||
return raw
|
||||
.split("\n")
|
||||
.slice(1)
|
||||
.map((v) => v.split('","').map((v) => v.replace('"', "")));
|
||||
.map((v) => v.split('","').map((v) => v.replace(/^"|"$/g, '').replaceAll('""','"')));
|
||||
} catch (error) {
|
||||
console.error("[Fetch] failed to fetch en prompts", error);
|
||||
return [];
|
||||
|
446
yarn.lock
446
yarn.lock
@@ -995,6 +995,11 @@
|
||||
"@babel/helper-validator-identifier" "^7.19.1"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@braintree/sanitize-url@^6.0.0":
|
||||
version "6.0.2"
|
||||
resolved "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f"
|
||||
integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||
@@ -1099,6 +1104,13 @@
|
||||
"@jridgewell/resolve-uri" "3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||
|
||||
"@khanacademy/simple-markdown@^0.8.6":
|
||||
version "0.8.6"
|
||||
resolved "https://registry.npmmirror.com/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz#9c9aef1f5ce2ce60292d13849165965a57c26f25"
|
||||
integrity sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==
|
||||
dependencies:
|
||||
"@types/react" ">=16.0.0"
|
||||
|
||||
"@next/env@13.3.1-canary.8":
|
||||
version "13.3.1-canary.8"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.3.1-canary.8.tgz#9f5cf57999e4f4b59ef6407924803a247cc4e451"
|
||||
@@ -1399,6 +1411,15 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@>=16.0.0":
|
||||
version "18.2.5"
|
||||
resolved "https://registry.npmmirror.com/@types/react/-/react-18.2.5.tgz#f9403e1113b12b53f7edcdd9a900c10dd4b49a59"
|
||||
integrity sha512-RuoMedzJ5AOh23Dvws13LU9jpZHIc/k90AgmK7CecAYeWmSr3553L4u5rk4sWAPBuQosfT7HmTfG4Rg5o4nGEA==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/scheduler@*":
|
||||
version "0.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
|
||||
@@ -1871,16 +1892,16 @@ comma-separated-tokens@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
|
||||
|
||||
commander@7, commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commander@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.0.tgz#71797971162cd3cf65f0b9d24eb28f8d303acdf1"
|
||||
integrity sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==
|
||||
|
||||
commander@^7.2.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
|
||||
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
|
||||
|
||||
commander@^8.0.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
|
||||
@@ -1903,6 +1924,20 @@ core-js-compat@^3.25.1:
|
||||
dependencies:
|
||||
browserslist "^4.21.5"
|
||||
|
||||
cose-base@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz#650334b41b869578a543358b80cda7e0abe0a60a"
|
||||
integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==
|
||||
dependencies:
|
||||
layout-base "^1.0.0"
|
||||
|
||||
cose-base@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmmirror.com/cose-base/-/cose-base-2.2.0.tgz#1c395c35b6e10bb83f9769ca8b817d614add5c01"
|
||||
integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==
|
||||
dependencies:
|
||||
layout-base "^2.0.0"
|
||||
|
||||
cosmiconfig@^7.0.1:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
|
||||
@@ -1973,6 +2008,280 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
|
||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||
|
||||
cytoscape-cose-bilkent@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmmirror.com/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz#762fa121df9930ffeb51a495d87917c570ac209b"
|
||||
integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==
|
||||
dependencies:
|
||||
cose-base "^1.0.0"
|
||||
|
||||
cytoscape-fcose@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmmirror.com/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz#e4d6f6490df4fab58ae9cea9e5c3ab8d7472f471"
|
||||
integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==
|
||||
dependencies:
|
||||
cose-base "^2.2.0"
|
||||
|
||||
cytoscape@^3.23.0:
|
||||
version "3.24.0"
|
||||
resolved "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.24.0.tgz#764e4ca3df37160b1c55244c648afd303a07e109"
|
||||
integrity sha512-W9fJMrAfr/zKFzDCpRR/wn6uoEQ7gfbJmxPK5DadXj69XyAhZYi1QXLOE+UXJfXVXxqGM1o1eeiIrtxrtB43zA==
|
||||
dependencies:
|
||||
heap "^0.2.6"
|
||||
lodash "^4.17.21"
|
||||
|
||||
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.3.tgz#39f1f4954e4a09ff69ac597c2d61906b04e84740"
|
||||
integrity sha512-JRHwbQQ84XuAESWhvIPaUV4/1UYTBOLiOPGWqgFDHZS1D5QN9c57FbH3QpEnQMYiOXNzKUQyGTZf+EVO7RT5TQ==
|
||||
dependencies:
|
||||
internmap "1 - 2"
|
||||
|
||||
d3-axis@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322"
|
||||
integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
|
||||
|
||||
d3-brush@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c"
|
||||
integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-drag "2 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-selection "3"
|
||||
d3-transition "3"
|
||||
|
||||
d3-chord@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966"
|
||||
integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
|
||||
dependencies:
|
||||
d3-path "1 - 3"
|
||||
|
||||
"d3-color@1 - 3", d3-color@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
|
||||
integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
|
||||
|
||||
d3-contour@4:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc"
|
||||
integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
|
||||
dependencies:
|
||||
d3-array "^3.2.0"
|
||||
|
||||
d3-delaunay@6:
|
||||
version "6.0.4"
|
||||
resolved "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b"
|
||||
integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
|
||||
dependencies:
|
||||
delaunator "5"
|
||||
|
||||
"d3-dispatch@1 - 3", d3-dispatch@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
|
||||
integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
|
||||
|
||||
"d3-drag@2 - 3", d3-drag@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
|
||||
integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-selection "3"
|
||||
|
||||
"d3-dsv@1 - 3", d3-dsv@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73"
|
||||
integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
|
||||
dependencies:
|
||||
commander "7"
|
||||
iconv-lite "0.6"
|
||||
rw "1"
|
||||
|
||||
"d3-ease@1 - 3", d3-ease@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
|
||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||
|
||||
d3-fetch@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22"
|
||||
integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
|
||||
dependencies:
|
||||
d3-dsv "1 - 3"
|
||||
|
||||
d3-force@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
|
||||
integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-quadtree "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
"d3-format@1 - 3", d3-format@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641"
|
||||
integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
|
||||
|
||||
d3-geo@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e"
|
||||
integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==
|
||||
dependencies:
|
||||
d3-array "2.5.0 - 3"
|
||||
|
||||
d3-hierarchy@3:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
|
||||
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
|
||||
|
||||
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||
integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526"
|
||||
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
|
||||
|
||||
d3-polygon@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398"
|
||||
integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
|
||||
|
||||
"d3-quadtree@1 - 3", d3-quadtree@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
|
||||
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
|
||||
|
||||
d3-random@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4"
|
||||
integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
|
||||
|
||||
d3-scale-chromatic@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a"
|
||||
integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
|
||||
d3-scale@4:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396"
|
||||
integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
|
||||
dependencies:
|
||||
d3-array "2.10.0 - 3"
|
||||
d3-format "1 - 3"
|
||||
d3-interpolate "1.2.0 - 3"
|
||||
d3-time "2.1.1 - 3"
|
||||
d3-time-format "2 - 4"
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
|
||||
|
||||
d3-shape@3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5"
|
||||
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
|
||||
dependencies:
|
||||
d3-path "^3.1.0"
|
||||
|
||||
"d3-time-format@2 - 4", d3-time-format@4:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a"
|
||||
integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
|
||||
dependencies:
|
||||
d3-time "1 - 3"
|
||||
|
||||
"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7"
|
||||
integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
|
||||
dependencies:
|
||||
d3-array "2 - 3"
|
||||
|
||||
"d3-timer@1 - 3", d3-timer@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
|
||||
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
|
||||
|
||||
"d3-transition@2 - 3", d3-transition@3:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
|
||||
integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
d3-dispatch "1 - 3"
|
||||
d3-ease "1 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
d3-zoom@3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
|
||||
integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-drag "2 - 3"
|
||||
d3-interpolate "1 - 3"
|
||||
d3-selection "2 - 3"
|
||||
d3-transition "2 - 3"
|
||||
|
||||
d3@^7.4.0, d3@^7.8.2:
|
||||
version "7.8.4"
|
||||
resolved "https://registry.npmmirror.com/d3/-/d3-7.8.4.tgz#e35d45800e4068cab07e59e5d883a4bb42ab217f"
|
||||
integrity sha512-q2WHStdhiBtD8DMmhDPyJmXUxr6VWRngKyiJ5EfXMxPw+tqT6BhNjhJZ4w3BHsNm3QoVfZLY8Orq/qPFczwKRA==
|
||||
dependencies:
|
||||
d3-array "3"
|
||||
d3-axis "3"
|
||||
d3-brush "3"
|
||||
d3-chord "3"
|
||||
d3-color "3"
|
||||
d3-contour "4"
|
||||
d3-delaunay "6"
|
||||
d3-dispatch "3"
|
||||
d3-drag "3"
|
||||
d3-dsv "3"
|
||||
d3-ease "3"
|
||||
d3-fetch "3"
|
||||
d3-force "3"
|
||||
d3-format "3"
|
||||
d3-geo "3"
|
||||
d3-hierarchy "3"
|
||||
d3-interpolate "3"
|
||||
d3-path "3"
|
||||
d3-polygon "3"
|
||||
d3-quadtree "3"
|
||||
d3-random "3"
|
||||
d3-scale "4"
|
||||
d3-scale-chromatic "3"
|
||||
d3-selection "3"
|
||||
d3-shape "3"
|
||||
d3-time "3"
|
||||
d3-time-format "4"
|
||||
d3-timer "3"
|
||||
d3-transition "3"
|
||||
d3-zoom "3"
|
||||
|
||||
dagre-d3-es@7.0.10:
|
||||
version "7.0.10"
|
||||
resolved "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz#19800d4be674379a3cd8c86a8216a2ac6827cadc"
|
||||
integrity sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==
|
||||
dependencies:
|
||||
d3 "^7.8.2"
|
||||
lodash-es "^4.17.21"
|
||||
|
||||
damerau-levenshtein@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
@@ -1983,6 +2292,11 @@ data-uri-to-buffer@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
|
||||
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
|
||||
|
||||
dayjs@^1.11.7:
|
||||
version "1.11.7"
|
||||
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2"
|
||||
integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==
|
||||
|
||||
debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
@@ -2050,6 +2364,13 @@ define-properties@^1.1.3, define-properties@^1.1.4:
|
||||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
delaunator@5:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b"
|
||||
integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==
|
||||
dependencies:
|
||||
robust-predicates "^3.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
@@ -2107,6 +2428,11 @@ domhandler@^4.2.0, domhandler@^4.3.1:
|
||||
dependencies:
|
||||
domelementtype "^2.2.0"
|
||||
|
||||
dompurify@2.4.5:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.npmmirror.com/dompurify/-/dompurify-2.4.5.tgz#0e89a27601f0bad978f9a924e7a05d5d2cccdd87"
|
||||
integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==
|
||||
|
||||
domutils@^2.8.0:
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||
@@ -2126,6 +2452,11 @@ electron-to-chromium@^1.4.284:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.345.tgz#c90b7183b39245cddf0e990337469063bfced6f0"
|
||||
integrity sha512-znGhOQK2TUYLICgS25uaM0a7pHy66rSxbre7l762vg9AUoCcJK+Bu+HCPWpjL/U/kK8/Hf+6E0szAUJSyVYb3Q==
|
||||
|
||||
elkjs@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.npmmirror.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
||||
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
||||
|
||||
emoji-picker-react@^4.4.7:
|
||||
version "4.4.8"
|
||||
resolved "https://registry.yarnpkg.com/emoji-picker-react/-/emoji-picker-react-4.4.8.tgz#cd18e942720d0d01e3d488a008f5e79aa315ec87"
|
||||
@@ -2905,6 +3236,11 @@ hastscript@^7.0.0:
|
||||
property-information "^6.0.0"
|
||||
space-separated-tokens "^2.0.0"
|
||||
|
||||
heap@^0.2.6:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.npmmirror.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc"
|
||||
integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==
|
||||
|
||||
highlight.js@~11.7.0:
|
||||
version "11.7.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
|
||||
@@ -2927,6 +3263,13 @@ husky@^8.0.0:
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
|
||||
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
|
||||
|
||||
iconv-lite@0.6:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ignore@^5.2.0:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
@@ -2982,6 +3325,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
|
||||
has "^1.0.3"
|
||||
side-channel "^1.0.4"
|
||||
|
||||
"internmap@1 - 2":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009"
|
||||
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
|
||||
|
||||
is-arguments@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
|
||||
@@ -3272,6 +3620,11 @@ katex@^0.15.0:
|
||||
dependencies:
|
||||
commander "^8.0.0"
|
||||
|
||||
khroma@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmmirror.com/khroma/-/khroma-2.0.0.tgz#7577de98aed9f36c7a474c4d453d94c0d6c6588b"
|
||||
integrity sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==
|
||||
|
||||
kleur@^4.0.3:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780"
|
||||
@@ -3289,6 +3642,16 @@ language-tags@=1.0.5:
|
||||
dependencies:
|
||||
language-subtag-registry "~0.3.2"
|
||||
|
||||
layout-base@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz#1291e296883c322a9dd4c5dd82063721b53e26e2"
|
||||
integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==
|
||||
|
||||
layout-base@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmmirror.com/layout-base/-/layout-base-2.0.1.tgz#d0337913586c90f9c2c075292069f5c2da5dd285"
|
||||
integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||
@@ -3347,6 +3710,11 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
@@ -3357,6 +3725,11 @@ lodash.merge@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-update@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
|
||||
@@ -3574,6 +3947,29 @@ merge2@^1.3.0, merge2@^1.4.1:
|
||||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
mermaid@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.npmmirror.com/mermaid/-/mermaid-10.1.0.tgz#6e40d5250174f4750ca6548e4ee00f6ae210855a"
|
||||
integrity sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA==
|
||||
dependencies:
|
||||
"@braintree/sanitize-url" "^6.0.0"
|
||||
"@khanacademy/simple-markdown" "^0.8.6"
|
||||
cytoscape "^3.23.0"
|
||||
cytoscape-cose-bilkent "^4.1.0"
|
||||
cytoscape-fcose "^2.1.0"
|
||||
d3 "^7.4.0"
|
||||
dagre-d3-es "7.0.10"
|
||||
dayjs "^1.11.7"
|
||||
dompurify "2.4.5"
|
||||
elkjs "^0.8.2"
|
||||
khroma "^2.0.0"
|
||||
lodash-es "^4.17.21"
|
||||
non-layered-tidy-tree-layout "^2.0.2"
|
||||
stylis "^4.1.2"
|
||||
ts-dedent "^2.2.0"
|
||||
uuid "^9.0.0"
|
||||
web-worker "^1.2.0"
|
||||
|
||||
micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
|
||||
@@ -3970,6 +4366,11 @@ node-releases@^2.0.8:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
|
||||
integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==
|
||||
|
||||
non-layered-tidy-tree-layout@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804"
|
||||
integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||
@@ -4520,6 +4921,11 @@ rimraf@^3.0.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
robust-predicates@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a"
|
||||
integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
@@ -4527,6 +4933,11 @@ run-parallel@^1.1.9:
|
||||
dependencies:
|
||||
queue-microtask "^1.2.2"
|
||||
|
||||
rw@1:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
|
||||
integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
|
||||
|
||||
rxjs@^7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4"
|
||||
@@ -4550,6 +4961,11 @@ safe-regex-test@^1.0.0:
|
||||
get-intrinsic "^1.1.3"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sass@^1.59.2:
|
||||
version "1.60.0"
|
||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.60.0.tgz#657f0c23a302ac494b09a5ba8497b739fb5b5a81"
|
||||
@@ -4784,6 +5200,11 @@ styled-jsx@5.1.1:
|
||||
dependencies:
|
||||
client-only "0.0.1"
|
||||
|
||||
stylis@^4.1.2:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.npmmirror.com/stylis/-/stylis-4.1.4.tgz#9cb60e7153d8ac6d02d773552bf51c7a0344535b"
|
||||
integrity sha512-USf5pszRYwuE6hg9by0OkKChkQYEXfkeTtm0xKw+jqQhwyjCVLdYyMBK7R+n7dhzsblAWJnGxju4vxq5eH20GQ==
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
@@ -4879,6 +5300,11 @@ trough@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876"
|
||||
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
|
||||
|
||||
ts-dedent@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5"
|
||||
integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
|
||||
|
||||
tsconfig-paths@^3.14.1:
|
||||
version "3.14.2"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"
|
||||
@@ -5072,6 +5498,11 @@ use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||
|
||||
uuid@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||
|
||||
uvu@^0.5.0:
|
||||
version "0.5.6"
|
||||
resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df"
|
||||
@@ -5118,6 +5549,11 @@ web-streams-polyfill@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
|
||||
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
|
||||
|
||||
web-worker@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da"
|
||||
integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==
|
||||
|
||||
which-boxed-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
|
||||
|
Reference in New Issue
Block a user