@@ -19,8 +19,10 @@ import JsxParser from "react-jsx-parser";
1919interface JSXPreviewContextValue {
2020 jsx : string ;
2121 processedJsx : string ;
22+ isStreaming : boolean ;
2223 error : Error | null ;
2324 setError : ( error : Error | null ) => void ;
25+ setLastGoodJsx : ( jsx : string ) => void ;
2426 components : JsxParserProps [ "components" ] ;
2527 bindings : JsxParserProps [ "bindings" ] ;
2628 onErrorProp ?: ( error : Error ) => void ;
@@ -70,6 +72,22 @@ const matchJsxTag = (code: string) => {
7072 } ;
7173} ;
7274
75+ const stripIncompleteTag = ( text : string ) => {
76+ // Find the last '<' that isn't part of a complete tag
77+ const lastOpen = text . lastIndexOf ( "<" ) ;
78+ if ( lastOpen === - 1 ) {
79+ return text ;
80+ }
81+
82+ const afterOpen = text . slice ( lastOpen ) ;
83+ // If there's no closing '>' after the last '<', it's an incomplete tag
84+ if ( ! afterOpen . includes ( ">" ) ) {
85+ return text . slice ( 0 , lastOpen ) ;
86+ }
87+
88+ return text ;
89+ } ;
90+
7391const completeJsxTag = ( code : string ) => {
7492 const stack : string [ ] = [ ] ;
7593 let result = "" ;
@@ -78,8 +96,8 @@ const completeJsxTag = (code: string) => {
7896 while ( currentPosition < code . length ) {
7997 const match = matchJsxTag ( code . slice ( currentPosition ) ) ;
8098 if ( ! match ) {
81- // No more tags found, append remaining content
82- result += code . slice ( currentPosition ) ;
99+ // No more tags found, strip any trailing incomplete tag
100+ result += stripIncompleteTag ( code . slice ( currentPosition ) ) ;
83101 break ;
84102 }
85103 const { tagName, type, endIndex } = match ;
@@ -126,6 +144,7 @@ export const JSXPreview = memo(
126144 } : JSXPreviewProps ) => {
127145 const [ prevJsx , setPrevJsx ] = useState ( jsx ) ;
128146 const [ error , setError ] = useState < Error | null > ( null ) ;
147+ const [ lastGoodJsx , setLastGoodJsx ] = useState ( "" ) ;
129148
130149 // Clear error when jsx changes (derived state pattern)
131150 if ( jsx !== prevJsx ) {
@@ -143,12 +162,23 @@ export const JSXPreview = memo(
143162 bindings,
144163 components,
145164 error,
165+ isStreaming,
146166 jsx,
147167 onErrorProp : onError ,
148168 processedJsx,
149169 setError,
170+ setLastGoodJsx,
150171 } ) ,
151- [ bindings , components , error , jsx , onError , processedJsx , setError ]
172+ [
173+ bindings ,
174+ components ,
175+ error ,
176+ isStreaming ,
177+ jsx ,
178+ onError ,
179+ processedJsx ,
180+ setError ,
181+ ]
152182 ) ;
153183
154184 return (
@@ -167,14 +197,24 @@ export type JSXPreviewContentProps = Omit<ComponentProps<"div">, "children">;
167197
168198export const JSXPreviewContent = memo (
169199 ( { className, ...props } : JSXPreviewContentProps ) => {
170- const { processedJsx, components, bindings, setError, onErrorProp } =
171- useJSXPreview ( ) ;
200+ const {
201+ processedJsx,
202+ isStreaming,
203+ components,
204+ bindings,
205+ setError,
206+ setLastGoodJsx,
207+ onErrorProp,
208+ } = useJSXPreview ( ) ;
172209 const errorReportedRef = useRef < string | null > ( null ) ;
210+ const lastGoodJsxRef = useRef ( "" ) ;
211+ const [ hadError , setHadError ] = useState ( false ) ;
173212
174213 // Reset error tracking when jsx changes
175214 // biome-ignore lint/correctness/useExhaustiveDependencies: processedJsx change should reset tracking
176215 useEffect ( ( ) => {
177216 errorReportedRef . current = null ;
217+ setHadError ( false ) ;
178218 } , [ processedJsx ] ) ;
179219
180220 const handleError = useCallback (
@@ -184,18 +224,38 @@ export const JSXPreviewContent = memo(
184224 return ;
185225 }
186226 errorReportedRef . current = processedJsx ;
227+
228+ // During streaming, suppress errors and fall back to last good JSX
229+ if ( isStreaming ) {
230+ setHadError ( true ) ;
231+ return ;
232+ }
233+
187234 setError ( err ) ;
188235 onErrorProp ?.( err ) ;
189236 } ,
190- [ processedJsx , onErrorProp , setError ]
237+ [ processedJsx , isStreaming , onErrorProp , setError ]
191238 ) ;
192239
240+ // Track the last JSX that rendered without error
241+ // biome-ignore lint/correctness/useExhaustiveDependencies: update when processedJsx changes without error
242+ useEffect ( ( ) => {
243+ if ( ! errorReportedRef . current ) {
244+ lastGoodJsxRef . current = processedJsx ;
245+ setLastGoodJsx ( processedJsx ) ;
246+ }
247+ } , [ processedJsx , setLastGoodJsx ] ) ;
248+
249+ // During streaming, if the current JSX errored, re-render with last good version
250+ const displayJsx =
251+ isStreaming && hadError ? lastGoodJsxRef . current : processedJsx ;
252+
193253 return (
194254 < div className = { cn ( "jsx-preview-content" , className ) } { ...props } >
195255 < JsxParser
196256 bindings = { bindings }
197257 components = { components }
198- jsx = { processedJsx }
258+ jsx = { displayJsx }
199259 onError = { handleError }
200260 renderInWrapper = { false }
201261 />
0 commit comments