mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-10-01 15:46:39 +08:00
feat: export as image
This commit is contained in:
parent
ba08b10de1
commit
f429a5d2a8
@ -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 {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
1
app/icons/export-Image.svg
Normal file
1
app/icons/export-Image.svg
Normal 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 |
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user