Spaces:
Sleeping
Sleeping
| import React from 'react'; | |
| import { Handle, Position } from 'reactflow'; | |
| import './OutputNode.css'; | |
| const OutputNode = ({ data, isConnectable }) => { | |
| const { connectedType, label, result } = data; | |
| // Log the props received by the component for debugging | |
| console.log(`[OutputNode: ${label}] Received data:`, data); | |
| // Function to download the output content | |
| const downloadOutput = () => { | |
| if (!result) return; | |
| let filename = `${label || 'output'}-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}`; | |
| let blob, url, content; | |
| switch (connectedType) { | |
| case 'textbox': | |
| case 'markdown': | |
| case 'json': | |
| // For text/JSON content | |
| content = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result); | |
| blob = new Blob([content], { type: 'text/plain' }); | |
| filename = `${filename}.txt`; | |
| break; | |
| case 'image': | |
| // For image content - handle if it's a URL | |
| let imageUrl = ''; | |
| if (typeof result === 'string') { | |
| imageUrl = result; | |
| } else if (Array.isArray(result) && typeof result[0] === 'string') { | |
| imageUrl = result[0]; | |
| } else if (typeof result === 'object' && result !== null && result.url) { | |
| imageUrl = result.url; | |
| } | |
| if (imageUrl) { | |
| // For a URL, open in a new tab for direct download | |
| window.open(imageUrl, '_blank'); | |
| return; | |
| } | |
| return; // Can't download if no URL | |
| case 'audio': | |
| case 'video': | |
| case 'file': | |
| // For file content - handle if it's a URL | |
| let fileUrl = ''; | |
| if (typeof result === 'string') { | |
| fileUrl = result; | |
| } else if (Array.isArray(result) && typeof result[0] === 'string') { | |
| fileUrl = result[0]; | |
| } else if (typeof result === 'object' && result !== null && result.url) { | |
| fileUrl = result.url; | |
| } | |
| if (fileUrl) { | |
| // For a URL, open in a new tab for direct download | |
| window.open(fileUrl, '_blank'); | |
| return; | |
| } | |
| return; // Can't download if no URL | |
| default: | |
| // Default case - try to stringify any content | |
| content = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result); | |
| blob = new Blob([content], { type: 'text/plain' }); | |
| filename = `${filename}.txt`; | |
| } | |
| // Create download link and trigger it | |
| if (blob) { | |
| url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| // Clean up | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| } | |
| }; | |
| const renderContent = () => { | |
| // Use a more robust check for the existence of a result. | |
| // Checks for null, undefined. Allows 0, false, and empty strings to be displayed. | |
| const hasResult = result !== null && result !== undefined; | |
| if (!hasResult) { | |
| console.log(`[OutputNode: ${label}] No result found. Rendering placeholder.`); | |
| switch (connectedType) { | |
| case 'textbox': | |
| return <div className="output-content placeholder"><p>Text output will appear here.</p></div>; | |
| case 'image': | |
| return <div className="output-content placeholder"><p>Image output will appear here.</p></div>; | |
| case 'audio': | |
| case 'video': | |
| case 'file': | |
| return <div className="output-content placeholder"><p>File output will appear here.</p></div>; | |
| default: | |
| return <div className="output-content placeholder"><p>Connect an output to see the result.</p></div>; | |
| } | |
| } | |
| console.log(`[OutputNode: ${label}] Result found. Rendering content for type: ${connectedType}`); | |
| // --- Robust Result Rendering --- | |
| let content; | |
| if (typeof result === 'object' && result !== null) { | |
| // If the result is an object or array, display it as a formatted JSON string. | |
| // This prevents React from crashing on "Objects are not valid as a React child". | |
| content = <pre>{JSON.stringify(result, null, 2)}</pre>; | |
| } else { | |
| // Otherwise, display the primitive value directly. | |
| content = <p>{String(result)}</p>; | |
| } | |
| switch (connectedType) { | |
| case 'textbox': | |
| case 'markdown': // Handle markdown as text for now | |
| case 'json': | |
| return <div className="output-content text-output">{content}</div>; | |
| case 'image': | |
| // Handle results that are strings, objects with a URL, or arrays containing a URL. | |
| let imageUrl = ''; | |
| if (typeof result === 'string') { | |
| imageUrl = result; | |
| } else if (Array.isArray(result) && typeof result[0] === 'string') { | |
| imageUrl = result[0]; | |
| } else if (typeof result === 'object' && result !== null && result.url) { | |
| imageUrl = result.url; | |
| } | |
| return <div className="output-content image-output"><img src={imageUrl} alt="Generated" style={{ maxWidth: '100%', maxHeight: '100%' }} /></div>; | |
| case 'audio': | |
| case 'video': | |
| case 'file': | |
| // Handle results that are strings, objects with a URL, or arrays containing a URL. | |
| let fileUrl = ''; | |
| if (typeof result === 'string') { | |
| fileUrl = result; | |
| } else if (Array.isArray(result) && typeof result[0] === 'string') { | |
| fileUrl = result[0]; | |
| } else if (typeof result === 'object' && result !== null && result.url) { | |
| fileUrl = result.url; | |
| } | |
| return ( | |
| <div className="output-content audio-output"> | |
| <p>File output:</p> | |
| <a href={fileUrl} target="_blank" rel="noopener noreferrer">{fileUrl}</a> | |
| {connectedType === 'audio' && <audio controls src={fileUrl} style={{ width: '100%', marginTop: '10px' }} />} | |
| {connectedType === 'video' && <video controls src={fileUrl} style={{ width: '100%', marginTop: '10px' }} />} | |
| </div> | |
| ); | |
| default: | |
| console.log(`[OutputNode: ${label}] Cannot determine how to render type '${connectedType}'. Displaying raw data.`); | |
| return <div className="output-content text-output">{content}</div>; | |
| } | |
| }; | |
| // Download button icon (simple SVG) | |
| const DownloadIcon = () => ( | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <path d="M12 16L12 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/> | |
| <path d="M9 13L12 16L15 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/> | |
| <path d="M3 20H21" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/> | |
| </svg> | |
| ); | |
| // Get the output type for the handle | |
| const getOutputDataType = () => { | |
| switch (connectedType) { | |
| case 'textbox': | |
| case 'markdown': | |
| return 'string'; | |
| case 'json': | |
| return 'object'; | |
| case 'image': | |
| return 'image'; | |
| case 'audio': | |
| return 'audio'; | |
| case 'video': | |
| return 'video'; | |
| case 'file': | |
| return 'file'; | |
| default: | |
| return 'any'; | |
| } | |
| }; | |
| return ( | |
| <div className={`output-node ${data.status || ''} ${data.isSource ? 'is-source' : ''}`}> | |
| <Handle | |
| type="target" | |
| position={Position.Left} | |
| id="target-any" | |
| isConnectable={isConnectable} | |
| /> | |
| <div className="output-node-header"> | |
| <strong>{label || 'Output'}</strong> | |
| {data.status && <span className="node-status">{data.status}</span>} | |
| {data.isSource && <span className="source-indicator">→ Source</span>} | |
| </div> | |
| {renderContent()} | |
| {result !== null && result !== undefined && ( | |
| <div className="output-actions"> | |
| <button | |
| className="download-button" | |
| onClick={downloadOutput} | |
| title="Download this output" | |
| > | |
| <DownloadIcon /> Download | |
| </button> | |
| </div> | |
| )} | |
| <Handle | |
| type="source" | |
| position={Position.Right} | |
| id="source-result" | |
| isConnectable={isConnectable} | |
| data-type={getOutputDataType()} | |
| /> | |
| </div> | |
| ); | |
| }; | |
| export default OutputNode; |