mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-09 03:26:38 +08:00
Merge remote-tracking branch 'upstream/main' into dev
This commit is contained in:
commit
1bec280900
38
.github/workflows/sync.yml
vendored
38
.github/workflows/sync.yml
vendored
@ -1,29 +1,33 @@
|
|||||||
name: Upstream Sync
|
name: Upstream Sync
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */12 * * *' # every 12 hours
|
- cron: "0 */6 * * *" # every 6 hours
|
||||||
workflow_dispatch: # on button click
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync_latest_from_upstream:
|
sync_latest_from_upstream:
|
||||||
name: Sync latest commits from upstream repo
|
name: Sync latest commits from upstream repo
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.repository.fork }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Step 1: run a standard checkout action, provided by github
|
# Step 1: run a standard checkout action
|
||||||
- name: Checkout target repo
|
- name: Checkout target repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Step 2: run the sync action
|
# Step 2: run the sync action
|
||||||
- name: Sync upstream changes
|
- name: Sync upstream changes
|
||||||
id: sync
|
id: sync
|
||||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||||
with:
|
with:
|
||||||
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
upstream_sync_repo: Yidadaa/ChatGPT-Next-Web
|
||||||
upstream_sync_branch: main
|
upstream_sync_branch: main
|
||||||
target_sync_branch: main
|
target_sync_branch: main
|
||||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||||
|
|
||||||
# Set test_mode true to run tests instead of the true action!!
|
# Set test_mode true to run tests instead of the true action!!
|
||||||
test_mode: false
|
test_mode: false
|
||||||
|
18
README.md
18
README.md
@ -35,14 +35,17 @@ One-Click to deploy your own ChatGPT web UI.
|
|||||||
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
|
- Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
|
||||||
- Automatically compresses chat history to support long conversations while also saving your tokens
|
- Automatically compresses chat history to support long conversations while also saving your tokens
|
||||||
- One-click export all chat history with full Markdown support
|
- One-click export all chat history with full Markdown support
|
||||||
|
- I18n supported
|
||||||
|
|
||||||
## 开发计划 Roadmap
|
## 开发计划 Roadmap
|
||||||
|
|
||||||
- System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
- System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
|
||||||
- User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
|
- User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
|
||||||
- Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
|
- Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
|
||||||
- Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
- Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
|
||||||
|
|
||||||
### 不会开发的功能 Not in Plan
|
### 不会开发的功能 Not in Plan
|
||||||
|
|
||||||
- User login, accounts, cloud sync 用户登录、账号管理、消息云同步
|
- User login, accounts, cloud sync 用户登录、账号管理、消息云同步
|
||||||
- UI text customize 界面文字自定义
|
- UI text customize 界面文字自定义
|
||||||
|
|
||||||
@ -71,7 +74,9 @@ One-Click to deploy your own ChatGPT web UI.
|
|||||||
- 前往 vercel 控制台,删除掉原先的 project,然后新建 project,选择你刚刚 fork 出来的项目重新进行部署即可;
|
- 前往 vercel 控制台,删除掉原先的 project,然后新建 project,选择你刚刚 fork 出来的项目重新进行部署即可;
|
||||||
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
|
- 在重新部署的过程中,请手动添加名为 `OPENAI_API_KEY` 的环境变量,并填入你的 api key 作为值。
|
||||||
|
|
||||||
本项目会持续更新,如果你想让代码库总是保持更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步,建议定期进行同步操作以获得新功能。
|
本项目会持续更新,当你 Fork 项目之后,默认会每天自动同步上游代码,无需额外操作。
|
||||||
|
|
||||||
|
如果你想让手动立即更新,可以查看 [Github 的文档](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) 了解如何让 fork 的项目与上游代码同步。
|
||||||
|
|
||||||
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
|
你可以 star/watch 本项目或者 follow 作者来及时获得新功能更新通知。
|
||||||
|
|
||||||
@ -84,7 +89,9 @@ We recommend that you follow the steps below to re-deploy:
|
|||||||
- Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy;
|
- Go to the Vercel dashboard, delete the original project, then create a new project and select the project you just forked to redeploy;
|
||||||
- Please manually add an environment variable named `OPENAI_API_KEY` and enter your API key as the value during the redeploy process.
|
- Please manually add an environment variable named `OPENAI_API_KEY` and enter your API key as the value during the redeploy process.
|
||||||
|
|
||||||
This project will be continuously maintained. If you want to keep the code repository up to date, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code. It is recommended to perform synchronization operations regularly.
|
This project will be continuously updated, and after forking the project, the upstream code will be automatically synchronized every day without additional operations.
|
||||||
|
|
||||||
|
If you want to update instantly, you can check out the [Github documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork) to learn how to synchronize a forked project with upstream code.
|
||||||
|
|
||||||
You can star or watch this project or follow author to get release notifictions in time.
|
You can star or watch this project or follow author to get release notifictions in time.
|
||||||
|
|
||||||
@ -180,14 +187,7 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
|
|||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 捐赠 Donate USDT
|
|
||||||
> BNB Smart Chain (BEP 20)
|
|
||||||
```
|
|
||||||
0x67cD02c7EB62641De576a1fA3EdB32eA0c3ffD89
|
|
||||||
```
|
|
||||||
|
|
||||||
## 鸣谢 Special Thanks
|
## 鸣谢 Special Thanks
|
||||||
|
|
||||||
### 捐赠者 Sponsor
|
### 捐赠者 Sponsor
|
||||||
|
|
||||||
[@mushan0x0](https://github.com/mushan0x0)
|
[@mushan0x0](https://github.com/mushan0x0)
|
||||||
|
@ -8,6 +8,15 @@ async function createStream(req: NextRequest) {
|
|||||||
|
|
||||||
const res = await requestOpenai(req);
|
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({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
function onParse(event: any) {
|
function onParse(event: any) {
|
||||||
|
@ -6,19 +6,21 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
box-shadow: var(--card-shadow);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button:hover {
|
.icon-button:hover {
|
||||||
filter: brightness(0.9);
|
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,25 +38,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin dark-button {
|
|
||||||
div:not(:global(.no-dark))>.icon-button-icon {
|
|
||||||
filter: invert(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button:hover {
|
|
||||||
filter: brightness(1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) {
|
|
||||||
@include dark-button;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
@include dark-button;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button-text {
|
.icon-button-text {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ export function IconButton(props: {
|
|||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
text?: string;
|
text?: string;
|
||||||
bordered?: boolean;
|
bordered?: boolean;
|
||||||
|
shadow?: boolean;
|
||||||
|
noDark?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
}) {
|
}) {
|
||||||
@ -14,12 +16,19 @@ export function IconButton(props: {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
styles["icon-button"] +
|
styles["icon-button"] +
|
||||||
` ${props.bordered && styles.border} ${props.className ?? ""}`
|
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
|
||||||
|
props.className ?? ""
|
||||||
|
} clickable`
|
||||||
}
|
}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
|
role="button"
|
||||||
>
|
>
|
||||||
<div className={styles["icon-button-icon"]}>{props.icon}</div>
|
<div
|
||||||
|
className={styles["icon-button-icon"] + ` ${props.noDark && "no-dark"}`}
|
||||||
|
>
|
||||||
|
{props.icon}
|
||||||
|
</div>
|
||||||
{props.text && (
|
{props.text && (
|
||||||
<div className={styles["icon-button-text"]}>{props.text}</div>
|
<div className={styles["icon-button-text"]}>{props.text}</div>
|
||||||
)}
|
)}
|
||||||
|
73
app/components/chat-list.tsx
Normal file
73
app/components/chat-list.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Message,
|
||||||
|
SubmitKey,
|
||||||
|
useChatStore,
|
||||||
|
ChatSession,
|
||||||
|
BOT_HELLO,
|
||||||
|
} from "../store";
|
||||||
|
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { isMobileScreen } from "../utils";
|
||||||
|
|
||||||
|
export function ChatItem(props: {
|
||||||
|
onClick?: () => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
title: string;
|
||||||
|
count: number;
|
||||||
|
time: string;
|
||||||
|
selected: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles["chat-item"]} ${
|
||||||
|
props.selected && styles["chat-item-selected"]
|
||||||
|
}`}
|
||||||
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
|
<div className={styles["chat-item-title"]}>{props.title}</div>
|
||||||
|
<div className={styles["chat-item-info"]}>
|
||||||
|
<div className={styles["chat-item-count"]}>
|
||||||
|
{Locale.ChatItem.ChatItemCount(props.count)}
|
||||||
|
</div>
|
||||||
|
<div className={styles["chat-item-date"]}>{props.time}</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatList() {
|
||||||
|
const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
|
||||||
|
(state) => [
|
||||||
|
state.sessions,
|
||||||
|
state.currentSessionIndex,
|
||||||
|
state.selectSession,
|
||||||
|
state.removeSession,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["chat-list"]}>
|
||||||
|
{sessions.map((item, i) => (
|
||||||
|
<ChatItem
|
||||||
|
title={item.topic}
|
||||||
|
time={item.lastUpdate}
|
||||||
|
count={item.messages.length}
|
||||||
|
key={i}
|
||||||
|
selected={i === selectedIndex}
|
||||||
|
onClick={() => selectSession(i)}
|
||||||
|
onDelete={() =>
|
||||||
|
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
|
||||||
|
removeSession(i)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
75
app/components/chat.module.scss
Normal file
75
app/components/chat.module.scss
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
.prompt-toast {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -50px;
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
|
||||||
|
.prompt-toast-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
border: var(--border-in-light);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 100px;
|
||||||
|
|
||||||
|
animation: slide-in-from-top ease 0.3s;
|
||||||
|
|
||||||
|
.prompt-toast-content {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-prompt {
|
||||||
|
.context-prompt-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.context-role {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-content {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-delete-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-prompt-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-prompt {
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.memory-prompt-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-prompt-content {
|
||||||
|
background-color: var(--gray);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
}
|
643
app/components/chat.tsx
Normal file
643
app/components/chat.tsx
Normal file
@ -0,0 +1,643 @@
|
|||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
||||||
|
|
||||||
|
import SendWhiteIcon from "../icons/send-white.svg";
|
||||||
|
import BrainIcon from "../icons/brain.svg";
|
||||||
|
import ExportIcon from "../icons/export.svg";
|
||||||
|
import MenuIcon from "../icons/menu.svg";
|
||||||
|
import CopyIcon from "../icons/copy.svg";
|
||||||
|
import DownloadIcon from "../icons/download.svg";
|
||||||
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
|
import BotIcon from "../icons/bot.svg";
|
||||||
|
import AddIcon from "../icons/add.svg";
|
||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
|
||||||
|
import { Message, SubmitKey, useChatStore, BOT_HELLO, ROLES } from "../store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
downloadAs,
|
||||||
|
isMobileScreen,
|
||||||
|
selectOrCopy,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
import { ControllerPool } from "../requests";
|
||||||
|
import { Prompt, usePromptStore } from "../store/prompt";
|
||||||
|
import Locale from "../locales";
|
||||||
|
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import styles from "./home.module.scss";
|
||||||
|
import chatStyle from "./chat.module.scss";
|
||||||
|
|
||||||
|
import { Modal, showModal, showToast } from "./ui-lib";
|
||||||
|
|
||||||
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
|
loading: () => <LoadingIcon />,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
||||||
|
loading: () => <LoadingIcon />,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function Avatar(props: { role: Message["role"] }) {
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
|
if (props.role !== "user") {
|
||||||
|
return <BotIcon className={styles["user-avtar"]} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["user-avtar"]}>
|
||||||
|
<Emoji unified={config.avatar} size={18} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportMessages(messages: Message[], topic: string) {
|
||||||
|
const mdText =
|
||||||
|
`# ${topic}\n\n` +
|
||||||
|
messages
|
||||||
|
.map((m) => {
|
||||||
|
return m.role === "user" ? `## ${m.content}` : m.content.trim();
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
const filename = `${topic}.md`;
|
||||||
|
|
||||||
|
showModal({
|
||||||
|
title: Locale.Export.Title,
|
||||||
|
children: (
|
||||||
|
<div className="markdown-body">
|
||||||
|
<pre className={styles["export-content"]}>{mdText}</pre>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Export.Copy}
|
||||||
|
onClick={() => copyToClipboard(mdText)}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="download"
|
||||||
|
icon={<DownloadIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Export.Download}
|
||||||
|
onClick={() => downloadAs(mdText, filename)}
|
||||||
|
/>,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function PromptToast(props: {
|
||||||
|
showToast?: boolean;
|
||||||
|
showModal?: boolean;
|
||||||
|
setShowModal: (_: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const session = chatStore.currentSession();
|
||||||
|
const context = session.context;
|
||||||
|
|
||||||
|
const addContextPrompt = (prompt: Message) => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.context.push(prompt);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeContextPrompt = (i: number) => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.context.splice(i, 1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateContextPrompt = (i: number, prompt: Message) => {
|
||||||
|
chatStore.updateCurrentSession((session) => {
|
||||||
|
session.context[i] = prompt;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={chatStyle["prompt-toast"]} key="prompt-toast">
|
||||||
|
{props.showToast && (
|
||||||
|
<div
|
||||||
|
className={chatStyle["prompt-toast-inner"] + " clickable"}
|
||||||
|
role="button"
|
||||||
|
onClick={() => props.setShowModal(true)}
|
||||||
|
>
|
||||||
|
<BrainIcon />
|
||||||
|
<span className={chatStyle["prompt-toast-content"]}>
|
||||||
|
{Locale.Context.Toast(context.length)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{props.showModal && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Context.Edit}
|
||||||
|
onClose={() => props.setShowModal(false)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="copy"
|
||||||
|
icon={<CopyIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.Memory.Copy}
|
||||||
|
onClick={() => copyToClipboard(session.memoryPrompt)}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<div className={chatStyle["context-prompt"]}>
|
||||||
|
{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"]}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateContextPrompt(i, {
|
||||||
|
...c,
|
||||||
|
content: e.target.value as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
className={chatStyle["context-delete-button"]}
|
||||||
|
onClick={() => removeContextPrompt(i)}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className={chatStyle["context-prompt-row"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text={Locale.Context.Add}
|
||||||
|
bordered
|
||||||
|
className={chatStyle["context-prompt-button"]}
|
||||||
|
onClick={() =>
|
||||||
|
addContextPrompt({
|
||||||
|
role: "system",
|
||||||
|
content: "",
|
||||||
|
date: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={chatStyle["memory-prompt"]}>
|
||||||
|
<div className={chatStyle["memory-prompt-title"]}>
|
||||||
|
{Locale.Memory.Title} ({session.lastSummarizeIndex} of{" "}
|
||||||
|
{session.messages.length})
|
||||||
|
</div>
|
||||||
|
<div className={chatStyle["memory-prompt-content"]}>
|
||||||
|
{session.memoryPrompt || Locale.Memory.EmptyContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSubmitHandler() {
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
const submitKey = config.submitKey;
|
||||||
|
|
||||||
|
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key !== "Enter") return false;
|
||||||
|
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
|
||||||
|
return (
|
||||||
|
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
||||||
|
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
||||||
|
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
|
||||||
|
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
|
||||||
|
(config.submitKey === SubmitKey.Enter &&
|
||||||
|
!e.altKey &&
|
||||||
|
!e.ctrlKey &&
|
||||||
|
!e.shiftKey &&
|
||||||
|
!e.metaKey)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
submitKey,
|
||||||
|
shouldSubmit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PromptHints(props: {
|
||||||
|
prompts: Prompt[];
|
||||||
|
onPromptSelect: (prompt: Prompt) => void;
|
||||||
|
}) {
|
||||||
|
if (props.prompts.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles["prompt-hints"]}>
|
||||||
|
{props.prompts.map((prompt, i) => (
|
||||||
|
<div
|
||||||
|
className={styles["prompt-hint"]}
|
||||||
|
key={prompt.title + i.toString()}
|
||||||
|
onClick={() => props.onPromptSelect(prompt)}
|
||||||
|
>
|
||||||
|
<div className={styles["hint-title"]}>{prompt.title}</div>
|
||||||
|
<div className={styles["hint-content"]}>{prompt.content}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useScrollToBottom() {
|
||||||
|
// for auto-scroll
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
|
||||||
|
// auto scroll
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const dom = scrollRef.current;
|
||||||
|
if (dom && autoScroll) {
|
||||||
|
setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrollRef,
|
||||||
|
autoScroll,
|
||||||
|
setAutoScroll,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Chat(props: {
|
||||||
|
showSideBar?: () => void;
|
||||||
|
sideBarShowing?: boolean;
|
||||||
|
}) {
|
||||||
|
type RenderMessage = Message & { preview?: boolean };
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
const [session, sessionIndex] = useChatStore((state) => [
|
||||||
|
state.currentSession(),
|
||||||
|
state.currentSessionIndex,
|
||||||
|
]);
|
||||||
|
const fontSize = useChatStore((state) => state.config.fontSize);
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [userInput, setUserInput] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { submitKey, shouldSubmit } = useSubmitHandler();
|
||||||
|
const { scrollRef, setAutoScroll } = useScrollToBottom();
|
||||||
|
const [hitBottom, setHitBottom] = useState(false);
|
||||||
|
|
||||||
|
const onChatBodyScroll = (e: HTMLElement) => {
|
||||||
|
const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
|
||||||
|
setHitBottom(isTouchBottom);
|
||||||
|
};
|
||||||
|
|
||||||
|
// prompt hints
|
||||||
|
const promptStore = usePromptStore();
|
||||||
|
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
||||||
|
const onSearch = useDebouncedCallback(
|
||||||
|
(text: string) => {
|
||||||
|
setPromptHints(promptStore.search(text));
|
||||||
|
},
|
||||||
|
100,
|
||||||
|
{ leading: true, trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPromptSelect = (prompt: Prompt) => {
|
||||||
|
setUserInput(prompt.content);
|
||||||
|
setPromptHints([]);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollInput = () => {
|
||||||
|
const dom = inputRef.current;
|
||||||
|
if (!dom) return;
|
||||||
|
const paddingBottomNum: number = parseInt(
|
||||||
|
window.getComputedStyle(dom).paddingBottom,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
||||||
|
};
|
||||||
|
|
||||||
|
// only search prompts when user input is short
|
||||||
|
const SEARCH_TEXT_LIMIT = 30;
|
||||||
|
const onInput = (text: string) => {
|
||||||
|
scrollInput();
|
||||||
|
setUserInput(text);
|
||||||
|
const n = text.trim().length;
|
||||||
|
|
||||||
|
// clear search results
|
||||||
|
if (n === 0) {
|
||||||
|
setPromptHints([]);
|
||||||
|
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
||||||
|
// check if need to trigger auto completion
|
||||||
|
if (text.startsWith("/")) {
|
||||||
|
let searchText = text.slice(1);
|
||||||
|
if (searchText.length === 0) {
|
||||||
|
searchText = " ";
|
||||||
|
}
|
||||||
|
onSearch(searchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// submit user input
|
||||||
|
const onUserSubmit = () => {
|
||||||
|
if (userInput.length <= 0) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
||||||
|
setUserInput("");
|
||||||
|
setPromptHints([]);
|
||||||
|
if (!isMobileScreen()) inputRef.current?.focus();
|
||||||
|
setAutoScroll(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// stop response
|
||||||
|
const onUserStop = (messageIndex: number) => {
|
||||||
|
ControllerPool.stop(sessionIndex, messageIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if should send message
|
||||||
|
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (shouldSubmit(e)) {
|
||||||
|
onUserSubmit();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResend = (botIndex: number) => {
|
||||||
|
// find last user input message and resend
|
||||||
|
for (let i = botIndex; i >= 0; i -= 1) {
|
||||||
|
if (messages[i].role === "user") {
|
||||||
|
setIsLoading(true);
|
||||||
|
chatStore
|
||||||
|
.onUserInput(messages[i].content)
|
||||||
|
.then(() => setIsLoading(false));
|
||||||
|
inputRef.current?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
|
const context: RenderMessage[] = session.context.slice();
|
||||||
|
|
||||||
|
if (
|
||||||
|
context.length === 0 &&
|
||||||
|
session.messages.at(0)?.content !== BOT_HELLO.content
|
||||||
|
) {
|
||||||
|
context.push(BOT_HELLO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// preview messages
|
||||||
|
const messages = context
|
||||||
|
.concat(session.messages as RenderMessage[])
|
||||||
|
.concat(
|
||||||
|
isLoading
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: "……",
|
||||||
|
date: new Date().toLocaleString(),
|
||||||
|
preview: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
.concat(
|
||||||
|
userInput.length > 0 && config.sendPreviewBubble
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: userInput,
|
||||||
|
date: new Date().toLocaleString(),
|
||||||
|
preview: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showPromptModal, setShowPromptModal] = useState(false);
|
||||||
|
|
||||||
|
// Auto focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.sideBarShowing && isMobileScreen()) return;
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.chat} key={session.id}>
|
||||||
|
<div className={styles["window-header"]}>
|
||||||
|
<div
|
||||||
|
className={styles["window-header-title"]}
|
||||||
|
onClick={props?.showSideBar}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
||||||
|
onClick={() => {
|
||||||
|
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
||||||
|
if (newTopic && newTopic !== session.topic) {
|
||||||
|
chatStore.updateCurrentSession(
|
||||||
|
(session) => (session.topic = newTopic!),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session.topic}
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-header-sub-title"]}>
|
||||||
|
{Locale.Chat.SubTitle(session.messages.length)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-actions"]}>
|
||||||
|
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
||||||
|
<IconButton
|
||||||
|
icon={<MenuIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.ChatList}
|
||||||
|
onClick={props?.showSideBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<BrainIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.CompressedHistory}
|
||||||
|
onClick={() => {
|
||||||
|
setShowPromptModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ExportIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.Export}
|
||||||
|
onClick={() => {
|
||||||
|
exportMessages(
|
||||||
|
session.messages.filter((msg) => !msg.isError),
|
||||||
|
session.topic,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PromptToast
|
||||||
|
showToast={!hitBottom}
|
||||||
|
showModal={showPromptModal}
|
||||||
|
setShowModal={setShowPromptModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["chat-body"]}
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
|
||||||
|
onWheel={(e) => setAutoScroll(hitBottom && e.deltaY > 0)}
|
||||||
|
onTouchStart={() => {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
setAutoScroll(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.map((message, i) => {
|
||||||
|
const isUser = message.role === "user";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={
|
||||||
|
isUser ? styles["chat-message-user"] : styles["chat-message"]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles["chat-message-container"]}>
|
||||||
|
<div className={styles["chat-message-avatar"]}>
|
||||||
|
<Avatar role={message.role} />
|
||||||
|
</div>
|
||||||
|
{(message.preview || message.streaming) && (
|
||||||
|
<div className={styles["chat-message-status"]}>
|
||||||
|
{Locale.Chat.Typing}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles["chat-message-item"]}>
|
||||||
|
{!isUser &&
|
||||||
|
!(message.preview || message.content.length === 0) && (
|
||||||
|
<div className={styles["chat-message-top-actions"]}>
|
||||||
|
{message.streaming ? (
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onUserStop(i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Stop}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => onResend(i)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Retry}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles["chat-message-top-action"]}
|
||||||
|
onClick={() => copyToClipboard(message.content)}
|
||||||
|
>
|
||||||
|
{Locale.Chat.Actions.Copy}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(message.preview || message.content.length === 0) &&
|
||||||
|
!isUser ? (
|
||||||
|
<LoadingIcon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="markdown-body"
|
||||||
|
style={{ fontSize: `${fontSize}px` }}
|
||||||
|
onContextMenu={(e) => onRightClick(e, message)}
|
||||||
|
onDoubleClickCapture={() => {
|
||||||
|
if (!isMobileScreen()) return;
|
||||||
|
setUserInput(message.content);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Markdown content={message.content} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isUser && !message.preview && (
|
||||||
|
<div className={styles["chat-message-actions"]}>
|
||||||
|
<div className={styles["chat-message-action-date"]}>
|
||||||
|
{message.date.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["chat-input-panel"]}>
|
||||||
|
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
||||||
|
<div className={styles["chat-input-panel-inner"]}>
|
||||||
|
<textarea
|
||||||
|
ref={inputRef}
|
||||||
|
className={styles["chat-input"]}
|
||||||
|
placeholder={Locale.Chat.Input(submitKey)}
|
||||||
|
rows={2}
|
||||||
|
onInput={(e) => onInput(e.currentTarget.value)}
|
||||||
|
value={userInput}
|
||||||
|
onKeyDown={onInputKeyDown}
|
||||||
|
onFocus={() => setAutoScroll(true)}
|
||||||
|
onBlur={() => {
|
||||||
|
setAutoScroll(false);
|
||||||
|
setTimeout(() => setPromptHints([]), 500);
|
||||||
|
}}
|
||||||
|
autoFocus={!props?.sideBarShowing}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<SendWhiteIcon />}
|
||||||
|
text={Locale.Chat.Send}
|
||||||
|
className={styles["chat-input-send"]}
|
||||||
|
noDark
|
||||||
|
onClick={onUserSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
47
app/components/error.tsx
Normal file
47
app/components/error.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import GithubIcon from "../icons/github.svg";
|
||||||
|
import { ISSUE_URL } from "../constant";
|
||||||
|
|
||||||
|
interface IErrorBoundaryState {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
info: React.ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null, info: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
||||||
|
// Update state with error details
|
||||||
|
this.setState({ hasError: true, error, info });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Render error message
|
||||||
|
return (
|
||||||
|
<div className="error">
|
||||||
|
<h2>Oops, something went wrong!</h2>
|
||||||
|
<pre>
|
||||||
|
<code>{this.state.error?.toString()}</code>
|
||||||
|
<code>{this.state.info?.componentStack}</code>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<a href={ISSUE_URL} className="report">
|
||||||
|
<IconButton
|
||||||
|
text="Report This Error"
|
||||||
|
icon={<GithubIcon />}
|
||||||
|
bordered
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// if no error occurred, render children
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
@import "./window.scss";
|
@import "./window.scss";
|
||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
@mixin container {
|
@mixin container {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
@ -73,7 +74,7 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -100%;
|
left: -100%;
|
||||||
z-index: 999;
|
z-index: 1000;
|
||||||
height: var(--full-height);
|
height: var(--full-height);
|
||||||
transition: all ease 0.3s;
|
transition: all ease 0.3s;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@ -132,18 +133,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-item:hover {
|
.chat-item:hover {
|
||||||
background-color: var(--hover-color);
|
background-color: var(--hover-color);
|
||||||
}
|
}
|
||||||
@ -218,6 +207,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-body-title {
|
.chat-body-title {
|
||||||
@ -343,6 +333,7 @@
|
|||||||
.chat-input-panel {
|
.chat-input-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
padding-top: 5px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
require("../polyfill");
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
@ -9,33 +10,21 @@ import styles from "./home.module.scss";
|
|||||||
import SettingsIcon from "../icons/settings.svg";
|
import SettingsIcon from "../icons/settings.svg";
|
||||||
import GithubIcon from "../icons/github.svg";
|
import GithubIcon from "../icons/github.svg";
|
||||||
import ChatGptIcon from "../icons/chatgpt.svg";
|
import ChatGptIcon from "../icons/chatgpt.svg";
|
||||||
import SendWhiteIcon from "../icons/send-white.svg";
|
|
||||||
import BrainIcon from "../icons/brain.svg";
|
|
||||||
import ExportIcon from "../icons/export.svg";
|
|
||||||
import BotIcon from "../icons/bot.svg";
|
import BotIcon from "../icons/bot.svg";
|
||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
import MenuIcon from "../icons/menu.svg";
|
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
|
||||||
import DownloadIcon from "../icons/download.svg";
|
|
||||||
|
|
||||||
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
|
import { useChatStore } from "../store";
|
||||||
import { showModal, showToast } from "./ui-lib";
|
import { isMobileScreen } from "../utils";
|
||||||
import {
|
|
||||||
copyToClipboard,
|
|
||||||
downloadAs,
|
|
||||||
isIOS,
|
|
||||||
isMobileScreen,
|
|
||||||
selectOrCopy,
|
|
||||||
} from "../utils";
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
import { ChatList } from "./chat-list";
|
||||||
|
import { Chat } from "./chat";
|
||||||
|
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { REPO_URL } from "../constant";
|
import { REPO_URL } from "../constant";
|
||||||
import { ControllerPool } from "../requests";
|
import { ErrorBoundary } from "./error";
|
||||||
import { Prompt, usePromptStore } from "../store/prompt";
|
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@ -46,470 +35,10 @@ export function Loading(props: { noLogo?: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|
||||||
loading: () => <LoadingIcon />,
|
|
||||||
});
|
|
||||||
|
|
||||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
|
||||||
loading: () => <LoadingIcon />,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Avatar(props: { role: Message["role"] }) {
|
|
||||||
const config = useChatStore((state) => state.config);
|
|
||||||
|
|
||||||
if (props.role === "assistant") {
|
|
||||||
return <BotIcon className={styles["user-avtar"]} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles["user-avtar"]}>
|
|
||||||
<Emoji unified={config.avatar} size={18} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatItem(props: {
|
|
||||||
onClick?: () => void;
|
|
||||||
onDelete?: () => void;
|
|
||||||
title: string;
|
|
||||||
count: number;
|
|
||||||
time: string;
|
|
||||||
selected: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${styles["chat-item"]} ${
|
|
||||||
props.selected && styles["chat-item-selected"]
|
|
||||||
}`}
|
|
||||||
onClick={props.onClick}
|
|
||||||
>
|
|
||||||
<div className={styles["chat-item-title"]}>{props.title}</div>
|
|
||||||
<div className={styles["chat-item-info"]}>
|
|
||||||
<div className={styles["chat-item-count"]}>
|
|
||||||
{Locale.ChatItem.ChatItemCount(props.count)}
|
|
||||||
</div>
|
|
||||||
<div className={styles["chat-item-date"]}>{props.time}</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
|
|
||||||
<DeleteIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ChatList() {
|
|
||||||
const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
|
|
||||||
(state) => [
|
|
||||||
state.sessions,
|
|
||||||
state.currentSessionIndex,
|
|
||||||
state.selectSession,
|
|
||||||
state.removeSession,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles["chat-list"]}>
|
|
||||||
{sessions.map((item, i) => (
|
|
||||||
<ChatItem
|
|
||||||
title={item.topic}
|
|
||||||
time={item.lastUpdate}
|
|
||||||
count={item.messages.length}
|
|
||||||
key={i}
|
|
||||||
selected={i === selectedIndex}
|
|
||||||
onClick={() => selectSession(i)}
|
|
||||||
onDelete={() => confirm(Locale.Home.DeleteChat) && removeSession(i)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSubmitHandler() {
|
|
||||||
const config = useChatStore((state) => state.config);
|
|
||||||
const submitKey = config.submitKey;
|
|
||||||
|
|
||||||
const shouldSubmit = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key !== "Enter") return false;
|
|
||||||
if (e.key === "Enter" && e.nativeEvent.isComposing) return false;
|
|
||||||
return (
|
|
||||||
(config.submitKey === SubmitKey.AltEnter && e.altKey) ||
|
|
||||||
(config.submitKey === SubmitKey.CtrlEnter && e.ctrlKey) ||
|
|
||||||
(config.submitKey === SubmitKey.ShiftEnter && e.shiftKey) ||
|
|
||||||
(config.submitKey === SubmitKey.MetaEnter && e.metaKey) ||
|
|
||||||
(config.submitKey === SubmitKey.Enter &&
|
|
||||||
!e.altKey &&
|
|
||||||
!e.ctrlKey &&
|
|
||||||
!e.shiftKey &&
|
|
||||||
!e.metaKey)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
submitKey,
|
|
||||||
shouldSubmit,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PromptHints(props: {
|
|
||||||
prompts: Prompt[];
|
|
||||||
onPromptSelect: (prompt: Prompt) => void;
|
|
||||||
}) {
|
|
||||||
if (props.prompts.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles["prompt-hints"]}>
|
|
||||||
{props.prompts.map((prompt, i) => (
|
|
||||||
<div
|
|
||||||
className={styles["prompt-hint"]}
|
|
||||||
key={prompt.title + i.toString()}
|
|
||||||
onClick={() => props.onPromptSelect(prompt)}
|
|
||||||
>
|
|
||||||
<div className={styles["hint-title"]}>{prompt.title}</div>
|
|
||||||
<div className={styles["hint-content"]}>{prompt.content}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Chat(props: {
|
|
||||||
showSideBar?: () => void;
|
|
||||||
sideBarShowing?: boolean;
|
|
||||||
}) {
|
|
||||||
type RenderMessage = Message & { preview?: boolean };
|
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
|
||||||
const [session, sessionIndex] = useChatStore((state) => [
|
|
||||||
state.currentSession(),
|
|
||||||
state.currentSessionIndex,
|
|
||||||
]);
|
|
||||||
const fontSize = useChatStore((state) => state.config.fontSize);
|
|
||||||
|
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
const [userInput, setUserInput] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { submitKey, shouldSubmit } = useSubmitHandler();
|
|
||||||
|
|
||||||
// prompt hints
|
|
||||||
const promptStore = usePromptStore();
|
|
||||||
const [promptHints, setPromptHints] = useState<Prompt[]>([]);
|
|
||||||
const onSearch = useDebouncedCallback(
|
|
||||||
(text: string) => {
|
|
||||||
setPromptHints(promptStore.search(text));
|
|
||||||
},
|
|
||||||
100,
|
|
||||||
{ leading: true, trailing: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPromptSelect = (prompt: Prompt) => {
|
|
||||||
setUserInput(prompt.content);
|
|
||||||
setPromptHints([]);
|
|
||||||
inputRef.current?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollInput = () => {
|
|
||||||
const dom = inputRef.current;
|
|
||||||
if (!dom) return;
|
|
||||||
const paddingBottomNum: number = parseInt(
|
|
||||||
window.getComputedStyle(dom).paddingBottom,
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
dom.scrollTop = dom.scrollHeight - dom.offsetHeight + paddingBottomNum;
|
|
||||||
};
|
|
||||||
|
|
||||||
// only search prompts when user input is short
|
|
||||||
const SEARCH_TEXT_LIMIT = 30;
|
|
||||||
const onInput = (text: string) => {
|
|
||||||
scrollInput();
|
|
||||||
setUserInput(text);
|
|
||||||
const n = text.trim().length;
|
|
||||||
|
|
||||||
// clear search results
|
|
||||||
if (n === 0) {
|
|
||||||
setPromptHints([]);
|
|
||||||
} else if (!chatStore.config.disablePromptHint && n < SEARCH_TEXT_LIMIT) {
|
|
||||||
// check if need to trigger auto completion
|
|
||||||
if (text.startsWith("/") && text.length > 1) {
|
|
||||||
onSearch(text.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// submit user input
|
|
||||||
const onUserSubmit = () => {
|
|
||||||
if (userInput.length <= 0) return;
|
|
||||||
setIsLoading(true);
|
|
||||||
chatStore.onUserInput(userInput).then(() => setIsLoading(false));
|
|
||||||
setUserInput("");
|
|
||||||
setPromptHints([]);
|
|
||||||
inputRef.current?.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
// stop response
|
|
||||||
const onUserStop = (messageIndex: number) => {
|
|
||||||
console.log(ControllerPool, sessionIndex, messageIndex);
|
|
||||||
ControllerPool.stop(sessionIndex, messageIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
// check if should send message
|
|
||||||
const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (shouldSubmit(e)) {
|
|
||||||
onUserSubmit();
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResend = (botIndex: number) => {
|
|
||||||
// find last user input message and resend
|
|
||||||
for (let i = botIndex; i >= 0; i -= 1) {
|
|
||||||
if (messages[i].role === "user") {
|
|
||||||
setIsLoading(true);
|
|
||||||
chatStore
|
|
||||||
.onUserInput(messages[i].content)
|
|
||||||
.then(() => setIsLoading(false));
|
|
||||||
inputRef.current?.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// for auto-scroll
|
|
||||||
const latestMessageRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
|
||||||
|
|
||||||
const config = useChatStore((state) => state.config);
|
|
||||||
|
|
||||||
// preview messages
|
|
||||||
const messages = (session.messages as RenderMessage[])
|
|
||||||
.concat(
|
|
||||||
isLoading
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
role: "assistant",
|
|
||||||
content: "……",
|
|
||||||
date: new Date().toLocaleString(),
|
|
||||||
preview: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
userInput.length > 0 && config.sendPreviewBubble
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: userInput,
|
|
||||||
date: new Date().toLocaleString(),
|
|
||||||
preview: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
);
|
|
||||||
|
|
||||||
// auto scroll
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const dom = latestMessageRef.current;
|
|
||||||
const inputDom = inputRef.current;
|
|
||||||
|
|
||||||
// only scroll when input overlaped message body
|
|
||||||
let shouldScroll = true;
|
|
||||||
if (dom && inputDom) {
|
|
||||||
const domRect = dom.getBoundingClientRect();
|
|
||||||
const inputRect = inputDom.getBoundingClientRect();
|
|
||||||
shouldScroll = domRect.top > inputRect.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dom && autoScroll && shouldScroll) {
|
|
||||||
dom.scrollIntoView({
|
|
||||||
block: "end",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.chat} key={session.id}>
|
|
||||||
<div className={styles["window-header"]}>
|
|
||||||
<div
|
|
||||||
className={styles["window-header-title"]}
|
|
||||||
onClick={props?.showSideBar}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
|
|
||||||
onClick={() => {
|
|
||||||
const newTopic = prompt(Locale.Chat.Rename, session.topic);
|
|
||||||
if (newTopic && newTopic !== session.topic) {
|
|
||||||
chatStore.updateCurrentSession(
|
|
||||||
(session) => (session.topic = newTopic!),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{session.topic}
|
|
||||||
</div>
|
|
||||||
<div className={styles["window-header-sub-title"]}>
|
|
||||||
{Locale.Chat.SubTitle(session.messages.length)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles["window-actions"]}>
|
|
||||||
<div className={styles["window-action-button"] + " " + styles.mobile}>
|
|
||||||
<IconButton
|
|
||||||
icon={<MenuIcon />}
|
|
||||||
bordered
|
|
||||||
title={Locale.Chat.Actions.ChatList}
|
|
||||||
onClick={props?.showSideBar}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles["window-action-button"]}>
|
|
||||||
<IconButton
|
|
||||||
icon={<BrainIcon />}
|
|
||||||
bordered
|
|
||||||
title={Locale.Chat.Actions.CompressedHistory}
|
|
||||||
onClick={() => {
|
|
||||||
showMemoryPrompt(session);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles["window-action-button"]}>
|
|
||||||
<IconButton
|
|
||||||
icon={<ExportIcon />}
|
|
||||||
bordered
|
|
||||||
title={Locale.Chat.Actions.Export}
|
|
||||||
onClick={() => {
|
|
||||||
exportMessages(session.messages, session.topic);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles["chat-body"]}>
|
|
||||||
{messages.map((message, i) => {
|
|
||||||
const isUser = message.role === "user";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className={
|
|
||||||
isUser ? styles["chat-message-user"] : styles["chat-message"]
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={styles["chat-message-container"]}>
|
|
||||||
<div className={styles["chat-message-avatar"]}>
|
|
||||||
<Avatar role={message.role} />
|
|
||||||
</div>
|
|
||||||
{(message.preview || message.streaming) && (
|
|
||||||
<div className={styles["chat-message-status"]}>
|
|
||||||
{Locale.Chat.Typing}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={styles["chat-message-item"]}>
|
|
||||||
{!isUser &&
|
|
||||||
!(message.preview || message.content.length === 0) && (
|
|
||||||
<div className={styles["chat-message-top-actions"]}>
|
|
||||||
{message.streaming ? (
|
|
||||||
<div
|
|
||||||
className={styles["chat-message-top-action"]}
|
|
||||||
onClick={() => onUserStop(i)}
|
|
||||||
>
|
|
||||||
{Locale.Chat.Actions.Stop}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className={styles["chat-message-top-action"]}
|
|
||||||
onClick={() => onResend(i)}
|
|
||||||
>
|
|
||||||
{Locale.Chat.Actions.Retry}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles["chat-message-top-action"]}
|
|
||||||
onClick={() => copyToClipboard(message.content)}
|
|
||||||
>
|
|
||||||
{Locale.Chat.Actions.Copy}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(message.preview || message.content.length === 0) &&
|
|
||||||
!isUser ? (
|
|
||||||
<LoadingIcon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="markdown-body"
|
|
||||||
style={{ fontSize: `${fontSize}px` }}
|
|
||||||
onContextMenu={(e) => onRightClick(e, message)}
|
|
||||||
onDoubleClickCapture={() => {
|
|
||||||
if (!isMobileScreen()) return;
|
|
||||||
setUserInput(message.content);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Markdown content={message.content} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!isUser && !message.preview && (
|
|
||||||
<div className={styles["chat-message-actions"]}>
|
|
||||||
<div className={styles["chat-message-action-date"]}>
|
|
||||||
{message.date.toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<div ref={latestMessageRef} style={{ opacity: 0, height: "1px" }}>
|
|
||||||
-
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles["chat-input-panel"]}>
|
|
||||||
<PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} />
|
|
||||||
<div className={styles["chat-input-panel-inner"]}>
|
|
||||||
<textarea
|
|
||||||
ref={inputRef}
|
|
||||||
className={styles["chat-input"]}
|
|
||||||
placeholder={Locale.Chat.Input(submitKey)}
|
|
||||||
rows={4}
|
|
||||||
onInput={(e) => onInput(e.currentTarget.value)}
|
|
||||||
value={userInput}
|
|
||||||
onKeyDown={onInputKeyDown}
|
|
||||||
onFocus={() => setAutoScroll(true)}
|
|
||||||
onBlur={() => {
|
|
||||||
setAutoScroll(false);
|
|
||||||
setTimeout(() => setPromptHints([]), 500);
|
|
||||||
}}
|
|
||||||
autoFocus={!props?.sideBarShowing}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={<SendWhiteIcon />}
|
|
||||||
text={Locale.Chat.Send}
|
|
||||||
className={styles["chat-input-send"] + " no-dark"}
|
|
||||||
onClick={onUserSubmit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSwitchTheme() {
|
function useSwitchTheme() {
|
||||||
const config = useChatStore((state) => state.config);
|
const config = useChatStore((state) => state.config);
|
||||||
|
|
||||||
@ -531,64 +60,6 @@ function useSwitchTheme() {
|
|||||||
}, [config.theme]);
|
}, [config.theme]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportMessages(messages: Message[], topic: string) {
|
|
||||||
const mdText =
|
|
||||||
`# ${topic}\n\n` +
|
|
||||||
messages
|
|
||||||
.map((m) => {
|
|
||||||
return m.role === "user" ? `## ${m.content}` : m.content.trim();
|
|
||||||
})
|
|
||||||
.join("\n\n");
|
|
||||||
const filename = `${topic}.md`;
|
|
||||||
|
|
||||||
showModal({
|
|
||||||
title: Locale.Export.Title,
|
|
||||||
children: (
|
|
||||||
<div className="markdown-body">
|
|
||||||
<pre className={styles["export-content"]}>{mdText}</pre>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
<IconButton
|
|
||||||
key="copy"
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
bordered
|
|
||||||
text={Locale.Export.Copy}
|
|
||||||
onClick={() => copyToClipboard(mdText)}
|
|
||||||
/>,
|
|
||||||
<IconButton
|
|
||||||
key="download"
|
|
||||||
icon={<DownloadIcon />}
|
|
||||||
bordered
|
|
||||||
text={Locale.Export.Download}
|
|
||||||
onClick={() => downloadAs(mdText, filename)}
|
|
||||||
/>,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMemoryPrompt(session: ChatSession) {
|
|
||||||
showModal({
|
|
||||||
title: `${Locale.Memory.Title} (${session.lastSummarizeIndex} of ${session.messages.length})`,
|
|
||||||
children: (
|
|
||||||
<div className="markdown-body">
|
|
||||||
<pre className={styles["export-content"]}>
|
|
||||||
{session.memoryPrompt || Locale.Memory.EmptyContent}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
<IconButton
|
|
||||||
key="copy"
|
|
||||||
icon={<CopyIcon />}
|
|
||||||
bordered
|
|
||||||
text={Locale.Memory.Copy}
|
|
||||||
onClick={() => copyToClipboard(session.memoryPrompt)}
|
|
||||||
/>,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const useHasHydrated = () => {
|
const useHasHydrated = () => {
|
||||||
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
const [hasHydrated, setHasHydrated] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -599,7 +70,7 @@ const useHasHydrated = () => {
|
|||||||
return hasHydrated;
|
return hasHydrated;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Home() {
|
function _Home() {
|
||||||
const [createNewSession, currentIndex, removeSession] = useChatStore(
|
const [createNewSession, currentIndex, removeSession] = useChatStore(
|
||||||
(state) => [
|
(state) => [
|
||||||
state.newSession,
|
state.newSession,
|
||||||
@ -668,11 +139,12 @@ export function Home() {
|
|||||||
setOpenSettings(true);
|
setOpenSettings(true);
|
||||||
setShowSideBar(false);
|
setShowSideBar(false);
|
||||||
}}
|
}}
|
||||||
|
shadow
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className={styles["sidebar-action"]}>
|
{/* <div className={styles["sidebar-action"]}>
|
||||||
<a href={REPO_URL} target="_blank">
|
<a href={REPO_URL} target="_blank">
|
||||||
<IconButton icon={<GithubIcon />} />
|
<IconButton icon={<GithubIcon />} shadow />
|
||||||
</a>
|
</a>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
@ -684,6 +156,7 @@ export function Home() {
|
|||||||
createNewSession();
|
createNewSession();
|
||||||
setShowSideBar(false);
|
setShowSideBar(false);
|
||||||
}}
|
}}
|
||||||
|
shadow
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -708,3 +181,11 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Home() {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<_Home></_Home>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -4,8 +4,8 @@ import RemarkMath from "remark-math";
|
|||||||
import RemarkBreaks from "remark-breaks";
|
import RemarkBreaks from "remark-breaks";
|
||||||
import RehypeKatex from "rehype-katex";
|
import RehypeKatex from "rehype-katex";
|
||||||
import RemarkGfm from "remark-gfm";
|
import RemarkGfm from "remark-gfm";
|
||||||
import RehypePrsim from "rehype-prism-plus";
|
import RehypeHighlight from "rehype-highlight";
|
||||||
import { useRef } from "react";
|
import { useRef, useState, RefObject, useEffect } from "react";
|
||||||
import { copyToClipboard } from "../utils";
|
import { copyToClipboard } from "../utils";
|
||||||
|
|
||||||
export function PreCode(props: { children: any }) {
|
export function PreCode(props: { children: any }) {
|
||||||
@ -27,11 +27,43 @@ export function PreCode(props: { children: any }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useLazyLoad = (ref: RefObject<Element>): boolean => {
|
||||||
|
const [isIntersecting, setIntersecting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIntersecting(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ref.current) {
|
||||||
|
observer.observe(ref.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return isIntersecting;
|
||||||
|
};
|
||||||
|
|
||||||
export function Markdown(props: { content: string }) {
|
export function Markdown(props: { content: string }) {
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||||
rehypePlugins={[RehypeKatex, [RehypePrsim, { ignoreMissing: true }]]}
|
rehypePlugins={[
|
||||||
|
RehypeKatex,
|
||||||
|
[
|
||||||
|
RehypeHighlight,
|
||||||
|
{
|
||||||
|
detect: false,
|
||||||
|
ignoreMissing: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]}
|
||||||
components={{
|
components={{
|
||||||
pre: PreCode,
|
pre: PreCode,
|
||||||
}}
|
}}
|
||||||
|
@ -20,7 +20,7 @@ import {
|
|||||||
useUpdateStore,
|
useUpdateStore,
|
||||||
useAccessStore,
|
useAccessStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
import { Avatar, PromptHints } from "./home";
|
import { Avatar } from "./chat";
|
||||||
|
|
||||||
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
import Locale, { AllLangs, changeLang, getLang } from "../locales";
|
||||||
import { getCurrentVersion } from "../utils";
|
import { getCurrentVersion } from "../utils";
|
||||||
@ -72,7 +72,6 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [usage, setUsage] = useState<{
|
const [usage, setUsage] = useState<{
|
||||||
granted?: number;
|
|
||||||
used?: number;
|
used?: number;
|
||||||
}>();
|
}>();
|
||||||
const [loadingUsage, setLoadingUsage] = useState(false);
|
const [loadingUsage, setLoadingUsage] = useState(false);
|
||||||
@ -81,8 +80,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
requestUsage()
|
requestUsage()
|
||||||
.then((res) =>
|
.then((res) =>
|
||||||
setUsage({
|
setUsage({
|
||||||
granted: res?.total_granted,
|
used: res,
|
||||||
used: res?.total_used,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -285,7 +283,8 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
checked={config.sendPreviewBubble}
|
checked={config.sendPreviewBubble}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateConfig(
|
updateConfig(
|
||||||
(config) => (config.sendPreviewBubble = e.currentTarget.checked),
|
(config) =>
|
||||||
|
(config.sendPreviewBubble = e.currentTarget.checked),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
@ -360,10 +359,7 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
subTitle={
|
subTitle={
|
||||||
loadingUsage
|
loadingUsage
|
||||||
? Locale.Settings.Usage.IsChecking
|
? Locale.Settings.Usage.IsChecking
|
||||||
: Locale.Settings.Usage.SubTitle(
|
: Locale.Settings.Usage.SubTitle(usage?.used ?? "[?]")
|
||||||
usage?.granted ?? "[?]",
|
|
||||||
usage?.used ?? "[?]",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{loadingUsage ? (
|
{loadingUsage ? (
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -24,18 +26,6 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slide-in {
|
|
||||||
from {
|
|
||||||
transform: translateY(10px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-item {
|
.list-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -138,6 +128,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.toast-content {
|
.toast-content {
|
||||||
|
max-width: 80vw;
|
||||||
|
word-break: break-all;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
.window-header {
|
.window-header {
|
||||||
padding: 14px 20px;
|
padding: 14px 20px;
|
||||||
border-bottom: rgba(0, 0, 0, 0.1) 1px solid;
|
border-bottom: rgba(0, 0, 0, 0.1) 1px solid;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -32,4 +33,4 @@
|
|||||||
|
|
||||||
.window-action-button {
|
.window-action-button {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export const OWNER = "Yidadaa";
|
export const OWNER = "Yidadaa";
|
||||||
export const REPO = "ChatGPT-Next-Web";
|
export const REPO = "ChatGPT-Next-Web";
|
||||||
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
|
||||||
|
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
|
||||||
export const UPDATE_URL = `${REPO_URL}#%E4%BF%9D%E6%8C%81%E6%9B%B4%E6%96%B0-keep-updated`;
|
export const UPDATE_URL = `${REPO_URL}#%E4%BF%9D%E6%8C%81%E6%9B%B4%E6%96%B0-keep-updated`;
|
||||||
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
|
||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @next/next/no-page-custom-font */
|
/* eslint-disable @next/next/no-page-custom-font */
|
||||||
import "./styles/globals.scss";
|
import "./styles/globals.scss";
|
||||||
import "./styles/markdown.scss";
|
import "./styles/markdown.scss";
|
||||||
import "./styles/prism.scss";
|
import "./styles/highlight.scss";
|
||||||
import process from "child_process";
|
import process from "child_process";
|
||||||
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ const cn = {
|
|||||||
Download: "下载文件",
|
Download: "下载文件",
|
||||||
},
|
},
|
||||||
Memory: {
|
Memory: {
|
||||||
Title: "上下文记忆 Prompt",
|
Title: "历史记忆",
|
||||||
EmptyContent: "尚未记忆",
|
EmptyContent: "尚未记忆",
|
||||||
Copy: "全部复制",
|
Copy: "全部复制",
|
||||||
},
|
},
|
||||||
@ -58,6 +58,7 @@ const cn = {
|
|||||||
en: "English",
|
en: "English",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
es: "Español",
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "头像",
|
Avatar: "头像",
|
||||||
@ -103,8 +104,8 @@ const cn = {
|
|||||||
},
|
},
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "账户余额",
|
Title: "账户余额",
|
||||||
SubTitle(granted: any, used: any) {
|
SubTitle(used: any) {
|
||||||
return `总共 $${granted},已使用 $${used}`;
|
return `本月已使用 $${used}`;
|
||||||
},
|
},
|
||||||
IsChecking: "正在检查…",
|
IsChecking: "正在检查…",
|
||||||
Check: "重新检查",
|
Check: "重新检查",
|
||||||
@ -138,7 +139,7 @@ const cn = {
|
|||||||
Topic:
|
Topic:
|
||||||
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
|
"使用四到五个字直接返回这句话的简要主题,不要解释、不要标点、不要语气词、不要多余文本,如果没有主题,请直接返回“闲聊”",
|
||||||
Summarize:
|
Summarize:
|
||||||
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内",
|
"简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 200 字以内",
|
||||||
},
|
},
|
||||||
ConfirmClearAll: "确认清除所有聊天、设置数据?",
|
ConfirmClearAll: "确认清除所有聊天、设置数据?",
|
||||||
},
|
},
|
||||||
@ -146,6 +147,11 @@ const cn = {
|
|||||||
Success: "已写入剪切板",
|
Success: "已写入剪切板",
|
||||||
Failed: "复制失败,请赋予剪切板权限",
|
Failed: "复制失败,请赋予剪切板权限",
|
||||||
},
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `已设置 ${x} 条前置上下文`,
|
||||||
|
Edit: "前置上下文和历史记忆",
|
||||||
|
Add: "新增一条",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocaleType = typeof cn;
|
export type LocaleType = typeof cn;
|
||||||
|
@ -54,12 +54,13 @@ const en: LocaleType = {
|
|||||||
Close: "Close",
|
Close: "Close",
|
||||||
},
|
},
|
||||||
Lang: {
|
Lang: {
|
||||||
Name: "Language",
|
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||||
Options: {
|
Options: {
|
||||||
cn: "简体中文",
|
cn: "简体中文",
|
||||||
en: "English",
|
en: "English",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
es: "Español",
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
@ -105,8 +106,8 @@ const en: LocaleType = {
|
|||||||
},
|
},
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "Account Balance",
|
Title: "Account Balance",
|
||||||
SubTitle(granted: any, used: any) {
|
SubTitle(used: any) {
|
||||||
return `Total $${granted}, Used $${used}`;
|
return `Used this month $${used}`;
|
||||||
},
|
},
|
||||||
IsChecking: "Checking...",
|
IsChecking: "Checking...",
|
||||||
Check: "Check Again",
|
Check: "Check Again",
|
||||||
@ -142,7 +143,7 @@ const en: LocaleType = {
|
|||||||
Topic:
|
Topic:
|
||||||
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.",
|
"Please generate a four to five word title summarizing our conversation without any lead-in, punctuation, quotation marks, periods, symbols, or additional text. Remove enclosing quotation marks.",
|
||||||
Summarize:
|
Summarize:
|
||||||
"Summarize our discussion briefly in 50 characters or less to use as a prompt for future context.",
|
"Summarize our discussion briefly in 200 words or less to use as a prompt for future context.",
|
||||||
},
|
},
|
||||||
ConfirmClearAll: "Confirm to clear all chat and setting data?",
|
ConfirmClearAll: "Confirm to clear all chat and setting data?",
|
||||||
},
|
},
|
||||||
@ -150,6 +151,11 @@ const en: LocaleType = {
|
|||||||
Success: "Copied to clipboard",
|
Success: "Copied to clipboard",
|
||||||
Failed: "Copy failed, please grant permission to access clipboard",
|
Failed: "Copy failed, please grant permission to access clipboard",
|
||||||
},
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `With ${x} contextual prompts`,
|
||||||
|
Edit: "Contextual and Memory Prompts",
|
||||||
|
Add: "Add One",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
@ -60,6 +60,7 @@ const es: LocaleType = {
|
|||||||
en: "Inglés",
|
en: "Inglés",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
es: "Español",
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "Avatar",
|
Avatar: "Avatar",
|
||||||
@ -78,7 +79,7 @@ const es: LocaleType = {
|
|||||||
SendKey: "Tecla de envío",
|
SendKey: "Tecla de envío",
|
||||||
Theme: "Tema",
|
Theme: "Tema",
|
||||||
TightBorder: "Borde ajustado",
|
TightBorder: "Borde ajustado",
|
||||||
SendPreviewBubble: "Send preview bubble",
|
SendPreviewBubble: "Enviar burbuja de vista previa",
|
||||||
Prompt: {
|
Prompt: {
|
||||||
Disable: {
|
Disable: {
|
||||||
Title: "Desactivar autocompletado",
|
Title: "Desactivar autocompletado",
|
||||||
@ -105,8 +106,8 @@ const es: LocaleType = {
|
|||||||
},
|
},
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "Saldo de la cuenta",
|
Title: "Saldo de la cuenta",
|
||||||
SubTitle(granted: any, used: any) {
|
SubTitle(used: any) {
|
||||||
return `Total $${granted}, Usado $${used}`;
|
return `Usado $${used}`;
|
||||||
},
|
},
|
||||||
IsChecking: "Comprobando...",
|
IsChecking: "Comprobando...",
|
||||||
Check: "Comprobar de nuevo",
|
Check: "Comprobar de nuevo",
|
||||||
@ -142,7 +143,7 @@ const es: LocaleType = {
|
|||||||
Topic:
|
Topic:
|
||||||
"Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.",
|
"Por favor, genera un título de cuatro a cinco palabras que resuma nuestra conversación sin ningún inicio, puntuación, comillas, puntos, símbolos o texto adicional. Elimina las comillas que lo envuelven.",
|
||||||
Summarize:
|
Summarize:
|
||||||
"Resuma nuestra discusión brevemente en 50 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
|
"Resuma nuestra discusión brevemente en 200 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
|
||||||
},
|
},
|
||||||
ConfirmClearAll:
|
ConfirmClearAll:
|
||||||
"¿Confirmar para borrar todos los datos de chat y configuración?",
|
"¿Confirmar para borrar todos los datos de chat y configuración?",
|
||||||
@ -152,6 +153,11 @@ const es: LocaleType = {
|
|||||||
Failed:
|
Failed:
|
||||||
"La copia falló, por favor concede permiso para acceder al portapapeles",
|
"La copia falló, por favor concede permiso para acceder al portapapeles",
|
||||||
},
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `With ${x} contextual prompts`,
|
||||||
|
Edit: "Contextual and Memory Prompts",
|
||||||
|
Add: "Add One",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default es;
|
export default es;
|
||||||
|
@ -2,10 +2,11 @@ import CN from "./cn";
|
|||||||
import EN from "./en";
|
import EN from "./en";
|
||||||
import TW from "./tw";
|
import TW from "./tw";
|
||||||
import ES from "./es";
|
import ES from "./es";
|
||||||
|
import IT from "./it";
|
||||||
|
|
||||||
export type { LocaleType } from "./cn";
|
export type { LocaleType } from "./cn";
|
||||||
|
|
||||||
export const AllLangs = ["en", "cn", "tw", "es"] as const;
|
export const AllLangs = ["en", "cn", "tw", "es", "it"] as const;
|
||||||
type Lang = (typeof AllLangs)[number];
|
type Lang = (typeof AllLangs)[number];
|
||||||
|
|
||||||
const LANG_KEY = "lang";
|
const LANG_KEY = "lang";
|
||||||
@ -47,6 +48,8 @@ export function getLang(): Lang {
|
|||||||
return "tw";
|
return "tw";
|
||||||
} else if (lang.includes("es")) {
|
} else if (lang.includes("es")) {
|
||||||
return "es";
|
return "es";
|
||||||
|
} else if (lang.includes("it")) {
|
||||||
|
return "it";
|
||||||
} else {
|
} else {
|
||||||
return "en";
|
return "en";
|
||||||
}
|
}
|
||||||
@ -57,4 +60,4 @@ export function changeLang(lang: Lang) {
|
|||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default { en: EN, cn: CN, tw: TW, es: ES }[getLang()];
|
export default { en: EN, cn: CN, tw: TW, es: ES, it: IT }[getLang()];
|
||||||
|
164
app/locales/it.ts
Normal file
164
app/locales/it.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import { SubmitKey } from "../store/app";
|
||||||
|
import type { LocaleType } from "./index";
|
||||||
|
|
||||||
|
const it: LocaleType = {
|
||||||
|
WIP: "Work in progress...",
|
||||||
|
Error: {
|
||||||
|
Unauthorized:
|
||||||
|
"Accesso non autorizzato, inserire il codice di accesso nella pagina delle impostazioni.",
|
||||||
|
},
|
||||||
|
ChatItem: {
|
||||||
|
ChatItemCount: (count: number) => `${count} messaggi`,
|
||||||
|
},
|
||||||
|
Chat: {
|
||||||
|
SubTitle: (count: number) => `${count} messaggi con ChatGPT`,
|
||||||
|
Actions: {
|
||||||
|
ChatList: "Vai alla Chat List",
|
||||||
|
CompressedHistory: "Prompt di memoria della cronologia compressa",
|
||||||
|
Export: "Esportazione di tutti i messaggi come Markdown",
|
||||||
|
Copy: "Copia",
|
||||||
|
Stop: "Stop",
|
||||||
|
Retry: "Riprova",
|
||||||
|
},
|
||||||
|
Rename: "Rinomina Chat",
|
||||||
|
Typing: "Typing…",
|
||||||
|
Input: (submitKey: string) => {
|
||||||
|
var inputHints = `Scrivi qualcosa e premi ${submitKey} per inviare`;
|
||||||
|
if (submitKey === String(SubmitKey.Enter)) {
|
||||||
|
inputHints += ", premi Shift + Enter per andare a capo";
|
||||||
|
}
|
||||||
|
return inputHints;
|
||||||
|
},
|
||||||
|
Send: "Invia",
|
||||||
|
},
|
||||||
|
Export: {
|
||||||
|
Title: "Tutti i messaggi",
|
||||||
|
Copy: "Copia tutto",
|
||||||
|
Download: "Scarica",
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
Title: "Prompt di memoria",
|
||||||
|
EmptyContent: "Vuoto.",
|
||||||
|
Copy: "Copia tutto",
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
NewChat: "Nuova Chat",
|
||||||
|
DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
|
||||||
|
},
|
||||||
|
Settings: {
|
||||||
|
Title: "Impostazioni",
|
||||||
|
SubTitle: "Tutte le impostazioni",
|
||||||
|
Actions: {
|
||||||
|
ClearAll: "Cancella tutti i dati",
|
||||||
|
ResetAll: "Resetta tutte le impostazioni",
|
||||||
|
Close: "Chiudi",
|
||||||
|
},
|
||||||
|
Lang: {
|
||||||
|
Name: "Lingue",
|
||||||
|
Options: {
|
||||||
|
cn: "简体中文",
|
||||||
|
en: "English",
|
||||||
|
tw: "繁體中文",
|
||||||
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Avatar: "Avatar",
|
||||||
|
FontSize: {
|
||||||
|
Title: "Dimensione carattere",
|
||||||
|
SubTitle: "Regolare la dimensione dei caratteri del contenuto della chat",
|
||||||
|
},
|
||||||
|
Update: {
|
||||||
|
Version: (x: string) => `Versione: ${x}`,
|
||||||
|
IsLatest: "Ultima versione",
|
||||||
|
CheckUpdate: "Controlla aggiornamenti",
|
||||||
|
IsChecking: "Sto controllando gli aggiornamenti...",
|
||||||
|
FoundUpdate: (x: string) => `Trovata nuova versione: ${x}`,
|
||||||
|
GoToUpdate: "Aggiorna",
|
||||||
|
},
|
||||||
|
SendKey: "Tasto invia",
|
||||||
|
Theme: "tema",
|
||||||
|
TightBorder: "Bordi stretti",
|
||||||
|
SendPreviewBubble: "Invia l'anteprima della bolla",
|
||||||
|
Prompt: {
|
||||||
|
Disable: {
|
||||||
|
Title: "Disabilita l'auto completamento",
|
||||||
|
SubTitle: "Input / per attivare il completamento automatico",
|
||||||
|
},
|
||||||
|
List: "Elenco dei suggerimenti",
|
||||||
|
ListCount: (builtin: number, custom: number) =>
|
||||||
|
`${builtin} built-in, ${custom} user-defined`,
|
||||||
|
Edit: "Modifica",
|
||||||
|
},
|
||||||
|
HistoryCount: {
|
||||||
|
Title: "Conteggio dei messaggi allegati",
|
||||||
|
SubTitle: "Numero di messaggi inviati allegati per richiesta",
|
||||||
|
},
|
||||||
|
CompressThreshold: {
|
||||||
|
Title: "Soglia di compressione della cronologia",
|
||||||
|
SubTitle:
|
||||||
|
"Comprimerà se la lunghezza dei messaggi non compressi supera il valore",
|
||||||
|
},
|
||||||
|
Token: {
|
||||||
|
Title: "Chiave API",
|
||||||
|
SubTitle:
|
||||||
|
"Utilizzare la chiave per ignorare il limite del codice di accesso",
|
||||||
|
Placeholder: "OpenAI API Key",
|
||||||
|
},
|
||||||
|
Usage: {
|
||||||
|
Title: "Bilancio Account",
|
||||||
|
SubTitle(used: any) {
|
||||||
|
return `Usato in questo mese $${used}`;
|
||||||
|
},
|
||||||
|
IsChecking: "Controllando...",
|
||||||
|
Check: "Controlla ancora",
|
||||||
|
},
|
||||||
|
AccessCode: {
|
||||||
|
Title: "Codice d'accesso",
|
||||||
|
SubTitle: "Controllo d'accesso abilitato",
|
||||||
|
Placeholder: "Inserisci il codice d'accesso",
|
||||||
|
},
|
||||||
|
Model: "Modello GPT",
|
||||||
|
Temperature: {
|
||||||
|
Title: "Temperature",
|
||||||
|
SubTitle: "Un valore maggiore rende l'output più casuale",
|
||||||
|
},
|
||||||
|
MaxTokens: {
|
||||||
|
Title: "Token massimi",
|
||||||
|
SubTitle: "Lunghezza massima dei token in ingresso e dei token generati",
|
||||||
|
},
|
||||||
|
PresencePenlty: {
|
||||||
|
Title: "Penalità di presenza",
|
||||||
|
SubTitle:
|
||||||
|
"Un valore maggiore aumenta la probabilità di parlare di nuovi argomenti",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Store: {
|
||||||
|
DefaultTopic: "Nuova conversazione",
|
||||||
|
BotHello: "Ciao, come posso aiutarti oggi?",
|
||||||
|
Error: "Qualcosa è andato storto, riprova più tardi.",
|
||||||
|
Prompt: {
|
||||||
|
History: (content: string) =>
|
||||||
|
"Questo è un riassunto della cronologia delle chat tra l'IA e l'utente:" +
|
||||||
|
content,
|
||||||
|
Topic:
|
||||||
|
"Si prega di generare un titolo di quattro o cinque parole che riassuma la nostra conversazione senza alcuna traccia, punteggiatura, virgolette, punti, simboli o testo aggiuntivo. Rimuovere le virgolette",
|
||||||
|
Summarize:
|
||||||
|
"Riassumi brevemente la nostra discussione in 200 caratteri o meno per usarla come spunto per una futura conversazione.",
|
||||||
|
},
|
||||||
|
ConfirmClearAll:
|
||||||
|
"Confermi la cancellazione di tutti i dati della chat e delle impostazioni?",
|
||||||
|
},
|
||||||
|
Copy: {
|
||||||
|
Success: "Copiato sugli appunti",
|
||||||
|
Failed:
|
||||||
|
"Copia fallita, concedere l'autorizzazione all'accesso agli appunti",
|
||||||
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `Con ${x} prompts contestuali`,
|
||||||
|
Edit: "Prompt contestuali e di memoria",
|
||||||
|
Add: "Aggiungi altro",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default it;
|
@ -59,6 +59,7 @@ const tw: LocaleType = {
|
|||||||
en: "English",
|
en: "English",
|
||||||
tw: "繁體中文",
|
tw: "繁體中文",
|
||||||
es: "Español",
|
es: "Español",
|
||||||
|
it: "Italiano",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Avatar: "大頭貼",
|
Avatar: "大頭貼",
|
||||||
@ -103,8 +104,8 @@ const tw: LocaleType = {
|
|||||||
},
|
},
|
||||||
Usage: {
|
Usage: {
|
||||||
Title: "帳戶餘額",
|
Title: "帳戶餘額",
|
||||||
SubTitle(granted: any, used: any) {
|
SubTitle(used: any) {
|
||||||
return `總共 $${granted},已使用 $${used}`;
|
return `本月已使用 $${used}`;
|
||||||
},
|
},
|
||||||
IsChecking: "正在檢查…",
|
IsChecking: "正在檢查…",
|
||||||
Check: "重新檢查",
|
Check: "重新檢查",
|
||||||
@ -137,7 +138,7 @@ const tw: LocaleType = {
|
|||||||
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
"這是 AI 與用戶的歷史聊天總結,作為前情提要:" + content,
|
||||||
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
|
Topic: "直接返回這句話的簡要主題,無須解釋,若無主題,請直接返回「閒聊」",
|
||||||
Summarize:
|
Summarize:
|
||||||
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 50 字以內",
|
"簡要總結一下你和用戶的對話,作為後續的上下文提示 prompt,且字數控制在 200 字以內",
|
||||||
},
|
},
|
||||||
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
ConfirmClearAll: "確認清除所有對話、設定數據?",
|
||||||
},
|
},
|
||||||
@ -145,6 +146,11 @@ const tw: LocaleType = {
|
|||||||
Success: "已複製到剪貼簿中",
|
Success: "已複製到剪貼簿中",
|
||||||
Failed: "複製失敗,請賦予剪貼簿權限",
|
Failed: "複製失敗,請賦予剪貼簿權限",
|
||||||
},
|
},
|
||||||
|
Context: {
|
||||||
|
Toast: (x: any) => `已設置 ${x} 條前置上下文`,
|
||||||
|
Edit: "前置上下文和歷史記憶",
|
||||||
|
Add: "新增壹條",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default tw;
|
export default tw;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
import { Home } from "./components/home";
|
import { Home } from "./components/home";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
27
app/polyfill.ts
Normal file
27
app/polyfill.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
declare global {
|
||||||
|
interface Array<T> {
|
||||||
|
at(index: number): T | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.prototype.at) {
|
||||||
|
Array.prototype.at = function (index: number) {
|
||||||
|
// Get the length of the array
|
||||||
|
const length = this.length;
|
||||||
|
|
||||||
|
// Convert negative index to a positive index
|
||||||
|
if (index < 0) {
|
||||||
|
index = length + index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return undefined if the index is out of range
|
||||||
|
if (index < 0 || index >= length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Array.prototype.slice method to get value at the specified index
|
||||||
|
return Array.prototype.slice.call(this, index, index + 1)[0];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
@ -1,10 +1,7 @@
|
|||||||
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
import type { ChatRequest, ChatReponse } from "./api/openai/typing";
|
||||||
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
import { filterConfig, Message, ModelConfig, useAccessStore } from "./store";
|
||||||
import Locale from "./locales";
|
import Locale from "./locales";
|
||||||
|
import { showToast } from "./components/ui-lib";
|
||||||
if (!Array.prototype.at) {
|
|
||||||
require("array.prototype.at/auto");
|
|
||||||
}
|
|
||||||
|
|
||||||
const TIME_OUT_MS = 30000;
|
const TIME_OUT_MS = 30000;
|
||||||
|
|
||||||
@ -52,6 +49,7 @@ export function requestOpenaiClient(path: string) {
|
|||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
path,
|
path,
|
||||||
...getHeaders(),
|
...getHeaders(),
|
||||||
},
|
},
|
||||||
@ -73,17 +71,38 @@ export async function requestChat(messages: Message[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function requestUsage() {
|
export async function requestUsage() {
|
||||||
|
const formatDate = (d: Date) =>
|
||||||
|
`${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, "0")}-${d
|
||||||
|
.getDate()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
const now = new Date(Date.now() + ONE_DAY);
|
||||||
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const startDate = formatDate(startOfMonth);
|
||||||
|
const endDate = formatDate(now);
|
||||||
const res = await requestOpenaiClient(
|
const res = await requestOpenaiClient(
|
||||||
"dashboard/billing/credit_grants?_vercel_no_cache=1",
|
`dashboard/billing/usage?start_date=${startDate}&end_date=${endDate}`,
|
||||||
)(null, "GET");
|
)(null, "GET");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = (await res.json()) as {
|
const response = (await res.json()) as {
|
||||||
total_available: number;
|
total_usage: number;
|
||||||
total_granted: number;
|
error?: {
|
||||||
total_used: number;
|
type: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
return response;
|
|
||||||
|
if (response.error && response.error.type) {
|
||||||
|
showToast(response.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.total_usage) {
|
||||||
|
response.total_usage = Math.round(response.total_usage) / 100;
|
||||||
|
}
|
||||||
|
return response.total_usage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Request usage] ", error, res.body);
|
console.error("[Request usage] ", error, res.body);
|
||||||
}
|
}
|
||||||
@ -95,7 +114,7 @@ export async function requestChatStream(
|
|||||||
filterBot?: boolean;
|
filterBot?: boolean;
|
||||||
modelConfig?: ModelConfig;
|
modelConfig?: ModelConfig;
|
||||||
onMessage: (message: string, done: boolean) => void;
|
onMessage: (message: string, done: boolean) => void;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error, statusCode?: number) => void;
|
||||||
onController?: (controller: AbortController) => void;
|
onController?: (controller: AbortController) => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@ -159,11 +178,10 @@ export async function requestChatStream(
|
|||||||
finish();
|
finish();
|
||||||
} else if (res.status === 401) {
|
} else if (res.status === 401) {
|
||||||
console.error("Anauthorized");
|
console.error("Anauthorized");
|
||||||
responseText = Locale.Error.Unauthorized;
|
options?.onError(new Error("Anauthorized"), res.status);
|
||||||
finish();
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Stream Error", res.body);
|
console.error("Stream Error", res.body);
|
||||||
options?.onError(new Error("Stream Error"));
|
options?.onError(new Error("Stream Error"), res.status);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("NetWork Error", err);
|
console.error("NetWork Error", err);
|
||||||
|
@ -11,13 +11,10 @@ import { trimTopic } from "../utils";
|
|||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
if (!Array.prototype.at) {
|
|
||||||
require("array.prototype.at/auto");
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Message = ChatCompletionResponseMessage & {
|
export type Message = ChatCompletionResponseMessage & {
|
||||||
date: string;
|
date: string;
|
||||||
streaming?: boolean;
|
streaming?: boolean;
|
||||||
|
isError?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum SubmitKey {
|
export enum SubmitKey {
|
||||||
@ -57,6 +54,8 @@ export interface ChatConfig {
|
|||||||
|
|
||||||
export type ModelConfig = ChatConfig["modelConfig"];
|
export type ModelConfig = ChatConfig["modelConfig"];
|
||||||
|
|
||||||
|
export const ROLES: Message["role"][] = ["system", "user", "assistant"];
|
||||||
|
|
||||||
const ENABLE_GPT4 = true;
|
const ENABLE_GPT4 = true;
|
||||||
|
|
||||||
export const ALL_MODELS = [
|
export const ALL_MODELS = [
|
||||||
@ -104,7 +103,7 @@ export function filterConfig(oldConfig: ModelConfig): Partial<ModelConfig> {
|
|||||||
return isValidModel(x as string);
|
return isValidModel(x as string);
|
||||||
},
|
},
|
||||||
max_tokens(x) {
|
max_tokens(x) {
|
||||||
return isValidNumber(x as number, 100, 4000);
|
return isValidNumber(x as number, 100, 32000);
|
||||||
},
|
},
|
||||||
presence_penalty(x) {
|
presence_penalty(x) {
|
||||||
return isValidNumber(x as number, -2, 2);
|
return isValidNumber(x as number, -2, 2);
|
||||||
@ -155,6 +154,7 @@ export interface ChatSession {
|
|||||||
id: number;
|
id: number;
|
||||||
topic: string;
|
topic: string;
|
||||||
memoryPrompt: string;
|
memoryPrompt: string;
|
||||||
|
context: Message[];
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
stat: ChatStat;
|
stat: ChatStat;
|
||||||
lastUpdate: string;
|
lastUpdate: string;
|
||||||
@ -162,6 +162,11 @@ export interface ChatSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
|
const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
|
||||||
|
export const BOT_HELLO: Message = {
|
||||||
|
role: "assistant",
|
||||||
|
content: Locale.Store.BotHello,
|
||||||
|
date: "",
|
||||||
|
};
|
||||||
|
|
||||||
function createEmptySession(): ChatSession {
|
function createEmptySession(): ChatSession {
|
||||||
const createDate = new Date().toLocaleString();
|
const createDate = new Date().toLocaleString();
|
||||||
@ -170,13 +175,8 @@ function createEmptySession(): ChatSession {
|
|||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
topic: DEFAULT_TOPIC,
|
topic: DEFAULT_TOPIC,
|
||||||
memoryPrompt: "",
|
memoryPrompt: "",
|
||||||
messages: [
|
context: [],
|
||||||
{
|
messages: [],
|
||||||
role: "assistant",
|
|
||||||
content: Locale.Store.BotHello,
|
|
||||||
date: createDate,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
stat: {
|
stat: {
|
||||||
tokenCount: 0,
|
tokenCount: 0,
|
||||||
wordCount: 0,
|
wordCount: 0,
|
||||||
@ -352,9 +352,15 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
set(() => ({}));
|
set(() => ({}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error, statusCode) {
|
||||||
botMessage.content += "\n\n" + Locale.Store.Error;
|
if (statusCode === 401) {
|
||||||
|
botMessage.content = Locale.Error.Unauthorized;
|
||||||
|
} else {
|
||||||
|
botMessage.content += "\n\n" + Locale.Store.Error;
|
||||||
|
}
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
|
userMessage.isError = true;
|
||||||
|
botMessage.isError = true;
|
||||||
set(() => ({}));
|
set(() => ({}));
|
||||||
ControllerPool.remove(sessionIndex, messageIndex);
|
ControllerPool.remove(sessionIndex, messageIndex);
|
||||||
},
|
},
|
||||||
@ -384,17 +390,20 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
getMessagesWithMemory() {
|
getMessagesWithMemory() {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const config = get().config;
|
const config = get().config;
|
||||||
const n = session.messages.length;
|
const messages = session.messages.filter((msg) => !msg.isError);
|
||||||
const recentMessages = session.messages.slice(
|
const n = messages.length;
|
||||||
Math.max(0, n - config.historyMessageCount),
|
|
||||||
);
|
|
||||||
|
|
||||||
const memoryPrompt = get().getMemoryPrompt();
|
const context = session.context.slice();
|
||||||
|
|
||||||
if (session.memoryPrompt) {
|
if (session.memoryPrompt && session.memoryPrompt.length > 0) {
|
||||||
recentMessages.unshift(memoryPrompt);
|
const memoryPrompt = get().getMemoryPrompt();
|
||||||
|
context.push(memoryPrompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const recentMessages = context.concat(
|
||||||
|
messages.slice(Math.max(0, n - config.historyMessageCount)),
|
||||||
|
);
|
||||||
|
|
||||||
return recentMessages;
|
return recentMessages;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -432,11 +441,13 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
let toBeSummarizedMsgs = session.messages.slice(
|
let toBeSummarizedMsgs = session.messages.slice(
|
||||||
session.lastSummarizeIndex,
|
session.lastSummarizeIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||||
|
|
||||||
if (historyMsgLength > 4000) {
|
if (historyMsgLength > get().config?.modelConfig?.max_tokens ?? 4000) {
|
||||||
|
const n = toBeSummarizedMsgs.length;
|
||||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||||
-config.historyMessageCount,
|
Math.max(0, n - config.historyMessageCount),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,7 +510,16 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: LOCAL_KEY,
|
name: LOCAL_KEY,
|
||||||
version: 1,
|
version: 1.1,
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
const state = persistedState as ChatStore;
|
||||||
|
|
||||||
|
if (version === 1) {
|
||||||
|
state.sessions.forEach((s) => (s.context = []));
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
23
app/styles/animation.scss
Normal file
23
app/styles/animation.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
@keyframes slide-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-in-from-top {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
}
|
@ -117,7 +117,7 @@ body {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
padding: 8px 10px;
|
padding: 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -188,7 +188,7 @@ input[type="text"] {
|
|||||||
appearance: none;
|
appearance: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: var(--border-in-light);
|
border: var(--border-in-light);
|
||||||
height: 32px;
|
min-height: 36px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
@ -235,6 +235,7 @@ pre {
|
|||||||
.copy-code-button {
|
.copy-code-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
|
top: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0px 5px;
|
padding: 0px 5px;
|
||||||
background-color: var(--black);
|
background-color: var(--black);
|
||||||
@ -255,3 +256,30 @@ pre {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
div:not(.no-dark) > svg {
|
||||||
|
filter: invert(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
width: 80%;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
115
app/styles/highlight.scss
Normal file
115
app/styles/highlight.scss
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
.markdown-body {
|
||||||
|
pre {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code {
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs,
|
||||||
|
pre {
|
||||||
|
background: #1a1b26;
|
||||||
|
color: #cbd2ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
Theme: Tokyo-night-Dark
|
||||||
|
origin: https://github.com/enkia/tokyo-night-vscode-theme
|
||||||
|
Description: Original highlight.js style
|
||||||
|
Author: (c) Henri Vandersleyen <hvandersleyen@gmail.com>
|
||||||
|
License: see project LICENSE
|
||||||
|
Touched: 2022
|
||||||
|
*/
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-meta {
|
||||||
|
color: #565f89;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-template-tag,
|
||||||
|
.hljs-variable.language_ {
|
||||||
|
color: #f7768e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-params,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #ff9e64;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-built_in {
|
||||||
|
color: #e0af68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-property,
|
||||||
|
.hljs-subst,
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-title.class_,
|
||||||
|
.hljs-title.class_.inherited__,
|
||||||
|
.hljs-title.function_ {
|
||||||
|
color: #7dcfff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #73daca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-symbol {
|
||||||
|
color: #9ece6a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-code,
|
||||||
|
.hljs-formula,
|
||||||
|
.hljs-section {
|
||||||
|
color: #7aa2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-char.escape_,
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-operator {
|
||||||
|
color: #bb9af7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-punctuation {
|
||||||
|
color: #c0caf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
@ -1,122 +0,0 @@
|
|||||||
.markdown-body {
|
|
||||||
pre {
|
|
||||||
background: #282a36;
|
|
||||||
color: #f8f8f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
color: #f8f8f2;
|
|
||||||
background: none;
|
|
||||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
|
||||||
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
tab-size: 4;
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
pre[class*="language-"] {
|
|
||||||
padding: 1em;
|
|
||||||
margin: 0.5em 0;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre) > code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
background: #282a36;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
:not(pre) > code[class*="language-"] {
|
|
||||||
padding: 0.1em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.comment,
|
|
||||||
.token.prolog,
|
|
||||||
.token.doctype,
|
|
||||||
.token.cdata {
|
|
||||||
color: #6272a4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.punctuation {
|
|
||||||
color: #f8f8f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.namespace {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.property,
|
|
||||||
.token.tag,
|
|
||||||
.token.constant,
|
|
||||||
.token.symbol,
|
|
||||||
.token.deleted {
|
|
||||||
color: #ff79c6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.boolean,
|
|
||||||
.token.number {
|
|
||||||
color: #bd93f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.selector,
|
|
||||||
.token.attr-name,
|
|
||||||
.token.string,
|
|
||||||
.token.char,
|
|
||||||
.token.builtin,
|
|
||||||
.token.inserted {
|
|
||||||
color: #50fa7b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.operator,
|
|
||||||
.token.entity,
|
|
||||||
.token.url,
|
|
||||||
.language-css .token.string,
|
|
||||||
.style .token.string,
|
|
||||||
.token.variable {
|
|
||||||
color: #f8f8f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.atrule,
|
|
||||||
.token.attr-value,
|
|
||||||
.token.function,
|
|
||||||
.token.class-name {
|
|
||||||
color: #f1fa8c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.keyword {
|
|
||||||
color: #8be9fd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.regex,
|
|
||||||
.token.important {
|
|
||||||
color: #ffb86c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.important,
|
|
||||||
.token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
}
|
|
22
app/utils.ts
22
app/utils.ts
@ -5,15 +5,19 @@ export function trimTopic(topic: string) {
|
|||||||
return topic.replace(/[,。!?、,.!?]*$/, "");
|
return topic.replace(/[,。!?、,.!?]*$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyToClipboard(text: string) {
|
export async function copyToClipboard(text: string) {
|
||||||
navigator.clipboard
|
try {
|
||||||
.writeText(text)
|
await navigator.clipboard.writeText(text);
|
||||||
.then((res) => {
|
} catch (error) {
|
||||||
showToast(Locale.Copy.Success);
|
const textarea = document.createElement("textarea");
|
||||||
})
|
textarea.value = text;
|
||||||
.catch((err) => {
|
document.body.appendChild(textarea);
|
||||||
showToast(Locale.Copy.Failed);
|
textarea.select();
|
||||||
});
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
} finally {
|
||||||
|
showToast(Locale.Copy.Success);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadAs(text: string, filename: string) {
|
export function downloadAs(text: string, filename: string) {
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-markdown": "^8.0.5",
|
"react-markdown": "^8.0.5",
|
||||||
|
"rehype-highlight": "^6.0.0",
|
||||||
"rehype-katex": "^6.0.2",
|
"rehype-katex": "^6.0.2",
|
||||||
"rehype-prism-plus": "^1.5.1",
|
|
||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
@ -39,7 +39,6 @@
|
|||||||
"@types/react-dom": "^18.0.11",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-katex": "^3.0.0",
|
"@types/react-katex": "^3.0.0",
|
||||||
"@types/spark-md5": "^3.0.2",
|
"@types/spark-md5": "^3.0.2",
|
||||||
"array.prototype.at": "^1.1.1",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.36.0",
|
"eslint": "^8.36.0",
|
||||||
"eslint-config-next": "13.2.3",
|
"eslint-config-next": "13.2.3",
|
||||||
|
@ -11,3 +11,5 @@ self.addEventListener("install", function (event) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (e) => {});
|
||||||
|
146
yarn.lock
146
yarn.lock
@ -1365,11 +1365,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
"@types/prismjs@^1.0.0":
|
|
||||||
version "1.26.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654"
|
|
||||||
integrity sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==
|
|
||||||
|
|
||||||
"@types/prop-types@*", "@types/prop-types@^15.0.0":
|
"@types/prop-types@*", "@types/prop-types@^15.0.0":
|
||||||
version "15.7.5"
|
version "15.7.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||||
@ -1570,16 +1565,6 @@ array-union@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||||
|
|
||||||
array.prototype.at@^1.1.1:
|
|
||||||
version "1.1.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/array.prototype.at/-/array.prototype.at-1.1.1.tgz#6deda3cd3c704afa16361387ea344e0b8d8831b5"
|
|
||||||
integrity sha512-n/wYNLJy/fVEU9EGPt2ww920hy1XX3XB2yTREFy1QsxctBgQV/tZIwg1G8jVxELna4pLCzg/xvvS/DDXtI4NNg==
|
|
||||||
dependencies:
|
|
||||||
call-bind "^1.0.2"
|
|
||||||
define-properties "^1.1.4"
|
|
||||||
es-abstract "^1.20.4"
|
|
||||||
es-shim-unscopables "^1.0.0"
|
|
||||||
|
|
||||||
array.prototype.flat@^1.3.1:
|
array.prototype.flat@^1.3.1:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2"
|
resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2"
|
||||||
@ -1769,21 +1754,11 @@ chalk@^4.0.0:
|
|||||||
ansi-styles "^4.1.0"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.1.0"
|
supports-color "^7.1.0"
|
||||||
|
|
||||||
character-entities-legacy@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b"
|
|
||||||
integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
|
|
||||||
|
|
||||||
character-entities@^2.0.0:
|
character-entities@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
|
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
|
||||||
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
|
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
|
||||||
|
|
||||||
character-reference-invalid@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9"
|
|
||||||
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
|
|
||||||
|
|
||||||
"chokidar@>=3.0.0 <4.0.0":
|
"chokidar@>=3.0.0 <4.0.0":
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||||
@ -2548,6 +2523,13 @@ fastq@^1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
reusify "^1.0.4"
|
||||||
|
|
||||||
|
fault@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c"
|
||||||
|
integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==
|
||||||
|
dependencies:
|
||||||
|
format "^0.2.0"
|
||||||
|
|
||||||
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
|
||||||
@ -2612,6 +2594,11 @@ form-data@^4.0.0:
|
|||||||
combined-stream "^1.0.8"
|
combined-stream "^1.0.8"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
format@^0.2.0:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
|
||||||
|
integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==
|
||||||
|
|
||||||
formdata-polyfill@^4.0.10:
|
formdata-polyfill@^4.0.10:
|
||||||
version "4.0.10"
|
version "4.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||||
@ -2867,14 +2854,7 @@ hast-util-parse-selector@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/hast" "^2.0.0"
|
"@types/hast" "^2.0.0"
|
||||||
|
|
||||||
hast-util-to-string@^2.0.0:
|
hast-util-to-text@^3.0.0, hast-util-to-text@^3.1.0:
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a"
|
|
||||||
integrity sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==
|
|
||||||
dependencies:
|
|
||||||
"@types/hast" "^2.0.0"
|
|
||||||
|
|
||||||
hast-util-to-text@^3.1.0:
|
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz#ecf30c47141f41e91a5d32d0b1e1859fd2ac04f2"
|
resolved "https://registry.yarnpkg.com/hast-util-to-text/-/hast-util-to-text-3.1.2.tgz#ecf30c47141f41e91a5d32d0b1e1859fd2ac04f2"
|
||||||
integrity sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==
|
integrity sha512-tcllLfp23dJJ+ju5wCCZHVpzsQQ43+moJbqVX3jNWPB7z/KFC4FyZD6R7y94cHL6MQ33YtMZL8Z0aIXXI4XFTw==
|
||||||
@ -2900,6 +2880,11 @@ hastscript@^7.0.0:
|
|||||||
property-information "^6.0.0"
|
property-information "^6.0.0"
|
||||||
space-separated-tokens "^2.0.0"
|
space-separated-tokens "^2.0.0"
|
||||||
|
|
||||||
|
highlight.js@~11.7.0:
|
||||||
|
version "11.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e"
|
||||||
|
integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ==
|
||||||
|
|
||||||
human-signals@^4.3.0:
|
human-signals@^4.3.0:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"
|
||||||
@ -2965,19 +2950,6 @@ internal-slot@^1.0.3, internal-slot@^1.0.4, internal-slot@^1.0.5:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
is-alphabetical@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b"
|
|
||||||
integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==
|
|
||||||
|
|
||||||
is-alphanumerical@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875"
|
|
||||||
integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==
|
|
||||||
dependencies:
|
|
||||||
is-alphabetical "^2.0.0"
|
|
||||||
is-decimal "^2.0.0"
|
|
||||||
|
|
||||||
is-arguments@^1.1.1:
|
is-arguments@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
|
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
|
||||||
@ -3046,11 +3018,6 @@ is-date-object@^1.0.1, is-date-object@^1.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-tostringtag "^1.0.0"
|
has-tostringtag "^1.0.0"
|
||||||
|
|
||||||
is-decimal@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7"
|
|
||||||
integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
|
|
||||||
|
|
||||||
is-docker@^2.0.0, is-docker@^2.1.1:
|
is-docker@^2.0.0, is-docker@^2.1.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||||
@ -3078,11 +3045,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob "^2.1.1"
|
is-extglob "^2.1.1"
|
||||||
|
|
||||||
is-hexadecimal@^2.0.0:
|
|
||||||
version "2.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027"
|
|
||||||
integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==
|
|
||||||
|
|
||||||
is-map@^2.0.1, is-map@^2.0.2:
|
is-map@^2.0.1, is-map@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
|
||||||
@ -3385,6 +3347,15 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens "^3.0.0 || ^4.0.0"
|
js-tokens "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
|
lowlight@^2.0.0:
|
||||||
|
version "2.8.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-2.8.1.tgz#5f54016ebd1b2f66b3d0b94d10ef6dd5df4f2e42"
|
||||||
|
integrity sha512-HCaGL61RKc1MYzEYn3rFoGkK0yslzCVDFJEanR19rc2L0mb8i58XM55jSRbzp9jcQrFzschPlwooC0vuNitk8Q==
|
||||||
|
dependencies:
|
||||||
|
"@types/hast" "^2.0.0"
|
||||||
|
fault "^2.0.0"
|
||||||
|
highlight.js "~11.7.0"
|
||||||
|
|
||||||
lru-cache@^5.1.1:
|
lru-cache@^5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||||
@ -4130,20 +4101,6 @@ parent-module@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites "^3.0.0"
|
callsites "^3.0.0"
|
||||||
|
|
||||||
parse-entities@^4.0.0:
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e"
|
|
||||||
integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
character-entities "^2.0.0"
|
|
||||||
character-entities-legacy "^3.0.0"
|
|
||||||
character-reference-invalid "^2.0.0"
|
|
||||||
decode-named-character-reference "^1.0.0"
|
|
||||||
is-alphanumerical "^2.0.0"
|
|
||||||
is-decimal "^2.0.0"
|
|
||||||
is-hexadecimal "^2.0.0"
|
|
||||||
|
|
||||||
parse-json@^5.0.0:
|
parse-json@^5.0.0:
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
|
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
|
||||||
@ -4154,11 +4111,6 @@ parse-json@^5.0.0:
|
|||||||
json-parse-even-better-errors "^2.3.0"
|
json-parse-even-better-errors "^2.3.0"
|
||||||
lines-and-columns "^1.1.6"
|
lines-and-columns "^1.1.6"
|
||||||
|
|
||||||
parse-numeric-range@^1.3.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz#7c63b61190d61e4d53a1197f0c83c47bb670ffa3"
|
|
||||||
integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==
|
|
||||||
|
|
||||||
parse5@^6.0.0:
|
parse5@^6.0.0:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||||
@ -4312,16 +4264,6 @@ readdirp@~3.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch "^2.2.1"
|
picomatch "^2.2.1"
|
||||||
|
|
||||||
refractor@^4.7.0:
|
|
||||||
version "4.8.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/refractor/-/refractor-4.8.1.tgz#fbdd889333a3d86c9c864479622855c9b38e9d42"
|
|
||||||
integrity sha512-/fk5sI0iTgFYlmVGYVew90AoYnNMP6pooClx/XKqyeeCQXrL0Kvgn8V0VEht5ccdljbzzF1i3Q213gcntkRExg==
|
|
||||||
dependencies:
|
|
||||||
"@types/hast" "^2.0.0"
|
|
||||||
"@types/prismjs" "^1.0.0"
|
|
||||||
hastscript "^7.0.0"
|
|
||||||
parse-entities "^4.0.0"
|
|
||||||
|
|
||||||
regenerate-unicode-properties@^10.1.0:
|
regenerate-unicode-properties@^10.1.0:
|
||||||
version "10.1.0"
|
version "10.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
|
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c"
|
||||||
@ -4374,6 +4316,17 @@ regjsparser@^0.9.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jsesc "~0.5.0"
|
jsesc "~0.5.0"
|
||||||
|
|
||||||
|
rehype-highlight@^6.0.0:
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/rehype-highlight/-/rehype-highlight-6.0.0.tgz#8097219d8813b51f4c2b6d92db27dac6cbc9a641"
|
||||||
|
integrity sha512-q7UtlFicLhetp7K48ZgZiJgchYscMma7XjzX7t23bqEJF8m6/s+viXQEe4oHjrATTIZpX7RG8CKD7BlNZoh9gw==
|
||||||
|
dependencies:
|
||||||
|
"@types/hast" "^2.0.0"
|
||||||
|
hast-util-to-text "^3.0.0"
|
||||||
|
lowlight "^2.0.0"
|
||||||
|
unified "^10.0.0"
|
||||||
|
unist-util-visit "^4.0.0"
|
||||||
|
|
||||||
rehype-katex@^6.0.2:
|
rehype-katex@^6.0.2:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-6.0.2.tgz#20197bbc10bdf79f6b999bffa6689d7f17226c35"
|
resolved "https://registry.yarnpkg.com/rehype-katex/-/rehype-katex-6.0.2.tgz#20197bbc10bdf79f6b999bffa6689d7f17226c35"
|
||||||
@ -4388,7 +4341,7 @@ rehype-katex@^6.0.2:
|
|||||||
unist-util-remove-position "^4.0.0"
|
unist-util-remove-position "^4.0.0"
|
||||||
unist-util-visit "^4.0.0"
|
unist-util-visit "^4.0.0"
|
||||||
|
|
||||||
rehype-parse@^8.0.0, rehype-parse@^8.0.2:
|
rehype-parse@^8.0.0:
|
||||||
version "8.0.4"
|
version "8.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-8.0.4.tgz#3d17c9ff16ddfef6bbcc8e6a25a99467b482d688"
|
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-8.0.4.tgz#3d17c9ff16ddfef6bbcc8e6a25a99467b482d688"
|
||||||
integrity sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==
|
integrity sha512-MJJKONunHjoTh4kc3dsM1v3C9kGrrxvA3U8PxZlP2SjH8RNUSrb+lF7Y0KVaUDnGH2QZ5vAn7ulkiajM9ifuqg==
|
||||||
@ -4398,18 +4351,6 @@ rehype-parse@^8.0.0, rehype-parse@^8.0.2:
|
|||||||
parse5 "^6.0.0"
|
parse5 "^6.0.0"
|
||||||
unified "^10.0.0"
|
unified "^10.0.0"
|
||||||
|
|
||||||
rehype-prism-plus@^1.5.1:
|
|
||||||
version "1.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/rehype-prism-plus/-/rehype-prism-plus-1.5.1.tgz#b5f4eb3c789a13ffe874c81039665e144bcb1cae"
|
|
||||||
integrity sha512-mowYefSfrIkMMxkb0fwuEXlvc5nA9b1vQ6mzujM81Qx28RI0mo7jCHsBZ2tJ4eIJKXdFn+EdPkZZBGB10K02vg==
|
|
||||||
dependencies:
|
|
||||||
hast-util-to-string "^2.0.0"
|
|
||||||
parse-numeric-range "^1.3.0"
|
|
||||||
refractor "^4.7.0"
|
|
||||||
rehype-parse "^8.0.2"
|
|
||||||
unist-util-filter "^4.0.0"
|
|
||||||
unist-util-visit "^4.0.0"
|
|
||||||
|
|
||||||
remark-breaks@^3.0.2:
|
remark-breaks@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/remark-breaks/-/remark-breaks-3.0.2.tgz#f466b9d3474d7323146c0149fc1496dabadd908e"
|
resolved "https://registry.yarnpkg.com/remark-breaks/-/remark-breaks-3.0.2.tgz#f466b9d3474d7323146c0149fc1496dabadd908e"
|
||||||
@ -4959,15 +4900,6 @@ unified@^10.0.0:
|
|||||||
trough "^2.0.0"
|
trough "^2.0.0"
|
||||||
vfile "^5.0.0"
|
vfile "^5.0.0"
|
||||||
|
|
||||||
unist-util-filter@^4.0.0:
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-filter/-/unist-util-filter-4.0.1.tgz#fd885dd48adaad345de5f5dc706ec4ff44a8d074"
|
|
||||||
integrity sha512-RynicUM/vbOSTSiUK+BnaK9XMfmQUh6gyi7L6taNgc7FIf84GukXVV3ucGzEN/PhUUkdP5hb1MmXc+3cvPUm5Q==
|
|
||||||
dependencies:
|
|
||||||
"@types/unist" "^2.0.0"
|
|
||||||
unist-util-is "^5.0.0"
|
|
||||||
unist-util-visit-parents "^5.0.0"
|
|
||||||
|
|
||||||
unist-util-find-after@^4.0.0:
|
unist-util-find-after@^4.0.0:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz#80c69c92b0504033638ce11973f4135f2c822e2d"
|
resolved "https://registry.yarnpkg.com/unist-util-find-after/-/unist-util-find-after-4.0.1.tgz#80c69c92b0504033638ce11973f4135f2c822e2d"
|
||||||
|
Loading…
Reference in New Issue
Block a user