mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-09-30 23:26:39 +08:00
refactor: export image
This commit is contained in:
parent
f429a5d2a8
commit
7d446cc7f8
@ -451,3 +451,10 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-body {
|
||||
height: 40vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ 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 (
|
||||
@ -60,6 +59,14 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
|
||||
const HtmlToImage = dynamic(
|
||||
async () => (await import("./html-to-image")).HtmlToImage,
|
||||
{
|
||||
loading: () => <LoadingIcon />,
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
|
||||
export function Avatar(props: { role: Message["role"] }) {
|
||||
const config = useChatStore((state) => state.config);
|
||||
|
||||
@ -343,8 +350,34 @@ export function Chat(props: {
|
||||
}, 500);
|
||||
});
|
||||
|
||||
//export image
|
||||
const [exportImageLoading, setExportImageLoading] = useState(false);
|
||||
// export image
|
||||
const dataUrl = useRef("");
|
||||
|
||||
async function exportImage(topic: string) {
|
||||
showModal({
|
||||
title: Locale.Export.Image,
|
||||
children: (
|
||||
<div className={styles["image-body"]}>
|
||||
<HtmlToImage
|
||||
getDataUrl={(url) => {
|
||||
dataUrl.current = url;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
actions: [
|
||||
<IconButton
|
||||
key="exportPng"
|
||||
icon={<ExportImage />}
|
||||
bordered
|
||||
text={Locale.Chat.Actions.ExportImage}
|
||||
onClick={() => {
|
||||
dataUrl.current && exportPng(topic, dataUrl.current);
|
||||
}}
|
||||
/>,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.chat} key={session.id}>
|
||||
@ -400,22 +433,14 @@ 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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<ExportImage />}
|
||||
bordered
|
||||
title={Locale.Chat.Actions.ExportImage}
|
||||
onClick={() => {
|
||||
exportImage(session.topic);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -607,28 +632,11 @@ 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);
|
||||
}
|
||||
function exportPng(topic: string, dataURL: string) {
|
||||
const a = document.createElement("a");
|
||||
a.href = dataURL;
|
||||
a.download = `${topic}-${new Date().toLocaleString()}.jpg`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
const useHasHydrated = () => {
|
||||
|
7
app/components/html-to-image.module.scss
Normal file
7
app/components/html-to-image.module.scss
Normal file
@ -0,0 +1,7 @@
|
||||
.image-wrap {
|
||||
height: 100%;
|
||||
> img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
35
app/components/html-to-image.tsx
Normal file
35
app/components/html-to-image.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { toJpeg } from "html-to-image";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import styles from "./html-to-image.module.scss";
|
||||
|
||||
export function HtmlToImage(props: { getDataUrl: (url: string) => void }) {
|
||||
const [imageUrl, setImageUrl] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.querySelector("#chat-body") as HTMLElement;
|
||||
if (element) {
|
||||
toJpeg(element, {
|
||||
width: element.scrollWidth,
|
||||
height: element.scrollHeight,
|
||||
})
|
||||
.then((dataUrl) => {
|
||||
setImageUrl(dataUrl);
|
||||
props?.getDataUrl(dataUrl);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{loading ? (
|
||||
<LoadingIcon />
|
||||
) : (
|
||||
<div className={styles["image-wrap"]}>
|
||||
<img src={imageUrl} alt="" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1 +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>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M44 24C44 22.8954 43.1046 22 42 22C40.8954 22 40 22.8954 40 24H44ZM24 8C25.1046 8 26 7.10457 26 6C26 4.89543 25.1046 4 24 4V8ZM39 40H9V44H39V40ZM8 39V9H4V39H8ZM40 24V39H44V24H40ZM9 8H24V4H9V8ZM9 40C8.44772 40 8 39.5523 8 39H4C4 41.7614 6.23857 44 9 44V40ZM39 44C41.7614 44 44 41.7614 44 39H40C40 39.5523 39.5523 40 39 40V44ZM8 9C8 8.44772 8.44771 8 9 8V4C6.23858 4 4 6.23857 4 9H8Z" fill="#333"/><path d="M6 35L16.6931 25.198C17.4389 24.5143 18.5779 24.4953 19.3461 25.1538L32 36" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 31L32.7735 26.2265C33.4772 25.5228 34.5914 25.4436 35.3877 26.0408L42 31" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M32 13L37 18L42 13" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M37 6L37 18" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Loading…
Reference in New Issue
Block a user