diff --git a/app/components/artifacts.tsx b/app/components/artifacts.tsx index 6c0164b91..4ae7170d5 100644 --- a/app/components/artifacts.tsx +++ b/app/components/artifacts.tsx @@ -23,84 +23,89 @@ import { Path, ApiPath, REPO_URL } from "@/app/constant"; import { Loading } from "./home"; import styles from "./artifacts.module.scss"; -export const HTMLPreview = forwardRef< - { - reload: () => void; - }, - { - code: string; - autoHeight?: boolean; - height?: number | string; - onLoad?: (title?: string) => void; - } ->(function HTMLPreview(props, ref) { - const iframeRef = useRef<HTMLIFrameElement>(null); - const [frameId, setFrameId] = useState<string>(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.) - */ +type HTMLPreviewProps = { + code: string; + autoHeight?: boolean; + height?: number | string; + onLoad?: (title?: string) => void; +}; - useEffect(() => { - const handleMessage = (e: any) => { - const { id, height, title } = e.data; - setTitle(title); - if (id == frameId) { - setIframeHeight(height); +export type HTMLPreviewHander = { + reload: () => void; +}; + +export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>( + function HTMLPreview(props, ref) { + const iframeRef = useRef<HTMLIFrameElement>(null); + const [frameId, setFrameId] = useState<string>(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(() => { + const handleMessage = (e: any) => { + const { id, height, title } = e.data; + setTitle(title); + if (id == frameId) { + setIframeHeight(height); + } + }; + window.addEventListener("message", handleMessage); + return () => { + window.removeEventListener("message", handleMessage); + }; + }, [frameId]); + + useImperativeHandle(ref, () => ({ + reload: () => { + setFrameId(nanoid()); + }, + })); + + const height = useMemo(() => { + if (!props.autoHeight) return props.height || 600; + if (typeof props.height === "string") { + return props.height; + } + const parentHeight = props.height || 600; + return iframeHeight + 40 > parentHeight + ? parentHeight + : iframeHeight + 40; + }, [props.autoHeight, props.height, iframeHeight]); + + const srcDoc = useMemo(() => { + const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', 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, frameId]); + + const handleOnLoad = () => { + if (props?.onLoad) { + props.onLoad(title); } }; - window.addEventListener("message", handleMessage); - return () => { - window.removeEventListener("message", handleMessage); - }; - }, [frameId]); - useImperativeHandle(ref, () => ({ - reload: () => { - setFrameId(nanoid()); - }, - })); - - const height = useMemo(() => { - if (!props.autoHeight) return props.height || 600; - if (typeof props.height === "string") { - return props.height; - } - const parentHeight = props.height || 600; - return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40; - }, [props.autoHeight, props.height, iframeHeight]); - - const srcDoc = useMemo(() => { - const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', 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, frameId]); - - const handleOnLoad = () => { - if (props?.onLoad) { - props.onLoad(title); - } - }; - - return ( - <iframe - className={styles["artifacts-iframe"]} - key={frameId} - ref={iframeRef} - sandbox="allow-forms allow-modals allow-scripts" - style={{ height }} - srcDoc={srcDoc} - onLoad={handleOnLoad} - /> - ); -}); + return ( + <iframe + className={styles["artifacts-iframe"]} + key={frameId} + ref={iframeRef} + sandbox="allow-forms allow-modals allow-scripts" + style={{ height }} + srcDoc={srcDoc} + onLoad={handleOnLoad} + /> + ); + }, +); export function ArtifactsShareButton({ getCode, @@ -203,7 +208,7 @@ export function Artifacts() { const [code, setCode] = useState(""); const [loading, setLoading] = useState(true); const [fileName, setFileName] = useState(""); - const previewRef = useRef<typeof HTMLPreview>(null); + const previewRef = useRef<HTMLPreviewHander>(null); useEffect(() => { if (id) { diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 717d40793..83fd5b8ab 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -14,7 +14,11 @@ import ReloadButtonIcon from "../icons/reload.svg"; import React from "react"; import { useDebouncedCallback } from "use-debounce"; import { showImageModal, FullScreen } from "./ui-lib"; -import { ArtifactsShareButton, HTMLPreview } from "./artifacts"; +import { + ArtifactsShareButton, + HTMLPreview, + HTMLPreviewHander, +} from "./artifacts"; import { Plugin } from "../constant"; import { useChatStore } from "../store"; import { IconButton } from "./button"; @@ -67,7 +71,7 @@ export function Mermaid(props: { code: string }) { export function PreCode(props: { children: any }) { const ref = useRef<HTMLPreElement>(null); - const previewRef = useRef<typeof HTMLPreview>(null); + const previewRef = useRef<HTMLPreviewHander>(null); const [mermaidCode, setMermaidCode] = useState(""); const [htmlCode, setHtmlCode] = useState(""); const { height } = useWindowSize();