diff --git a/app/components/chat.tsx b/app/components/chat.tsx
index 80c6655d..880df70d 100644
--- a/app/components/chat.tsx
+++ b/app/components/chat.tsx
@@ -7,7 +7,6 @@ import RenameIcon from "../icons/rename.svg";
import ExportIcon from "../icons/share.svg";
import ReturnIcon from "../icons/return.svg";
import CopyIcon from "../icons/copy.svg";
-import DownloadIcon from "../icons/download.svg";
import LoadingIcon from "../icons/three-dots.svg";
import PromptIcon from "../icons/prompt.svg";
import MaskIcon from "../icons/mask.svg";
@@ -53,7 +52,7 @@ import { IconButton } from "./button";
import styles from "./home.module.scss";
import chatStyle from "./chat.module.scss";
-import { ListItem, Modal, showModal, showToast } from "./ui-lib";
+import { ListItem, Modal } from "./ui-lib";
import { useLocation, useNavigate } from "react-router-dom";
import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant";
import { Avatar } from "./emoji";
@@ -61,49 +60,12 @@ import { MaskAvatar, MaskConfig } from "./mask";
import { useMaskStore } from "../store/mask";
import { useCommand } from "../command";
import { prettyObject } from "../utils/format";
+import { ExportMessageModal } from "./exporter";
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
loading: () => ,
});
-function exportMessages(messages: ChatMessage[], topic: string) {
- const mdText =
- `# ${topic}\n\n` +
- messages
- .map((m) => {
- return m.role === "user"
- ? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
- : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
- })
- .join("\n\n");
- const filename = `${topic}.md`;
-
- showModal({
- title: Locale.Export.Title,
- children: (
-
- ),
- actions: [
- }
- bordered
- text={Locale.Export.Copy}
- onClick={() => copyToClipboard(mdText)}
- />,
- }
- bordered
- text={Locale.Export.Download}
- onClick={() => downloadAs(mdText, filename)}
- />,
- ],
- });
-}
-
export function SessionConfigModel(props: { onClose: () => void }) {
const chatStore = useChatStore();
const session = chatStore.currentSession();
@@ -451,6 +413,8 @@ export function Chat() {
const config = useAppConfig();
const fontSize = config.fontSize;
+ const [showExport, setShowExport] = useState(false);
+
const inputRef = useRef(null);
const [userInput, setUserInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -739,10 +703,7 @@ export function Chat() {
bordered
title={Locale.Chat.Actions.Export}
onClick={() => {
- exportMessages(
- session.messages.filter((msg) => !msg.isError),
- session.topic,
- );
+ setShowExport(true);
}}
/>
@@ -917,6 +878,10 @@ export function Chat() {
/>
+
+ {showExport && (
+ setShowExport(false)} />
+ )}
);
}
diff --git a/app/components/exporter.module.scss b/app/components/exporter.module.scss
new file mode 100644
index 00000000..1460ac76
--- /dev/null
+++ b/app/components/exporter.module.scss
@@ -0,0 +1,212 @@
+.message-exporter {
+ &-body {
+ margin-top: 20px;
+ }
+}
+
+.export-content {
+ white-space: break-spaces;
+ padding: 10px !important;
+}
+
+.steps {
+ background-color: var(--gray);
+ border-radius: 10px;
+ overflow: hidden;
+ padding: 5px;
+ position: relative;
+ box-shadow: var(--card-shadow) inset;
+
+ .steps-progress {
+ $padding: 5px;
+ height: calc(100% - 2 * $padding);
+ width: calc(100% - 2 * $padding);
+ position: absolute;
+ top: $padding;
+ left: $padding;
+
+ &-inner {
+ box-sizing: border-box;
+ box-shadow: var(--card-shadow);
+ border: var(--border-in-light);
+ content: "";
+ display: inline-block;
+ width: 0%;
+ height: 100%;
+ background-color: var(--white);
+ transition: all ease 0.3s;
+ border-radius: 8px;
+ }
+ }
+
+ .steps-inner {
+ display: flex;
+ transform: scale(1);
+
+ .step {
+ flex-grow: 1;
+ padding: 5px 10px;
+ font-size: 14px;
+ color: var(--black);
+ opacity: 0.5;
+ transition: all ease 0.3s;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ $radius: 8px;
+
+ &-finished {
+ opacity: 0.9;
+ }
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &-current {
+ color: var(--primary);
+ }
+
+ .step-index {
+ background-color: var(--gray);
+ border: var(--border-in-light);
+ border-radius: 6px;
+ display: inline-block;
+ padding: 0px 5px;
+ font-size: 12px;
+ margin-right: 8px;
+ opacity: 0.8;
+ }
+
+ .step-name {
+ font-size: 12px;
+ }
+ }
+ }
+}
+
+.preview-actions {
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+
+ button {
+ flex-grow: 1;
+ &:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+}
+
+.image-previewer {
+ .preview-body {
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: var(--card-shadow) inset;
+ background-color: var(--gray);
+
+ .chat-info {
+ background-color: var(--second);
+ padding: 20px;
+ border-radius: 10px;
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+ position: relative;
+ overflow: hidden;
+
+ @media screen and (max-width: 600px) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .icons {
+ margin-bottom: 20px;
+ }
+ }
+
+ .logo {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ transform: scale(2);
+ }
+
+ .main-title {
+ font-size: 20px;
+ font-weight: bolder;
+ }
+
+ .sub-title {
+ font-size: 12px;
+ }
+
+ .icons {
+ margin-top: 10px;
+ display: flex;
+ align-items: center;
+
+ .icon-space {
+ font-size: 12px;
+ margin: 0 10px;
+ font-weight: bolder;
+ color: var(--primary);
+ }
+ }
+
+ .chat-info-item {
+ font-size: 12px;
+ color: var(--primary);
+ padding: 2px 15px;
+ border-radius: 10px;
+ background-color: var(--white);
+ box-shadow: var(--card-shadow);
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+ }
+ }
+
+ .message {
+ margin-bottom: 20px;
+ display: flex;
+
+ .avatar {
+ margin-right: 10px;
+ }
+
+ .body {
+ border-radius: 10px;
+ padding: 8px 10px;
+ max-width: calc(100% - 104px);
+ box-shadow: var(--card-shadow);
+ border: var(--border-in-light);
+ }
+
+ &-assistant {
+ .body {
+ background-color: var(--white);
+ }
+ }
+
+ &-user {
+ flex-direction: row-reverse;
+
+ .avatar {
+ margin-right: 0;
+ }
+
+ .body {
+ background-color: var(--second);
+ margin-right: 10px;
+ }
+ }
+ }
+ }
+
+ .default-theme {
+ }
+}
diff --git a/app/components/exporter.tsx b/app/components/exporter.tsx
new file mode 100644
index 00000000..8f38be0f
--- /dev/null
+++ b/app/components/exporter.tsx
@@ -0,0 +1,398 @@
+import { ChatMessage, useAppConfig, useChatStore } from "../store";
+import Locale from "../locales";
+import styles from "./exporter.module.scss";
+import { List, ListItem, Modal, showToast } from "./ui-lib";
+import { IconButton } from "./button";
+import { copyToClipboard, downloadAs } from "../utils";
+
+import CopyIcon from "../icons/copy.svg";
+import LoadingIcon from "../icons/three-dots.svg";
+import ChatGptIcon from "../icons/chatgpt.svg";
+import ShareIcon from "../icons/share.svg";
+
+import DownloadIcon from "../icons/download.svg";
+import { useMemo, useRef, useState } from "react";
+import { MessageSelector, useMessageSelector } from "./message-selector";
+import { Avatar } from "./emoji";
+import { MaskAvatar } from "./mask";
+import dynamic from "next/dynamic";
+
+import { toBlob, toPng } from "html-to-image";
+
+const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
+ loading: () => ,
+});
+
+export function ExportMessageModal(props: { onClose: () => void }) {
+ return (
+
+ );
+}
+
+function useSteps(
+ steps: Array<{
+ name: string;
+ value: string;
+ }>,
+) {
+ const stepCount = steps.length;
+ const [currentStepIndex, setCurrentStepIndex] = useState(0);
+ const nextStep = () =>
+ setCurrentStepIndex((currentStepIndex + 1) % stepCount);
+ const prevStep = () =>
+ setCurrentStepIndex((currentStepIndex - 1 + stepCount) % stepCount);
+
+ return {
+ currentStepIndex,
+ setCurrentStepIndex,
+ nextStep,
+ prevStep,
+ currentStep: steps[currentStepIndex],
+ };
+}
+
+function Steps<
+ T extends {
+ name: string;
+ value: string;
+ }[],
+>(props: { steps: T; onStepChange?: (index: number) => void; index: number }) {
+ const steps = props.steps;
+ const stepCount = steps.length;
+
+ return (
+
+
+
+ {steps.map((step, i) => {
+ return (
+
{
+ props.onStepChange?.(i);
+ }}
+ role="button"
+ >
+ {i + 1}
+ {step.name}
+
+ );
+ })}
+
+
+ );
+}
+
+export function MessageExporter() {
+ const steps = [
+ {
+ name: Locale.Export.Steps.Select,
+ value: "select",
+ },
+ {
+ name: Locale.Export.Steps.Preview,
+ value: "preview",
+ },
+ ];
+ const { currentStep, setCurrentStepIndex, currentStepIndex } =
+ useSteps(steps);
+ const formats = ["text", "image"] as const;
+ type ExportFormat = (typeof formats)[number];
+
+ const [exportConfig, setExportConfig] = useState({
+ format: "image" as ExportFormat,
+ includeContext: true,
+ });
+
+ function updateExportConfig(updater: (config: typeof exportConfig) => void) {
+ const config = { ...exportConfig };
+ updater(config);
+ setExportConfig(config);
+ }
+
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const { selection, updateSelection } = useMessageSelector();
+ const selectedMessages = useMemo(() => {
+ const ret: ChatMessage[] = [];
+ if (exportConfig.includeContext) {
+ ret.push(...session.mask.context);
+ }
+ ret.push(...session.messages.filter((m, i) => selection.has(m.id ?? i)));
+ return ret;
+ }, [
+ exportConfig.includeContext,
+ session.messages,
+ session.mask.context,
+ selection,
+ ]);
+
+ return (
+ <>
+
+
+
+ {currentStep.value === "select" && (
+ <>
+
+
+
+
+
+ {
+ updateExportConfig(
+ (config) =>
+ (config.includeContext = e.currentTarget.checked),
+ );
+ }}
+ >
+
+
+
+ >
+ )}
+
+ {currentStep.value === "preview" && (
+ <>
+ {exportConfig.format === "text" ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+ >
+ );
+}
+
+export function PreviewActions(props: {
+ download: () => void;
+ copy: () => void;
+}) {
+ return (
+
+ }
+ onClick={props.copy}
+ >
+ }
+ onClick={props.download}
+ >
+ }
+ onClick={() => showToast(Locale.WIP)}
+ >
+
+ );
+}
+
+export function ImagePreviewer(props: {
+ messages: ChatMessage[];
+ topic: string;
+}) {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const mask = session.mask;
+ const config = useAppConfig();
+
+ const previewRef = useRef(null);
+
+ const copy = () => {
+ const dom = previewRef.current;
+ if (!dom) return;
+ toBlob(dom).then((blob) => {
+ if (!blob) return;
+ try {
+ navigator.clipboard
+ .write([
+ new ClipboardItem({
+ "image/png": blob,
+ }),
+ ])
+ .then(() => {
+ showToast(Locale.Copy.Success);
+ });
+ } catch (e) {
+ console.error("[Copy Image] ", e);
+ showToast(Locale.Copy.Failed);
+ }
+ });
+ };
+ const download = () => {
+ const dom = previewRef.current;
+ if (!dom) return;
+ toPng(dom)
+ .then((blob) => {
+ if (!blob) return;
+ const link = document.createElement("a");
+ link.download = `${props.topic}.png`;
+ link.href = blob;
+ link.click();
+ })
+ .catch((e) => console.log("[Export Image] ", e));
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
ChatGPT Next Web
+
+ github.com/Yidadaa/ChatGPT-Next-Web
+
+
+
+
+
+ Model: {mask.modelConfig.model}
+
+
+ Messages: {props.messages.length}
+
+
+ Topic: {session.topic}
+
+
+ Time:{" "}
+ {new Date(
+ props.messages.at(-1)?.date ?? Date.now(),
+ ).toLocaleString()}
+
+
+
+ {props.messages.map((m, i) => {
+ return (
+
+
+ {m.role === "user" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export function MarkdownPreviewer(props: {
+ messages: ChatMessage[];
+ topic: string;
+}) {
+ const mdText =
+ `# ${props.topic}\n\n` +
+ props.messages
+ .map((m) => {
+ return m.role === "user"
+ ? `## ${Locale.Export.MessageFromYou}:\n${m.content}`
+ : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`;
+ })
+ .join("\n\n");
+
+ const copy = () => {
+ copyToClipboard(mdText);
+ };
+ const download = () => {
+ downloadAs(mdText, `${props.topic}.md`);
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/components/home.module.scss b/app/components/home.module.scss
index e0a1c3d0..96ce17c1 100644
--- a/app/components/home.module.scss
+++ b/app/components/home.module.scss
@@ -558,11 +558,6 @@
}
}
-.export-content {
- white-space: break-spaces;
- padding: 10px !important;
-}
-
.loading-content {
display: flex;
flex-direction: column;
diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx
index fb37fdc4..108b0570 100644
--- a/app/components/markdown.tsx
+++ b/app/components/markdown.tsx
@@ -121,7 +121,7 @@ export function Markdown(
content: string;
loading?: boolean;
fontSize?: number;
- parentRef: RefObject;
+ parentRef?: RefObject;
defaultShow?: boolean;
} & React.DOMAttributes,
) {
@@ -129,7 +129,7 @@ export function Markdown(
const renderedHeight = useRef(0);
const inView = useRef(!!props.defaultShow);
- const parent = props.parentRef.current;
+ const parent = props.parentRef?.current;
const md = mdRef.current;
const checkInView = () => {
diff --git a/app/components/message-selector.module.scss b/app/components/message-selector.module.scss
new file mode 100644
index 00000000..c1bbf201
--- /dev/null
+++ b/app/components/message-selector.module.scss
@@ -0,0 +1,55 @@
+.message-selector {
+ .message-filter {
+ display: flex;
+
+ .search-bar {
+ max-width: unset;
+ flex-grow: 1;
+ }
+
+ .filter-item:not(:last-child) {
+ margin-right: 10px;
+ }
+ }
+
+ .messages {
+ margin-top: 20px;
+ border-radius: 10px;
+ border: var(--border-in-light);
+ overflow: hidden;
+
+ .message {
+ display: flex;
+ align-items: center;
+ padding: 8px 10px;
+ cursor: pointer;
+
+ &-selected {
+ background-color: var(--second);
+ }
+
+ &:not(:last-child) {
+ border-bottom: var(--border-in-light);
+ }
+
+ .avatar {
+ margin-right: 10px;
+ }
+
+ .body {
+ flex-grow: 1;
+ max-width: calc(100% - 40px);
+
+ .date {
+ font-size: 12px;
+ line-height: 1.2;
+ opacity: 0.5;
+ }
+
+ .content {
+ font-size: 12px;
+ }
+ }
+ }
+ }
+}
diff --git a/app/components/message-selector.tsx b/app/components/message-selector.tsx
new file mode 100644
index 00000000..2c100082
--- /dev/null
+++ b/app/components/message-selector.tsx
@@ -0,0 +1,211 @@
+import { useEffect, useState } from "react";
+import { ChatMessage, useAppConfig, useChatStore } from "../store";
+import { Updater } from "../typing";
+import { IconButton } from "./button";
+import { Avatar } from "./emoji";
+import { MaskAvatar } from "./mask";
+import Locale from "../locales";
+
+import styles from "./message-selector.module.scss";
+
+function useShiftRange() {
+ const [startIndex, setStartIndex] = useState();
+ const [endIndex, setEndIndex] = useState();
+ const [shiftDown, setShiftDown] = useState(false);
+
+ const onClickIndex = (index: number) => {
+ if (shiftDown && startIndex !== undefined) {
+ setEndIndex(index);
+ } else {
+ setStartIndex(index);
+ setEndIndex(undefined);
+ }
+ };
+
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key !== "Shift") return;
+ setShiftDown(true);
+ };
+ const onKeyUp = (e: KeyboardEvent) => {
+ if (e.key !== "Shift") return;
+ setShiftDown(false);
+ setStartIndex(undefined);
+ setEndIndex(undefined);
+ };
+
+ window.addEventListener("keyup", onKeyUp);
+ window.addEventListener("keydown", onKeyDown);
+
+ return () => {
+ window.removeEventListener("keyup", onKeyUp);
+ window.removeEventListener("keydown", onKeyDown);
+ };
+ }, []);
+
+ return {
+ onClickIndex,
+ startIndex,
+ endIndex,
+ };
+}
+
+export function useMessageSelector() {
+ const [selection, setSelection] = useState(new Set());
+ const updateSelection: Updater> = (updater) => {
+ const newSelection = new Set(selection);
+ updater(newSelection);
+ setSelection(newSelection);
+ };
+
+ return {
+ selection,
+ updateSelection,
+ };
+}
+
+export function MessageSelector(props: {
+ selection: Set;
+ updateSelection: Updater>;
+ defaultSelectAll?: boolean;
+ onSelected?: (messages: ChatMessage[]) => void;
+}) {
+ const chatStore = useChatStore();
+ const session = chatStore.currentSession();
+ const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
+ const messages = session.messages.filter(
+ (m, i) =>
+ m.id && // messsage must has id
+ isValid(m) &&
+ (i >= session.messages.length - 1 || isValid(session.messages[i + 1])),
+ );
+ const messageCount = messages.length;
+ const config = useAppConfig();
+
+ const [searchInput, setSearchInput] = useState("");
+ const [searchIds, setSearchIds] = useState(new Set());
+ const isInSearchResult = (id: number) => {
+ return searchInput.length === 0 || searchIds.has(id);
+ };
+ const doSearch = (text: string) => {
+ const searchResuts = new Set();
+ if (text.length > 0) {
+ messages.forEach((m) =>
+ m.content.includes(text) ? searchResuts.add(m.id!) : null,
+ );
+ }
+ setSearchIds(searchResuts);
+ };
+
+ // for range selection
+ const { startIndex, endIndex, onClickIndex } = useShiftRange();
+
+ const selectAll = () => {
+ props.updateSelection((selection) =>
+ messages.forEach((m) => selection.add(m.id!)),
+ );
+ };
+
+ useEffect(() => {
+ if (props.defaultSelectAll) {
+ selectAll();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ if (startIndex === undefined || endIndex === undefined) {
+ return;
+ }
+ const [start, end] = [startIndex, endIndex].sort((a, b) => a - b);
+ props.updateSelection((selection) => {
+ for (let i = start; i <= end; i += 1) {
+ selection.add(messages[i].id ?? i);
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [startIndex, endIndex]);
+
+ return (
+
+
+ {
+ setSearchInput(e.currentTarget.value);
+ doSearch(e.currentTarget.value);
+ }}
+ >
+
+
+
+ props.updateSelection((selection) => {
+ selection.clear();
+ messages
+ .slice(messageCount - 10)
+ .forEach((m) => selection.add(m.id!));
+ })
+ }
+ />
+
+ props.updateSelection((selection) => selection.clear())
+ }
+ />
+
+
+
+ {messages.map((m, i) => {
+ if (!isInSearchResult(m.id!)) return null;
+
+ return (
+
{
+ props.updateSelection((selection) => {
+ const id = m.id ?? i;
+ selection.has(id) ? selection.delete(id) : selection.add(id);
+ });
+ onClickIndex(i);
+ }}
+ >
+
+ {m.role === "user" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {new Date(m.date).toLocaleString()}
+
+
+ {m.content}
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index c164603b..989a54bf 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -36,11 +36,30 @@ const cn = {
},
},
Export: {
- Title: "导出聊天记录为 Markdown",
+ Title: "分享聊天记录",
Copy: "全部复制",
Download: "下载文件",
+ Share: "分享到 ShareGPT",
MessageFromYou: "来自你的消息",
MessageFromChatGPT: "来自 ChatGPT 的消息",
+ Format: {
+ Title: "导出格式",
+ SubTitle: "可以导出 Markdown 文本或者 PNG 图片",
+ },
+ IncludeContext: {
+ Title: "包含面具上下文",
+ SubTitle: "是否在消息中展示面具上下文",
+ },
+ Steps: {
+ Select: "选取",
+ Preview: "预览",
+ },
+ },
+ Select: {
+ Search: "搜索消息",
+ All: "选取全部",
+ Latest: "最近十条",
+ Clear: "清除选中",
},
Memory: {
Title: "历史摘要",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 12ad101a..1ff66558 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -37,11 +37,30 @@ const en: RequiredLocaleType = {
},
},
Export: {
- Title: "All Messages",
+ Title: "Export Messages",
Copy: "Copy All",
Download: "Download",
MessageFromYou: "Message From You",
MessageFromChatGPT: "Message From ChatGPT",
+ Share: "Share to ShareGPT",
+ Format: {
+ Title: "Export Format",
+ SubTitle: "Markdown or PNG Image",
+ },
+ IncludeContext: {
+ Title: "Including Context",
+ SubTitle: "Export context prompts in mask or not",
+ },
+ Steps: {
+ Select: "Select",
+ Preview: "Preview",
+ },
+ },
+ Select: {
+ Search: "Search",
+ All: "Select All",
+ Latest: "Select Latest",
+ Clear: "Clear",
},
Memory: {
Title: "Memory Prompt",
diff --git a/package.json b/package.json
index 7acc6aad..e1d97f7b 100644
--- a/package.json
+++ b/package.json
@@ -13,12 +13,13 @@
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
},
"dependencies": {
- "@hello-pangea/dnd": "^16.2.0",
"@fortaine/fetch-event-source": "^3.0.6",
+ "@hello-pangea/dnd": "^16.2.0",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11",
"emoji-picker-react": "^4.4.7",
"fuse.js": "^6.6.2",
+ "html-to-image": "^1.11.11",
"mermaid": "^10.1.0",
"next": "^13.4.3",
"node-fetch": "^3.3.1",
diff --git a/yarn.lock b/yarn.lock
index 584db6dc..1e7b44ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3215,6 +3215,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
dependencies:
react-is "^16.7.0"
+html-to-image@^1.11.11:
+ version "1.11.11"
+ resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea"
+ integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==
+
human-signals@^4.3.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2"