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 LoadingButtonIcon from "../icons/loading.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; onLoad?: (title?: string) => void; }) { const ref = useRef(null); const frameId = useRef(nanoid()); const [iframeHeight, setIframeHeight] = useState(600); const [title, setTitle] = useState(""); /* * 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, title } = e.data; setTitle(title); if (id == frameId.current) { 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 = ``; if (props.code.includes("")) { props.code.replace("", "" + script); } return props.code + script; }, [props.code]); return ( ); } export function ArtifactShareButton({ getCode, id, style, fileName, }: { getCode: () => string; id?: string; style?: any; fileName?: string; }) { const [loading, setLoading] = useState(false); const [name, setName] = useState(id); const [show, setShow] = useState(false); const shareUrl = useMemo( () => [location.origin, "#", Path.Artifact, "/", name].join(""), [name], ); const upload = (code: string) => id ? Promise.resolve({ id }) : fetch(ApiPath.Artifact, { method: "POST", body: code, }) .then((res) => res.json()) .then(({ id }) => { if (id) { return { id }; } throw Error(); }) .catch((e) => { showToast(Locale.Export.Artifact.Error); }); return ( <>
: } bordered title={Locale.Export.Artifact.Title} onClick={() => { setLoading(true); upload(getCode()) .then((res) => { if (res?.id) { setShow(true); setName(res?.id); } }) .finally(() => setLoading(false)); }} />
{show && (
setShow(false)} actions={[ } bordered text={Locale.Export.Download} onClick={() => { downloadAs(getCode(), `${fileName || name}.html`).then(() => setShow(false), ); }} />, } bordered text={Locale.Chat.Actions.Copy} onClick={() => { copyToClipboard(shareUrl).then(() => setShow(false)); }} />, ]} >
)} ); } export function Artifact() { const { id } = useParams(); const [code, setCode] = useState(""); const [loading, setLoading] = useState(true); const [fileName, setFileName] = useState(""); const { height } = useWindowSize(); useEffect(() => { if (id) { fetch(`${ApiPath.Artifact}?id=${id}`) .then((res) => res.text()) .then(setCode); } }, [id]); return (
} shadow />
NextChat Artifact
code} fileName={fileName} />
{loading && } {code && ( { setFileName(title as string); setLoading(false); }} /> )}
); }