11"use client" ;
22
3- import { useState , useCallback , useEffect , useMemo , type ReactNode } from "react" ;
3+ import { useState , useCallback , useMemo , useRef , type ReactNode } from "react" ;
44import { useAgent } from "@copilotkit/react-core/v2" ;
55import { SEED_TEMPLATES } from "@/components/template-library/seed-templates" ;
66
@@ -35,29 +35,28 @@ export function SaveTemplateOverlay({
3535 const [ saveState , setSaveState ] = useState < SaveState > ( "idle" ) ;
3636 const [ templateName , setTemplateName ] = useState ( "" ) ;
3737
38- // Capture pending_template once — it may be cleared by the agent later.
39- // Syncs external agent state into local state (legitimate effect-based setState).
38+ // Capture pending_template at mount time — it may be cleared by the agent later.
39+ // Uses ref (not state) to avoid an async re-render that would shift sibling positions
40+ // and cause React to remount the iframe, losing rendered 3D/canvas content.
4041 const pending = agent . state ?. pending_template as { id : string ; name : string } | null | undefined ;
41- const [ capturedSource , setCapturedSource ] = useState < { id : string ; name : string } | null > ( null ) ;
42- useEffect ( ( ) => {
43- if ( pending ?. id ) {
44- // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time capture of external agent state
45- setCapturedSource ( ( prev ) => prev ?? pending ) ;
46- }
47- } , [ pending ] ) ;
42+ const sourceRef = useRef < { id : string ; name : string } | null > ( null ) ;
43+ // eslint-disable-next-line react-hooks/refs -- one-time ref init during render (React-endorsed pattern)
44+ if ( pending ?. id && ! sourceRef . current ) {
45+ sourceRef . current = pending ; // eslint-disable-line react-hooks/refs
46+ }
4847
4948 // Check if this content matches an existing template:
5049 // 1. Exact HTML match (seed templates rendered as-is)
5150 // 2. Source template captured from pending_template (applied templates with modified data)
5251 const matchedTemplate = useMemo ( ( ) => {
5352 // First check source template from apply flow
54- if ( capturedSource ) {
53+ if ( sourceRef . current ) { // eslint-disable-line react-hooks/refs
5554 const allTemplates = [
5655 ...SEED_TEMPLATES ,
5756 ...( ( agent . state ?. templates as { id : string ; name : string } [ ] ) || [ ] ) ,
5857 ] ;
59- const found = allTemplates . find ( ( t ) => t . id === capturedSource . id ) ;
60- if ( found ) return found ;
58+ const source = allTemplates . find ( ( t ) => t . id === sourceRef . current ! . id ) ; // eslint-disable-line react-hooks/refs
59+ if ( source ) return source ;
6160 }
6261 // Then check exact HTML match
6362 if ( ! html ) return null ;
@@ -68,7 +67,7 @@ export function SaveTemplateOverlay({
6867 ...( ( agent . state ?. templates as { id : string ; name : string ; html : string } [ ] ) || [ ] ) ,
6968 ] ;
7069 return allTemplates . find ( ( t ) => t . html && normalise ( t . html ) === norm ) ?? null ;
71- } , [ html , agent . state ?. templates , capturedSource ] ) ;
70+ } , [ html , agent . state ?. templates ] ) ;
7271
7372 const handleSave = useCallback ( ( ) => {
7473 const name = templateName . trim ( ) || title || "Untitled Template" ;
0 commit comments