@@ -276,6 +276,110 @@ class SandboxApiClient {
276276 } ) ;
277277 }
278278
279+ async readFileStream ( path : string ) : Promise < {
280+ path : string ;
281+ mimeType : string ;
282+ size : number ;
283+ isBinary : boolean ;
284+ encoding : string ;
285+ content : string ;
286+ } > {
287+ const response = await fetch ( `${ this . baseUrl } /api/read/stream` , {
288+ method : "POST" ,
289+ headers : {
290+ "Content-Type" : "application/json" ,
291+ "X-Sandbox-Client-Id" : this . sandboxId ,
292+ } ,
293+ body : JSON . stringify ( { path } ) ,
294+ } ) ;
295+
296+ if ( ! response . ok ) {
297+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
298+ }
299+
300+ // Parse SSE stream with proper buffering to handle chunk splitting
301+ const reader = response . body ?. getReader ( ) ;
302+ if ( ! reader ) {
303+ throw new Error ( "No response body" ) ;
304+ }
305+
306+ const decoder = new TextDecoder ( ) ;
307+ let metadata : any = null ;
308+ let content = "" ;
309+ let buffer = "" ; // Buffer for incomplete lines
310+
311+ try {
312+ while ( true ) {
313+ const { done, value } = await reader . read ( ) ;
314+
315+ if ( value ) {
316+ // Add new data to buffer
317+ buffer += decoder . decode ( value , { stream : true } ) ;
318+ }
319+
320+ if ( done ) break ;
321+
322+ // Process complete lines from buffer
323+ let newlineIndex = buffer . indexOf ( "\n" ) ;
324+ while ( newlineIndex !== - 1 ) {
325+ const line = buffer . slice ( 0 , newlineIndex ) . trim ( ) ;
326+ buffer = buffer . slice ( newlineIndex + 1 ) ;
327+
328+ if ( line . startsWith ( "data: " ) ) {
329+ try {
330+ const data = JSON . parse ( line . slice ( 6 ) ) ;
331+
332+ if ( data . type === "metadata" ) {
333+ metadata = data ;
334+ } else if ( data . type === "chunk" ) {
335+ content += data . data ;
336+ } else if ( data . type === "complete" ) {
337+ return {
338+ path,
339+ mimeType : metadata ?. mimeType || "unknown" ,
340+ size : metadata ?. size || 0 ,
341+ isBinary : metadata ?. isBinary || false ,
342+ encoding : metadata ?. encoding || "utf-8" ,
343+ content,
344+ } ;
345+ } else if ( data . type === "error" ) {
346+ throw new Error ( data . error ) ;
347+ }
348+ } catch ( parseError ) {
349+ console . error ( "Failed to parse SSE line:" , line . substring ( 0 , 100 ) , parseError ) ;
350+ // Skip malformed lines
351+ }
352+ }
353+
354+ newlineIndex = buffer . indexOf ( "\n" ) ;
355+ }
356+ }
357+
358+ // Process any remaining data in buffer
359+ if ( buffer . trim ( ) . startsWith ( "data: " ) ) {
360+ try {
361+ const data = JSON . parse ( buffer . trim ( ) . slice ( 6 ) ) ;
362+ if ( data . type === "complete" ) {
363+ return {
364+ path,
365+ mimeType : metadata ?. mimeType || "unknown" ,
366+ size : metadata ?. size || 0 ,
367+ isBinary : metadata ?. isBinary || false ,
368+ encoding : metadata ?. encoding || "utf-8" ,
369+ content,
370+ } ;
371+ }
372+ } catch ( e ) {
373+ // Ignore final buffer parsing errors
374+ }
375+ }
376+ } finally {
377+ reader . releaseLock ( ) ;
378+ }
379+
380+ throw new Error ( "Stream ended unexpectedly" ) ;
381+ }
382+
279383 async deleteFile ( path : string ) {
280384 return this . doFetch ( "/api/delete" , {
281385 method : "POST" ,
@@ -321,6 +425,12 @@ class SandboxApiClient {
321425 } ) ;
322426 }
323427
428+ async createTestBinaryFile ( ) {
429+ return this . doFetch ( "/api/create-test-binary" , {
430+ method : "POST" ,
431+ } ) ;
432+ }
433+
324434 async setupNextjs ( projectName ?: string ) {
325435 return this . doFetch ( "/api/templates/nextjs" , {
326436 method : "POST" ,
@@ -1455,6 +1565,20 @@ function FilesTab({
14551565 const [ gitBranch , setGitBranch ] = useState ( "main" ) ;
14561566 const [ gitTargetDir , setGitTargetDir ] = useState ( "" ) ;
14571567
1568+ // Binary File Support
1569+ const [ binaryFilePath , setBinaryFilePath ] = useState ( "/workspace/demo-chart.png" ) ;
1570+ const [ binaryFileMetadata , setBinaryFileMetadata ] = useState < {
1571+ path : string ;
1572+ mimeType : string ;
1573+ size : number ;
1574+ isBinary : boolean ;
1575+ encoding : string ;
1576+ content ?: string ;
1577+ } | null > ( null ) ;
1578+ const [ isCreatingBinary , setIsCreatingBinary ] = useState ( false ) ;
1579+ const [ isReadingBinary , setIsReadingBinary ] = useState ( false ) ;
1580+ const [ useStreaming , setUseStreaming ] = useState ( false ) ;
1581+
14581582 const addResult = ( type : "success" | "error" , message : string ) => {
14591583 setResults ( ( prev ) => [ ...prev , { type, message, timestamp : new Date ( ) } ] ) ;
14601584 } ;
@@ -1608,6 +1732,47 @@ function FilesTab({
16081732 }
16091733 } ;
16101734
1735+ const handleCreateTestBinary = async ( ) => {
1736+ if ( ! client ) return ;
1737+ setIsCreatingBinary ( true ) ;
1738+ try {
1739+ const result = await client . createTestBinaryFile ( ) ;
1740+ addResult ( "success" , `Created test PNG: ${ result . path } ` ) ;
1741+ setBinaryFilePath ( result . path ) ;
1742+ // Clear any existing metadata to show fresh state
1743+ setBinaryFileMetadata ( null ) ;
1744+ } catch ( error : any ) {
1745+ addResult ( "error" , `Failed to create test binary: ${ error . message } ` ) ;
1746+ } finally {
1747+ setIsCreatingBinary ( false ) ;
1748+ }
1749+ } ;
1750+
1751+ const handleReadBinaryFile = async ( ) => {
1752+ if ( ! client || ! binaryFilePath . trim ( ) ) return ;
1753+ setIsReadingBinary ( true ) ;
1754+ try {
1755+ const result = useStreaming
1756+ ? await client . readFileStream ( binaryFilePath )
1757+ : await client . readFile ( binaryFilePath ) ;
1758+
1759+ setBinaryFileMetadata ( {
1760+ path : result . path ,
1761+ mimeType : result . mimeType || "unknown" ,
1762+ size : result . size || 0 ,
1763+ isBinary : result . isBinary || false ,
1764+ encoding : result . encoding || "utf-8" ,
1765+ content : result . content ,
1766+ } ) ;
1767+ addResult ( "success" , `Read binary file with metadata${ useStreaming ? ' (streamed)' : '' } : ${ binaryFilePath } ` ) ;
1768+ } catch ( error : any ) {
1769+ addResult ( "error" , `Failed to read binary file: ${ error . message } ` ) ;
1770+ setBinaryFileMetadata ( null ) ;
1771+ } finally {
1772+ setIsReadingBinary ( false ) ;
1773+ }
1774+ } ;
1775+
16111776 return (
16121777 < div className = "files-tab" >
16131778 < div className = "files-section" >
@@ -2052,6 +2217,133 @@ function FilesTab({
20522217 </ div >
20532218 </ div >
20542219
2220+ { /* Binary File Support Showcase */ }
2221+ < div className = "binary-showcase-section" >
2222+ < h2 > 🎨 Binary File Support Demo</ h2 >
2223+ < p className = "section-description" >
2224+ Test the new binary file reading capabilities with automatic format detection and metadata extraction.
2225+ </ p >
2226+
2227+ < div className = "operation-group" >
2228+ < h3 > Step 1: Create Test Binary File</ h3 >
2229+ < p className = "help-text" > Generate a PNG chart using matplotlib in the sandbox</ p >
2230+ < button
2231+ onClick = { handleCreateTestBinary }
2232+ disabled = { isCreatingBinary || connectionStatus !== "connected" }
2233+ className = "action-button create-binary"
2234+ >
2235+ { isCreatingBinary ? "Creating..." : "🎨 Create Test PNG Chart" }
2236+ </ button >
2237+ </ div >
2238+
2239+ < div className = "operation-group" >
2240+ < h3 > Step 2: Read Binary File with Metadata</ h3 >
2241+ < div className = "streaming-toggle" >
2242+ < label className = "toggle-label" >
2243+ < input
2244+ type = "checkbox"
2245+ checked = { useStreaming }
2246+ onChange = { ( e ) => setUseStreaming ( e . target . checked ) }
2247+ className = "toggle-checkbox"
2248+ />
2249+ < span className = "toggle-text" >
2250+ Use streaming (readFileStream)
2251+ </ span >
2252+ </ label >
2253+ < p className = "help-text" >
2254+ { useStreaming
2255+ ? "📡 Streams file in chunks via SSE - better for large files"
2256+ : "📄 Reads entire file at once - simpler but loads all into memory" }
2257+ </ p >
2258+ </ div >
2259+ < div className = "input-group" >
2260+ < input
2261+ type = "text"
2262+ placeholder = "Binary file path"
2263+ value = { binaryFilePath }
2264+ onChange = { ( e ) => setBinaryFilePath ( e . target . value ) }
2265+ className = "file-input"
2266+ />
2267+ < button
2268+ onClick = { handleReadBinaryFile }
2269+ disabled = { ! binaryFilePath . trim ( ) || isReadingBinary || connectionStatus !== "connected" }
2270+ className = "action-button"
2271+ >
2272+ { isReadingBinary ? "Reading..." : "📖 Read & Display" }
2273+ </ button >
2274+ </ div >
2275+ </ div >
2276+
2277+ { /* File Metadata Display */ }
2278+ { binaryFileMetadata && (
2279+ < div className = "binary-metadata-card" >
2280+ < h4 > 📊 File Metadata</ h4 >
2281+ < div className = "metadata-grid" >
2282+ < div className = "metadata-item" >
2283+ < span className = "metadata-label" > File Type:</ span >
2284+ < span className = "metadata-value" >
2285+ { binaryFileMetadata . isBinary ? "🖼️" : "📄" } { binaryFileMetadata . mimeType }
2286+ </ span >
2287+ </ div >
2288+ < div className = "metadata-item" >
2289+ < span className = "metadata-label" > Size:</ span >
2290+ < span className = "metadata-value" >
2291+ { ( binaryFileMetadata . size / 1024 ) . toFixed ( 2 ) } KB
2292+ </ span >
2293+ </ div >
2294+ < div className = "metadata-item" >
2295+ < span className = "metadata-label" > Encoding:</ span >
2296+ < span className = "metadata-value metadata-encoding" >
2297+ { binaryFileMetadata . encoding }
2298+ </ span >
2299+ </ div >
2300+ < div className = "metadata-item" >
2301+ < span className = "metadata-label" > Binary:</ span >
2302+ < span className = { `metadata-value ${ binaryFileMetadata . isBinary ? "binary-yes" : "binary-no" } ` } >
2303+ { binaryFileMetadata . isBinary ? "✓ Yes" : "✗ No" }
2304+ </ span >
2305+ </ div >
2306+ </ div >
2307+
2308+ { /* File Preview */ }
2309+ < div className = "file-preview" >
2310+ < h4 > 🔍 Preview</ h4 >
2311+ { binaryFileMetadata . isBinary && binaryFileMetadata . mimeType . startsWith ( "image/" ) ? (
2312+ < div className = "image-preview" >
2313+ < img
2314+ src = { `data:${ binaryFileMetadata . mimeType } ;base64,${ binaryFileMetadata . content } ` }
2315+ alt = "Binary file preview"
2316+ className = "preview-image"
2317+ />
2318+ < p className = "preview-caption" >
2319+ ✅ Binary file successfully read and decoded from base64!
2320+ </ p >
2321+ </ div >
2322+ ) : binaryFileMetadata . isBinary ? (
2323+ < div className = "binary-preview" >
2324+ < p className = "binary-info" >
2325+ 📦 Binary file ({ binaryFileMetadata . mimeType } )
2326+ </ p >
2327+ < pre className = "base64-preview" >
2328+ { binaryFileMetadata . content ?. substring ( 0 , 200 ) } ...
2329+ </ pre >
2330+ < p className = "preview-caption" >
2331+ Base64 encoded content (first 200 chars shown)
2332+ </ p >
2333+ </ div >
2334+ ) : (
2335+ < div className = "text-preview" >
2336+ < pre className = "code-block" >
2337+ { binaryFileMetadata . content }
2338+ </ pre >
2339+ < p className = "preview-caption" > Text file content</ p >
2340+ </ div >
2341+ ) }
2342+ </ div >
2343+ </ div >
2344+ ) }
2345+ </ div >
2346+
20552347 { /* Results */ }
20562348 < div className = "results-section" >
20572349 < h3 > Operation Results</ h3 >
0 commit comments