feat: export as image

This commit is contained in:
Dogtiti 2023-04-01 17:57:17 +08:00
parent ba08b10de1
commit f429a5d2a8
10 changed files with 79 additions and 5 deletions

View File

@ -218,6 +218,7 @@
flex: 1;
overflow: auto;
padding: 20px;
background-color: var(--white);
}
.chat-body-title {

View File

@ -20,6 +20,7 @@ import MenuIcon from "../icons/menu.svg";
import CloseIcon from "../icons/close.svg";
import CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg";
import ExportImage from "../icons/export-image.svg";
import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
import { showModal, showToast } from "./ui-lib";
@ -36,6 +37,7 @@ import dynamic from "next/dynamic";
import { REPO_URL } from "../constant";
import { ControllerPool } from "../requests";
import { Prompt, usePromptStore } from "../store/prompt";
import { toPng } from "html-to-image";
export function Loading(props: { noLogo?: boolean }) {
return (
@ -341,6 +343,9 @@ export function Chat(props: {
}, 500);
});
//export image
const [exportImageLoading, setExportImageLoading] = useState(false);
return (
<div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}>
@ -394,10 +399,28 @@ export function Chat(props: {
}}
/>
</div>
<div className={styles["window-action-button"]}>
{exportImageLoading ? (
<IconButton
icon={<LoadingIcon />}
bordered
title={Locale.Chat.Actions.GeneratingImage}
/>
) : (
<IconButton
icon={<ExportImage />}
bordered
title={Locale.Chat.Actions.ExportImage}
onClick={() => {
exportImage(session.topic, setExportImageLoading);
}}
/>
)}
</div>
</div>
</div>
<div className={styles["chat-body"]}>
<div className={styles["chat-body"]} id="chat-body">
{messages.map((message, i) => {
const isUser = message.role === "user";
@ -584,6 +607,30 @@ function showMemoryPrompt(session: ChatSession) {
});
}
async function exportImage(
topic: string,
setExportImageLoading: (loading: boolean) => void,
) {
setExportImageLoading(true);
const element = document.querySelector("#chat-body") as HTMLElement;
try {
const dataURL = await toPng(element, {
width: element.scrollWidth,
height: element.scrollHeight,
});
let link = document.createElement("a");
link.download = `${topic}-${new Date().toLocaleString()}.png`;
link.href = dataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setExportImageLoading(false);
} catch (error) {
showToast(Locale.Export.Failed);
setExportImageLoading(false);
}
}
const useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false);

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1680342009123" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3081" id="mx_n_1680342009124" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M859.3 191.8H164.2c-55 0-100 45-100 100V765c0 55 45 100 100 100h695.1c55 0 100-45 100-100V291.8c0-55-45-100-100-100z m36 573.2c0 19.5-16.5 36-36 36H164.2c-19.5 0-36-16.5-36-36V291.8c0-19.5 16.5-36 36-36h695.1c19.5 0 36 16.5 36 36V765z" fill="#333333" p-id="3082"></path><path d="M64.21 638.816l197.238-126.841 34.617 53.83L98.827 692.646zM368.923 723.565l378.68-281.275 38.164 51.38-378.68 281.274z" fill="#333333" p-id="3083"></path><path d="M295.644 489.41L456.4 739.385l-53.83 34.618-160.756-249.975zM901.4 778.2L705.8 474l51.2-38.7 198.2 308.3zM661 277.8c-42.4 0-76.8 34.4-76.8 76.8s34.4 76.8 76.8 76.8 76.8-34.4 76.8-76.8-34.4-76.8-76.8-76.8z m33.1 109.8c-18.2 18.2-47.9 18.2-66.1 0-18.2-18.2-18.2-47.9 0-66.1s47.9-18.2 66.1 0 18.2 47.9 0 66.1z" fill="#333333" p-id="3084"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -60,6 +60,7 @@ export default function RootLayout({
<link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"
rel="stylesheet"
crossOrigin="anonymous"
></link>
<script src="/serviceWorkerRegister.js" defer></script>
</head>

View File

@ -17,6 +17,8 @@ const cn = {
Copy: "复制",
Stop: "停止",
Retry: "重试",
ExportImage: "导出图片",
GeneratingImage: "正在生成图片",
},
Rename: "重命名对话",
Typing: "正在输入…",
@ -33,6 +35,8 @@ const cn = {
Title: "导出聊天记录为 Markdown",
Copy: "全部复制",
Download: "下载文件",
Image: "导出图片",
Failed: "导出失败",
},
Memory: {
Title: "上下文记忆 Prompt",

View File

@ -19,6 +19,8 @@ const en: LocaleType = {
Copy: "Copy",
Stop: "Stop",
Retry: "Retry",
ExportImage: "Export All Messages as Image",
GeneratingImage: "Generating Image",
},
Rename: "Rename Chat",
Typing: "Typing…",
@ -35,6 +37,8 @@ const en: LocaleType = {
Title: "All Messages",
Copy: "Copy All",
Download: "Download",
Image: "Export Image",
Failed: "Export Failed",
},
Memory: {
Title: "Memory Prompt",

View File

@ -19,6 +19,8 @@ const es: LocaleType = {
Copy: "Copiar",
Stop: "Detener",
Retry: "Reintentar",
ExportImage: "Exportar imagen",
GeneratingImage: "Generando imagen",
},
Rename: "Renombrar chat",
Typing: "Escribiendo...",
@ -35,6 +37,8 @@ const es: LocaleType = {
Title: "Todos los mensajes",
Copy: "Copiar todo",
Download: "Descargar",
Image: "Exportar imagen",
Failed: "Exportación fallida",
},
Memory: {
Title: "Historial de memoria",
@ -143,12 +147,14 @@ const es: LocaleType = {
Summarize:
"Resuma nuestra discusión brevemente en 50 caracteres o menos para usarlo como un recordatorio para futuros contextos.",
},
ConfirmClearAll: "¿Confirmar para borrar todos los datos de chat y configuración?",
ConfirmClearAll:
"¿Confirmar para borrar todos los datos de chat y configuración?",
},
Copy: {
Success: "Copiado al portapapeles",
Failed: "La copia falló, por favor concede permiso para acceder al portapapeles",
Failed:
"La copia falló, por favor concede permiso para acceder al portapapeles",
},
};
export default es;
export default es;

View File

@ -18,6 +18,8 @@ const tw: LocaleType = {
Copy: "複製",
Stop: "停止",
Retry: "重試",
ExportImage: "匯出圖片",
GeneratingImage: "正在生成圖片",
},
Rename: "重命名對話",
Typing: "正在輸入…",
@ -34,6 +36,8 @@ const tw: LocaleType = {
Title: "匯出聊天記錄為 Markdown",
Copy: "複製全部",
Download: "下載檔案",
Image: "匯出圖片",
Failed: "匯出失敗",
},
Memory: {
Title: "上下文記憶 Prompt",

View File

@ -17,15 +17,16 @@
"emoji-picker-react": "^4.4.7",
"eventsource-parser": "^0.1.0",
"fuse.js": "^6.6.2",
"html-to-image": "^1.11.11",
"next": "^13.2.3",
"node-fetch": "^3.3.1",
"openai": "^3.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^8.0.5",
"remark-breaks": "^3.0.2",
"rehype-katex": "^6.0.2",
"rehype-prism-plus": "^1.5.1",
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.59.2",

View File

@ -2890,6 +2890,11 @@ hastscript@^7.0.0:
property-information "^6.0.0"
space-separated-tokens "^2.0.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"