Spaces:
Running
Running
| /** | |
| * 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; |