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; flex: 1;
overflow: auto; overflow: auto;
padding: 20px; padding: 20px;
background-color: var(--white);
} }
.chat-body-title { .chat-body-title {

View File

@ -20,6 +20,7 @@ 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 CopyIcon from "../icons/copy.svg";
import DownloadIcon from "../icons/download.svg"; import DownloadIcon from "../icons/download.svg";
import ExportImage from "../icons/export-image.svg";
import { Message, SubmitKey, useChatStore, ChatSession } from "../store"; import { Message, SubmitKey, useChatStore, ChatSession } from "../store";
import { showModal, showToast } from "./ui-lib"; import { showModal, showToast } from "./ui-lib";
@ -36,6 +37,7 @@ import dynamic from "next/dynamic";
import { REPO_URL } from "../constant"; import { REPO_URL } from "../constant";
import { ControllerPool } from "../requests"; import { ControllerPool } from "../requests";
import { Prompt, usePromptStore } from "../store/prompt"; import { Prompt, usePromptStore } from "../store/prompt";
import { toPng } from "html-to-image";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (
@ -341,6 +343,9 @@ export function Chat(props: {
}, 500); }, 500);
}); });
//export image
const [exportImageLoading, setExportImageLoading] = useState(false);
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className={styles["window-header"]}> <div className={styles["window-header"]}>
@ -394,10 +399,28 @@ export function Chat(props: {
}} }}
/> />
</div> </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> </div>
<div className={styles["chat-body"]}> <div className={styles["chat-body"]} id="chat-body">
{messages.map((message, i) => { {messages.map((message, i) => {
const isUser = message.role === "user"; 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 useHasHydrated = () => {
const [hasHydrated, setHasHydrated] = useState<boolean>(false); 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 <link
href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap" href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;700;900&display=swap"
rel="stylesheet" rel="stylesheet"
crossOrigin="anonymous"
></link> ></link>
<script src="/serviceWorkerRegister.js" defer></script> <script src="/serviceWorkerRegister.js" defer></script>
</head> </head>

View File

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

View File

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

View File

@ -19,6 +19,8 @@ const es: LocaleType = {
Copy: "Copiar", Copy: "Copiar",
Stop: "Detener", Stop: "Detener",
Retry: "Reintentar", Retry: "Reintentar",
ExportImage: "Exportar imagen",
GeneratingImage: "Generando imagen",
}, },
Rename: "Renombrar chat", Rename: "Renombrar chat",
Typing: "Escribiendo...", Typing: "Escribiendo...",
@ -35,6 +37,8 @@ const es: LocaleType = {
Title: "Todos los mensajes", Title: "Todos los mensajes",
Copy: "Copiar todo", Copy: "Copiar todo",
Download: "Descargar", Download: "Descargar",
Image: "Exportar imagen",
Failed: "Exportación fallida",
}, },
Memory: { Memory: {
Title: "Historial de memoria", Title: "Historial de memoria",
@ -143,11 +147,13 @@ const es: LocaleType = {
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 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: { Copy: {
Success: "Copiado al portapapeles", 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",
}, },
}; };

View File

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

View File

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

View File

@ -2890,6 +2890,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"
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: 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"