1- import { useEffect , useState , useRef , useMemo } from "react" ;
1+ import {
2+ useEffect ,
3+ useState ,
4+ useRef ,
5+ useMemo ,
6+ forwardRef ,
7+ useImperativeHandle ,
8+ } from "react" ;
29import { useParams } from "react-router" ;
310import { useWindowSize } from "@/app/utils" ;
411import { IconButton } from "./button" ;
@@ -8,80 +15,97 @@ import CopyIcon from "../icons/copy.svg";
815import DownloadIcon from "../icons/download.svg" ;
916import GithubIcon from "../icons/github.svg" ;
1017import LoadingButtonIcon from "../icons/loading.svg" ;
18+ import ReloadButtonIcon from "../icons/reload.svg" ;
1119import Locale from "../locales" ;
1220import { Modal , showToast } from "./ui-lib" ;
1321import { copyToClipboard , downloadAs } from "../utils" ;
1422import { Path , ApiPath , REPO_URL } from "@/app/constant" ;
1523import { Loading } from "./home" ;
1624import styles from "./artifacts.module.scss" ;
1725
18- export function HTMLPreview ( props : {
26+ type HTMLPreviewProps = {
1927 code : string ;
2028 autoHeight ?: boolean ;
2129 height ?: number | string ;
2230 onLoad ?: ( title ?: string ) => void ;
23- } ) {
24- const ref = useRef < HTMLIFrameElement > ( null ) ;
25- const frameId = useRef < string > ( nanoid ( ) ) ;
26- const [ iframeHeight , setIframeHeight ] = useState ( 600 ) ;
27- const [ title , setTitle ] = useState ( "" ) ;
28- /*
29- * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
30- * 1. using srcdoc
31- * 2. using src with dataurl:
32- * easy to share
33- * length limit (Data URIs cannot be larger than 32,768 characters.)
34- */
31+ } ;
3532
36- useEffect ( ( ) => {
37- const handleMessage = ( e : any ) => {
38- const { id, height, title } = e . data ;
39- setTitle ( title ) ;
40- if ( id == frameId . current ) {
41- setIframeHeight ( height ) ;
42- }
43- } ;
44- window . addEventListener ( "message" , handleMessage ) ;
45- return ( ) => {
46- window . removeEventListener ( "message" , handleMessage ) ;
47- } ;
48- } , [ ] ) ;
33+ export type HTMLPreviewHander = {
34+ reload : ( ) => void ;
35+ } ;
4936
50- const height = useMemo ( ( ) => {
51- if ( ! props . autoHeight ) return props . height || 600 ;
52- if ( typeof props . height === "string" ) {
53- return props . height ;
54- }
55- const parentHeight = props . height || 600 ;
56- return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40 ;
57- } , [ props . autoHeight , props . height , iframeHeight ] ) ;
37+ export const HTMLPreview = forwardRef < HTMLPreviewHander , HTMLPreviewProps > (
38+ function HTMLPreview ( props , ref ) {
39+ const iframeRef = useRef < HTMLIFrameElement > ( null ) ;
40+ const [ frameId , setFrameId ] = useState < string > ( nanoid ( ) ) ;
41+ const [ iframeHeight , setIframeHeight ] = useState ( 600 ) ;
42+ const [ title , setTitle ] = useState ( "" ) ;
43+ /*
44+ * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
45+ * 1. using srcdoc
46+ * 2. using src with dataurl:
47+ * easy to share
48+ * length limit (Data URIs cannot be larger than 32,768 characters.)
49+ */
5850
59- const srcDoc = useMemo ( ( ) => {
60- const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${ frameId . current } ', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>` ;
61- if ( props . code . includes ( "</head>" ) ) {
62- props . code . replace ( "</head>" , "</head>" + script ) ;
63- }
64- return props . code + script ;
65- } , [ props . code ] ) ;
51+ useEffect ( ( ) => {
52+ const handleMessage = ( e : any ) => {
53+ const { id, height, title } = e . data ;
54+ setTitle ( title ) ;
55+ if ( id == frameId ) {
56+ setIframeHeight ( height ) ;
57+ }
58+ } ;
59+ window . addEventListener ( "message" , handleMessage ) ;
60+ return ( ) => {
61+ window . removeEventListener ( "message" , handleMessage ) ;
62+ } ;
63+ } , [ frameId ] ) ;
6664
67- const handleOnLoad = ( ) => {
68- if ( props ?. onLoad ) {
69- props . onLoad ( title ) ;
70- }
71- } ;
65+ useImperativeHandle ( ref , ( ) => ( {
66+ reload : ( ) => {
67+ setFrameId ( nanoid ( ) ) ;
68+ } ,
69+ } ) ) ;
7270
73- return (
74- < iframe
75- className = { styles [ "artifacts-iframe" ] }
76- id = { frameId . current }
77- ref = { ref }
78- sandbox = "allow-forms allow-modals allow-scripts"
79- style = { { height } }
80- srcDoc = { srcDoc }
81- onLoad = { handleOnLoad }
82- />
83- ) ;
84- }
71+ const height = useMemo ( ( ) => {
72+ if ( ! props . autoHeight ) return props . height || 600 ;
73+ if ( typeof props . height === "string" ) {
74+ return props . height ;
75+ }
76+ const parentHeight = props . height || 600 ;
77+ return iframeHeight + 40 > parentHeight
78+ ? parentHeight
79+ : iframeHeight + 40 ;
80+ } , [ props . autoHeight , props . height , iframeHeight ] ) ;
81+
82+ const srcDoc = useMemo ( ( ) => {
83+ const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${ frameId } ', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>` ;
84+ if ( props . code . includes ( "</head>" ) ) {
85+ props . code . replace ( "</head>" , "</head>" + script ) ;
86+ }
87+ return props . code + script ;
88+ } , [ props . code , frameId ] ) ;
89+
90+ const handleOnLoad = ( ) => {
91+ if ( props ?. onLoad ) {
92+ props . onLoad ( title ) ;
93+ }
94+ } ;
95+
96+ return (
97+ < iframe
98+ className = { styles [ "artifacts-iframe" ] }
99+ key = { frameId }
100+ ref = { iframeRef }
101+ sandbox = "allow-forms allow-modals allow-scripts"
102+ style = { { height } }
103+ srcDoc = { srcDoc }
104+ onLoad = { handleOnLoad }
105+ />
106+ ) ;
107+ } ,
108+ ) ;
85109
86110export function ArtifactsShareButton ( {
87111 getCode,
@@ -184,6 +208,7 @@ export function Artifacts() {
184208 const [ code , setCode ] = useState ( "" ) ;
185209 const [ loading , setLoading ] = useState ( true ) ;
186210 const [ fileName , setFileName ] = useState ( "" ) ;
211+ const previewRef = useRef < HTMLPreviewHander > ( null ) ;
187212
188213 useEffect ( ( ) => {
189214 if ( id ) {
@@ -208,6 +233,13 @@ export function Artifacts() {
208233 < a href = { REPO_URL } target = "_blank" rel = "noopener noreferrer" >
209234 < IconButton bordered icon = { < GithubIcon /> } shadow />
210235 </ a >
236+ < IconButton
237+ bordered
238+ style = { { marginLeft : 20 } }
239+ icon = { < ReloadButtonIcon /> }
240+ shadow
241+ onClick = { ( ) => previewRef . current ?. reload ( ) }
242+ />
211243 < div className = { styles [ "artifacts-title" ] } > NextChat Artifacts</ div >
212244 < ArtifactsShareButton
213245 id = { id }
@@ -220,6 +252,7 @@ export function Artifacts() {
220252 { code && (
221253 < HTMLPreview
222254 code = { code }
255+ ref = { previewRef }
223256 autoHeight = { false }
224257 height = { "100%" }
225258 onLoad = { ( title ) => {
0 commit comments