/** * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import React, { useState } from 'react'; import { Code2, Play, Copy, Check, MessageCircle } from 'lucide-react'; import Editor from '@monaco-editor/react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import ToggleButton from './ToggleButton'; const CodePreview = ({ output, onCodeChange, fullResponse }) => { const [showCode, setShowCode] = useState(false); const [showReasoning, setShowReasoning] = useState(false); const [isCopied, setIsCopied] = useState(false); const handleCopy = async () => { try { await navigator.clipboard.writeText(output.code); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); } catch (err) { console.error('Failed to copy code:', err); } }; const renderSketch = (code) => { // Make sure we're working with a string const codeString = typeof code === 'string' ? code : code.toString(); const wrappedCode = codeString.includes('function setup()') ? codeString : ` function setup() { createCanvas(500, 500); ${codeString} } function draw() { // Add default draw function if not present if (typeof window.draw !== 'function') { window.draw = function() {}; } } `; const formattedCodeResponse = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> <title>p5.js Sketch</title> <style> body { padding: 0; margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; overflow: hidden; } canvas { max-width: 100% !important; height: auto !important; } </style> </head> <body> <script> try { ${wrappedCode} // Immediately call setup if it hasn't been called if (typeof window.setup === 'function') { new p5(); } } catch (error) { console.error('Sketch error:', error); document.body.innerHTML = '<div style="color: red; padding: 20px;"><h3>🔴 Error:</h3><pre>' + error.message + '</pre></div>'; } </script> </body> </html> `; return ( <div className="relative w-full h-[500px] bg-gray-50 rounded-lg overflow-hidden"> <iframe srcDoc={formattedCodeResponse} title="p5.js Sketch" width="100%" height="100%" style={{ border: "none" }} className="absolute inset-0" /> </div> ); }; // Make sure we're passing the actual code string to renderSketch const sketchCode = output?.code || ''; return ( <div className="mb-4 p-6 rounded-3xl bg-gray-100"> <div className="mb-4"> {showCode ? ( <div className="w-full h-[500px] rounded-lg overflow-hidden border"> <Editor height="500px" defaultLanguage="javascript" value={sketchCode} onChange={(value) => onCodeChange(output.id, value)} theme="light" options={{ minimap: { enabled: false }, fontSize: 12, lineNumbers: 'off', scrollBeyondLastLine: false, automaticLayout: true, tabSize: 2, wordWrap: 'on', padding: { top: 8, bottom: 8 } }} /> </div> ) : showReasoning ? ( <div className="w-full h-[500px] rounded-lg overflow-y-auto border p-4 prose prose-xs max-w-none bg-white"> <ReactMarkdown remarkPlugins={[remarkGfm]} className="text-xs text-gray-700" components={{ code: ({node, inline, className, children, ...props}) => ( <code className={`${className} ${inline ? 'text-[0.7rem] bg-slate-50 text-slate-900 px-1.5 py-0.5 rounded border border-slate-200' : ''}`} {...props}> {children} </code> ), pre: ({node, children, ...props}) => ( <pre className="text-[0.7rem] bg-slate-50 text-slate-900 p-3 rounded-md overflow-x-auto border border-slate-200" {...props}> {children} </pre> ), p: ({node, children}) => ( <p className="text-xs mb-2 text-gray-700">{children}</p> ), h1: ({node, children}) => ( <h1 className="text-sm font-bold mb-2 text-gray-900">{children}</h1> ), h2: ({node, children}) => ( <h2 className="text-xs font-bold mb-2 text-gray-900">{children}</h2> ), h3: ({node, children}) => ( <h3 className="text-xs font-semibold mb-1 text-gray-900">{children}</h3> ), ul: ({node, children}) => ( <ul className="text-xs list-disc pl-4 mb-2 text-gray-700">{children}</ul> ), ol: ({node, children}) => ( <ol className="text-xs list-decimal pl-4 mb-2 text-gray-700">{children}</ol> ), li: ({node, children}) => ( <li className="text-xs mb-1 text-gray-700">{children}</li> ) }} > {fullResponse} </ReactMarkdown> </div> ) : ( renderSketch(sketchCode) )} </div> <div className="flex flex-col sm:flex-row justify-between items-center gap-3 mt-2"> <div className="inline-flex rounded-full bg-gray-200 gap-1 w-full sm:w-auto justify-center"> <ToggleButton icon={Play} label="Preview" isSelected={!showCode && !showReasoning} onClick={() => { setShowCode(false); setShowReasoning(false); }} /> <ToggleButton icon={MessageCircle} label="Reasoning" isSelected={showReasoning} onClick={() => { setShowCode(false); setShowReasoning(true); }} /> <ToggleButton icon={Code2} label="Code" isSelected={showCode} onClick={() => { setShowCode(true); setShowReasoning(false); }} /> </div> <button type="button" onClick={handleCopy} className={`px-3.5 py-2.5 rounded-full transition-colors inline-flex text-sm border border-gray-300 items-center gap-1 w-full sm:w-auto justify-center ${ isCopied ? "bg-gray-500 text-white" : "bg-transparent text-gray-700 hover:bg-gray-100" }`} > {isCopied ? ( <> <Check size={14} /> Copied! </> ) : ( <> <Copy size={14} /> Copy Code </> )} </button> </div> </div> ); }; export default CodePreview;