|
|
import React, { useState, useEffect, useRef } from 'react'; |
|
|
import JSZip from 'jszip'; |
|
|
import CopyIcon from './icons/CopyIcon'; |
|
|
import DownloadIcon from './icons/DownloadIcon'; |
|
|
import { AppStatus, AppType } from '../types'; |
|
|
|
|
|
interface CodeDisplayProps { |
|
|
appType: AppType; |
|
|
pythonCode?: string; |
|
|
cppFiles?: Record<string, string> | null; |
|
|
status: AppStatus; |
|
|
} |
|
|
|
|
|
const PythonCodeView: React.FC<{ code: string }> = ({ code }) => { |
|
|
const [copyText, setCopyText] = useState('Copy Code'); |
|
|
const codeRef = useRef<HTMLElement>(null); |
|
|
const finalCode = code.replace(/\n```$/, '').trim(); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (window.hljs && codeRef.current) { |
|
|
|
|
|
window.hljs.highlightElement(codeRef.current); |
|
|
} |
|
|
}, [finalCode]); |
|
|
|
|
|
const handleCopy = () => { |
|
|
navigator.clipboard.writeText(finalCode).then(() => { |
|
|
setCopyText('Copied!'); |
|
|
setTimeout(() => setCopyText('Copy Code'), 2000); |
|
|
}); |
|
|
}; |
|
|
|
|
|
const handleDownload = () => { |
|
|
const blob = new Blob([finalCode], { type: 'text/python' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'generated_app.py'; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="relative h-full"> |
|
|
<div className="absolute top-4 right-4 flex items-center gap-2 z-10"> |
|
|
<button onClick={handleDownload} className="flex items-center gap-2 bg-gray-700 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition-all duration-200 text-sm font-semibold" aria-label="Download code"> |
|
|
<DownloadIcon className="w-5 h-5" /> |
|
|
Download |
|
|
</button> |
|
|
<button onClick={handleCopy} className="flex items-center gap-2 bg-gray-700 text-white py-2 px-4 rounded-lg hover:bg-gray-600 transition-all duration-200 text-sm font-semibold" aria-label="Copy code"> |
|
|
<CopyIcon className="w-5 h-5" /> |
|
|
{copyText} |
|
|
</button> |
|
|
</div> |
|
|
<pre className="h-full w-full overflow-auto p-6 pt-20"> |
|
|
<code ref={codeRef} className="language-python text-sm font-code whitespace-pre-wrap text-gray-300"> |
|
|
{finalCode} |
|
|
</code> |
|
|
</pre> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
type BuildSystem = 'makefile' | 'cmake' | 'meson'; |
|
|
|
|
|
const CppCodeView: React.FC<{ files: Record<string, string> }> = ({ files }) => { |
|
|
const [activeFile, setActiveFile] = useState(Object.keys(files)[0] || ''); |
|
|
const [buildSystem, setBuildSystem] = useState<BuildSystem>('cmake'); |
|
|
const [isDownloading, setIsDownloading] = useState(false); |
|
|
const codeRef = useRef<HTMLElement>(null); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (window.hljs && codeRef.current) { |
|
|
|
|
|
window.hljs.highlightElement(codeRef.current); |
|
|
} |
|
|
}, [activeFile, files]); |
|
|
|
|
|
const getBuildFileContent = (): { filename: string, content: string } => { |
|
|
const targetName = "MyApp"; |
|
|
const sources = Object.keys(files).filter(f => f.endsWith('.cpp')).join(' '); |
|
|
const headers = Object.keys(files).filter(f => f.endsWith('.h')).join(' '); |
|
|
|
|
|
switch(buildSystem) { |
|
|
case 'makefile': |
|
|
return { |
|
|
filename: 'Makefile', |
|
|
content: ` |
|
|
CXX = g++ |
|
|
CXXFLAGS = -std=c++17 -fPIC \`pkg-config --cflags Qt6Widgets Qt6Gui Qt6Core Qt6WebEngineWidgets\` |
|
|
LDFLAGS = \`pkg-config --libs Qt6Widgets Qt6Gui Qt6Core Qt6WebEngineWidgets\` |
|
|
TARGET = ${targetName} |
|
|
SOURCES = ${sources} |
|
|
OBJECTS = $(SOURCES:.cpp=.o) |
|
|
|
|
|
.PHONY: all clean |
|
|
|
|
|
all: $(TARGET) |
|
|
|
|
|
$(TARGET): $(OBJECTS) |
|
|
\t$(CXX) $(OBJECTS) -o $(TARGET) $(LDFLAGS) |
|
|
|
|
|
%.o: %.cpp ${headers} |
|
|
\t$(CXX) $(CXXFLAGS) -c $< -o $@ |
|
|
|
|
|
clean: |
|
|
\trm -f $(OBJECTS) $(TARGET) |
|
|
`.trim() |
|
|
}; |
|
|
case 'cmake': |
|
|
return { |
|
|
filename: 'CMakeLists.txt', |
|
|
content: ` |
|
|
cmake_minimum_required(VERSION 3.16) |
|
|
project(${targetName} VERSION 1.0) |
|
|
|
|
|
set(CMAKE_CXX_STANDARD 17) |
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON) |
|
|
set(CMAKE_AUTOMOC ON) |
|
|
set(CMAKE_AUTORCC ON) |
|
|
set(CMAKE_AUTOUIC ON) |
|
|
|
|
|
find_package(Qt6 COMPONENTS Widgets Gui Core WebEngineWidgets REQUIRED) |
|
|
|
|
|
add_executable(\${PROJECT_NAME} |
|
|
${sources.split(' ').join('\n ')} |
|
|
${headers.split(' ').join('\n ')} |
|
|
) |
|
|
|
|
|
target_link_libraries(\${PROJECT_NAME} PRIVATE Qt6::Widgets Qt6::Gui Qt6::Core Qt6::WebEngineWidgets) |
|
|
`.trim() |
|
|
}; |
|
|
case 'meson': |
|
|
return { |
|
|
filename: 'meson.build', |
|
|
content: ` |
|
|
project('${targetName}', 'cpp', version : '1.0', default_options : ['cpp_std=c++17']) |
|
|
|
|
|
qt6 = dependency('qt6', modules: ['Core', 'Gui', 'Widgets', 'WebEngineWidgets'], required: true) |
|
|
|
|
|
executable( |
|
|
'${targetName}', |
|
|
files(${sources.split(' ').map(s=>`'${s}'`).join(', ')}), |
|
|
dependencies: [qt6], |
|
|
install: true |
|
|
) |
|
|
`.trim() |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
const handleDownloadZip = async () => { |
|
|
setIsDownloading(true); |
|
|
const zip = new JSZip(); |
|
|
|
|
|
Object.entries(files).forEach(([name, content]) => { |
|
|
zip.file(name, content); |
|
|
}); |
|
|
|
|
|
const buildFile = getBuildFileContent(); |
|
|
zip.file(buildFile.filename, buildFile.content); |
|
|
|
|
|
try { |
|
|
const content = await zip.generateAsync({type:"blob"}); |
|
|
const url = URL.createObjectURL(content); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'qt_app.zip'; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
URL.revokeObjectURL(url); |
|
|
} catch (e) { |
|
|
console.error("Failed to generate ZIP", e); |
|
|
} finally { |
|
|
setIsDownloading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
if (!activeFile) return <div className="p-4 text-gray-400">No files were generated.</div> |
|
|
|
|
|
return ( |
|
|
<div className="h-full flex flex-col"> |
|
|
<div className="flex-shrink-0 flex items-center justify-between border-b border-gray-700 p-2 gap-2 flex-wrap"> |
|
|
<div className="flex items-center gap-1 flex-wrap"> |
|
|
{Object.keys(files).map(name => ( |
|
|
<button key={name} onClick={() => setActiveFile(name)} className={`px-3 py-2 text-sm rounded-md transition-colors ${activeFile === name ? 'bg-blue-600 text-white font-semibold' : 'bg-gray-800 hover:bg-gray-700 text-gray-300'}`}> |
|
|
{name} |
|
|
</button> |
|
|
))} |
|
|
</div> |
|
|
</div> |
|
|
<div className="flex-shrink-0 flex items-center justify-end border-b border-gray-700 p-2 gap-2 flex-wrap bg-gray-800/50"> |
|
|
<div className="flex items-center gap-2"> |
|
|
<label htmlFor="build-system" className="text-sm font-medium text-gray-300">Build System:</label> |
|
|
<select id="build-system" value={buildSystem} onChange={e => setBuildSystem(e.target.value as BuildSystem)} className="bg-gray-700 text-white py-2 px-3 rounded-lg text-sm font-semibold border-0 focus:ring-2 focus:ring-blue-500"> |
|
|
<option value="cmake">CMake</option> |
|
|
<option value="makefile">Makefile</option> |
|
|
<option value="meson">Meson</option> |
|
|
</select> |
|
|
</div> |
|
|
<button onClick={handleDownloadZip} disabled={isDownloading} className="flex items-center gap-2 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-500 transition-all duration-200 text-sm font-semibold disabled:bg-gray-500"> |
|
|
<DownloadIcon className="w-5 h-5" /> |
|
|
{isDownloading ? 'Zipping...' : 'Download ZIP'} |
|
|
</button> |
|
|
</div> |
|
|
<div className="relative flex-grow overflow-hidden"> |
|
|
<pre className="h-full w-full overflow-auto p-6"> |
|
|
<code ref={codeRef} className="language-cpp text-sm font-code whitespace-pre-wrap text-gray-300"> |
|
|
{files[activeFile]} |
|
|
</code> |
|
|
</pre> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
|
|
|
const CodeDisplay: React.FC<CodeDisplayProps> = ({ appType, pythonCode, cppFiles, status }) => { |
|
|
if (status !== AppStatus.SUCCESS) { |
|
|
return <div className="p-4">Generating...</div>; |
|
|
} |
|
|
|
|
|
return ( |
|
|
<div className="h-full flex flex-col bg-gray-900 rounded-b-2xl"> |
|
|
{appType === 'python' && pythonCode ? ( |
|
|
<PythonCodeView code={pythonCode} /> |
|
|
) : appType === 'c++' && cppFiles ? ( |
|
|
<CppCodeView files={cppFiles} /> |
|
|
) : ( |
|
|
<div className="p-4 text-gray-400">Code generation complete, but no output received.</div> |
|
|
)} |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default CodeDisplay; |