mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-09 03:11:47 +08:00
save artifact content to cloudflare workers kv
This commit is contained in:
190
app/components/artifact.tsx
Normal file
190
app/components/artifact.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useEffect, useState, useRef, useMemo } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { useWindowSize } from "@/app/utils";
|
||||
import { IconButton } from "./button";
|
||||
import { nanoid } from "nanoid";
|
||||
import ExportIcon from "../icons/share.svg";
|
||||
import CopyIcon from "../icons/copy.svg";
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
import Locale from "../locales";
|
||||
import { Modal, showToast } from "./ui-lib";
|
||||
import { copyToClipboard, downloadAs } from "../utils";
|
||||
import { Path, ApiPath, REPO_URL } from "@/app/constant";
|
||||
import { Loading } from "./home";
|
||||
|
||||
export function HTMLPreview(props: {
|
||||
code: string;
|
||||
autoHeight?: boolean;
|
||||
height?: number;
|
||||
}) {
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const frameId = useRef<string>(nanoid());
|
||||
const [iframeHeight, setIframeHeight] = useState(600);
|
||||
/*
|
||||
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
|
||||
* 1. using srcdoc
|
||||
* 2. using src with dataurl:
|
||||
* easy to share
|
||||
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", (e) => {
|
||||
const { id, height } = e.data;
|
||||
if (id == frameId.current) {
|
||||
console.log("setHeight", height);
|
||||
setIframeHeight(height);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const height = useMemo(() => {
|
||||
const parentHeight = props.height || 600;
|
||||
if (props.autoHeight !== false) {
|
||||
return iframeHeight > parentHeight ? parentHeight : iframeHeight + 40;
|
||||
} else {
|
||||
return parentHeight;
|
||||
}
|
||||
}, [props.autoHeight, props.height, iframeHeight]);
|
||||
|
||||
const srcDoc = useMemo(() => {
|
||||
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
||||
if (props.code.includes("</head>")) {
|
||||
props.code.replace("</head>", "</head>" + script);
|
||||
}
|
||||
return props.code + script;
|
||||
}, [props.code]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
id={frameId.current}
|
||||
ref={ref}
|
||||
frameBorder={0}
|
||||
sandbox="allow-forms allow-modals allow-scripts"
|
||||
style={{ width: "100%", height }}
|
||||
// src={`data:text/html,${encodeURIComponent(srcDoc)}`}
|
||||
srcDoc={srcDoc}
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArtifactShareButton({ getCode, id, style }) {
|
||||
const [name, setName] = useState(id);
|
||||
const [show, setShow] = useState(false);
|
||||
const shareUrl = useMemo(() =>
|
||||
[location.origin, "#", Path.Artifact, "/", name].join(""),
|
||||
);
|
||||
const upload = (code) =>
|
||||
fetch(ApiPath.Artifact, {
|
||||
method: "POST",
|
||||
body: getCode(),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ id }) => {
|
||||
if (id) {
|
||||
setShow(true);
|
||||
return setName(id);
|
||||
}
|
||||
throw Error();
|
||||
})
|
||||
.catch((e) => {
|
||||
showToast(Locale.Export.Artifact.Error);
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="window-action-button" style={style}>
|
||||
<IconButton
|
||||
icon={<ExportIcon />}
|
||||
bordered
|
||||
title={Locale.Export.Artifact.Title}
|
||||
onClick={() => {
|
||||
upload(getCode());
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{show && (
|
||||
<div className="modal-mask">
|
||||
<Modal
|
||||
title={Locale.Export.Artifact.Title}
|
||||
onClose={() => setShow(false)}
|
||||
actions={[
|
||||
<IconButton
|
||||
key="download"
|
||||
icon={<DownloadIcon />}
|
||||
bordered
|
||||
text={Locale.Export.Download}
|
||||
onClick={() => {
|
||||
downloadAs(getCode(), `${id}.html`).then(() =>
|
||||
setShow(false),
|
||||
);
|
||||
}}
|
||||
/>,
|
||||
<IconButton
|
||||
key="copy"
|
||||
icon={<CopyIcon />}
|
||||
bordered
|
||||
text={Locale.Chat.Actions.Copy}
|
||||
onClick={() => {
|
||||
copyToClipboard(shareUrl).then(() => setShow(false));
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<a target="_blank" href={shareUrl}>
|
||||
{shareUrl}
|
||||
</a>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function Artifact() {
|
||||
const { id } = useParams();
|
||||
const [code, setCode] = useState("");
|
||||
const { height } = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetch(`${ApiPath.Artifact}?id=${id}`)
|
||||
.then((res) => res.text())
|
||||
.then(setCode);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
disply: "block",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 40,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton bordered icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
</div>
|
||||
<ArtifactShareButton id={id} getCode={() => code} />
|
||||
</div>
|
||||
{code ? (
|
||||
<HTMLPreview code={code} autoHeight={false} height={height - 40} />
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -39,6 +39,10 @@ export function Loading(props: { noLogo?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
const Artifact = dynamic(async () => (await import("./artifact")).Artifact, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const Settings = dynamic(async () => (await import("./settings")).Settings, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
@@ -125,6 +129,7 @@ const loadAsyncGoogleFont = () => {
|
||||
function Screen() {
|
||||
const config = useAppConfig();
|
||||
const location = useLocation();
|
||||
const isArtifact = location.pathname.includes(Path.Artifact);
|
||||
const isHome = location.pathname === Path.Home;
|
||||
const isAuth = location.pathname === Path.Auth;
|
||||
const isMobileScreen = useMobileScreen();
|
||||
@@ -135,6 +140,14 @@ function Screen() {
|
||||
loadAsyncGoogleFont();
|
||||
}, []);
|
||||
|
||||
if (isArtifact) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route exact path="/artifact/:id" element={<Artifact />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
@@ -13,7 +13,7 @@ import LoadingIcon from "../icons/three-dots.svg";
|
||||
import React from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { showImageModal } from "./ui-lib";
|
||||
import { nanoid } from "nanoid";
|
||||
import { ArtifactShareButton, HTMLPreview } from "./artifact";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -61,56 +61,6 @@ export function Mermaid(props: { code: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function HTMLPreview(props: { code: string }) {
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const frameId = useRef<string>(nanoid());
|
||||
const [height, setHeight] = useState(600);
|
||||
/*
|
||||
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
|
||||
* 1. using srcdoc
|
||||
* 2. using src with dataurl:
|
||||
* easy to share
|
||||
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("message", (e) => {
|
||||
const { id, height } = e.data;
|
||||
if (id == frameId.current) {
|
||||
console.log("setHeight", height);
|
||||
if (height < 600) {
|
||||
setHeight(height + 40);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const script = encodeURIComponent(
|
||||
`<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="no-dark html"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
overflow: "auto",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<iframe
|
||||
id={frameId.current}
|
||||
ref={ref}
|
||||
frameBorder={0}
|
||||
sandbox="allow-forms allow-modals allow-scripts"
|
||||
style={{ width: "100%", height }}
|
||||
src={`data:text/html,${encodeURIComponent(props.code)}${script}`}
|
||||
// srcDoc={props.code + script}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const refText = ref.current?.innerText;
|
||||
@@ -151,7 +101,22 @@ export function PreCode(props: { children: any }) {
|
||||
{mermaidCode.length > 0 && (
|
||||
<Mermaid code={mermaidCode} key={mermaidCode} />
|
||||
)}
|
||||
{htmlCode.length > 0 && <HTMLPreview code={htmlCode} key={htmlCode} />}
|
||||
{htmlCode.length > 0 && (
|
||||
<div
|
||||
className="no-dark html"
|
||||
style={{
|
||||
overflow: "auto",
|
||||
position: "relative",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<ArtifactShareButton
|
||||
style={{ position: "absolute", right: 10, top: 10 }}
|
||||
getCode={() => htmlCode}
|
||||
/>
|
||||
<HTMLPreview code={htmlCode} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user