suisuyy
Rename project back to xpaintai in package.json and metadata.json; add QuestionMarkIcon component; enhance AI features with ask functionality in useAiFeatures hook and App component
02ce812
| import React, { useState, useCallback } from 'react'; | |
| import Toolbar from './components/Toolbar'; | |
| import CanvasComponent from './components/CanvasComponent'; | |
| import AiEditModal from './components/AiEditModal'; | |
| import ZoomSlider from './components/ZoomSlider'; | |
| import SettingsPanel from './components/SettingsPanel'; | |
| import ConfirmModal from './components/ConfirmModal'; | |
| import { DEFAULT_AI_API_ENDPOINT } from './constants'; | |
| // Custom Hooks | |
| import { useToasts } from './hooks/useToasts'; | |
| import { useCanvasHistory } from './hooks/useCanvasHistory'; | |
| import { useAiFeatures, AiImageQuality, AiDimensionsMode } from './hooks/useAiFeatures'; | |
| import { useDrawingTools } from './hooks/useDrawingTools'; | |
| import { useFullscreen } from './hooks/useFullscreen'; | |
| import { useConfirmModal } from './hooks/useConfirmModal'; | |
| import { useCanvasFileUtils } from './hooks/useCanvasFileUtils'; | |
| const App: React.FC = () => { | |
| // Core UI States | |
| const [zoomLevel, setZoomLevel] = useState<number>(0.5); | |
| const [showSettingsPanel, setShowSettingsPanel] = useState<boolean>(false); | |
| // Settings specific states | |
| const [showZoomSlider, setShowZoomSlider] = useState<boolean>(true); | |
| const [aiImageQuality, setAiImageQuality] = useState<AiImageQuality>('medium'); | |
| const [aiApiEndpoint, setAiApiEndpoint] = useState<string>(DEFAULT_AI_API_ENDPOINT); | |
| const [aiDimensionsMode, setAiDimensionsMode] = useState<AiDimensionsMode>('match_canvas'); | |
| // Custom Hooks Instantiation | |
| const { toasts, showToast } = useToasts(); | |
| const { | |
| historyStack, | |
| currentHistoryIndex, | |
| canvasWidth, | |
| canvasHeight, | |
| isLoading: isCanvasLoading, | |
| currentDataURL, | |
| handleDrawEnd, | |
| handleUndo, | |
| handleRedo, | |
| updateCanvasState, | |
| } = useCanvasHistory(); | |
| const { | |
| penColor, | |
| setPenColor, | |
| penSize, | |
| setPenSize, | |
| isEraserMode, | |
| toggleEraserMode, | |
| effectivePenColor, | |
| } = useDrawingTools(); | |
| const { isFullscreenActive, toggleFullscreen } = useFullscreen(showToast); | |
| const { confirmModalInfo, requestConfirmation, closeConfirmModal } = useConfirmModal(); | |
| const { | |
| handleLoadImageFile, | |
| handleExportImage, | |
| handleClearCanvas, | |
| handleCanvasSizeChange, | |
| } = useCanvasFileUtils({ | |
| canvasState: { currentDataURL, canvasWidth, canvasHeight }, | |
| historyActions: { updateCanvasState }, | |
| uiActions: { showToast, setZoomLevel }, | |
| confirmModalActions: { requestConfirmation }, | |
| }); | |
| const { | |
| isMagicUploading, | |
| showAiEditModal, | |
| aiPrompt, | |
| isGeneratingAiImage, | |
| sharedImageUrlForAi, | |
| aiEditError, | |
| isAskingAi, | |
| askUrl, | |
| handleMagicUpload, | |
| handleGenerateAiImage, | |
| handleAskAi, | |
| handleCancelAiEdit, | |
| setAiPrompt, | |
| clearAskUrl, | |
| } = useAiFeatures({ | |
| currentDataURL, | |
| showToast, | |
| updateCanvasState, | |
| setZoomLevel, | |
| aiImageQuality, | |
| aiApiEndpoint, | |
| aiDimensionsMode, | |
| currentCanvasWidth: canvasWidth, // Pass current canvas dimensions | |
| currentCanvasHeight: canvasHeight, // Pass current canvas dimensions | |
| }); | |
| const handlePromptChange = (newPrompt: string) => { | |
| setAiPrompt(newPrompt); | |
| // Clear the previous "Ask" response when the user types a new prompt | |
| if (askUrl !== null) { | |
| clearAskUrl(); | |
| } | |
| }; | |
| // Simple UI Toggles | |
| const handleToggleSettingsPanel = () => setShowSettingsPanel(prev => !prev); | |
| const canShare = !!currentDataURL; | |
| // Loading State | |
| if (isCanvasLoading) { | |
| return ( | |
| <div className="flex justify-center items-center h-screen bg-gradient-to-br from-slate-200 to-sky-100"> | |
| <div className="text-center p-8 bg-white/50 backdrop-blur-md rounded-xl shadow-2xl"> | |
| <svg className="animate-spin h-12 w-12 text-blue-600 mx-auto mb-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> | |
| <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| <p className="text-2xl font-semibold text-slate-700">Loading Your Canvas...</p> | |
| <p className="text-sm text-slate-500 mt-1">Getting things ready!</p> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Main App Render | |
| return ( | |
| <div className="min-h-screen flex flex-col items-center bg-gradient-to-br from-slate-100 to-sky-200 font-sans"> | |
| <Toolbar | |
| penColor={penColor} | |
| onColorChange={setPenColor} | |
| penSize={penSize} | |
| onSizeChange={setPenSize} | |
| onUndo={handleUndo} | |
| canUndo={currentHistoryIndex > 0} | |
| onRedo={handleRedo} | |
| canRedo={currentHistoryIndex < historyStack.length - 1 && historyStack.length > 0} | |
| onLoadImage={handleLoadImageFile} | |
| onExportImage={handleExportImage} | |
| onClearCanvas={handleClearCanvas} | |
| isEraserMode={isEraserMode} | |
| onToggleEraser={toggleEraserMode} | |
| onMagicUpload={handleMagicUpload} | |
| isMagicUploading={isMagicUploading} | |
| canShare={canShare} | |
| onToggleSettings={handleToggleSettingsPanel} | |
| /> | |
| <main className="flex-grow grid place-items-center p-4 md:p-6 w-full mt-4 mb-4 overflow-auto"> | |
| <div | |
| style={{ | |
| transform: `scale(${zoomLevel})`, | |
| transformOrigin: 'top left', | |
| transition: 'transform 0.1s ease-out' | |
| }} | |
| aria-label={`Canvas zoomed to ${Math.round(zoomLevel * 100)}%`} | |
| > | |
| {currentDataURL ? ( | |
| <CanvasComponent | |
| key={`canvas-comp-${canvasWidth}-${canvasHeight}`} | |
| penColor={effectivePenColor} | |
| penSize={penSize} | |
| isEraserMode={isEraserMode} | |
| dataURLToLoad={currentDataURL} | |
| onDrawEnd={handleDrawEnd} | |
| canvasPhysicalWidth={canvasWidth} | |
| canvasPhysicalHeight={canvasHeight} | |
| zoomLevel={zoomLevel} | |
| /> | |
| ) : ( | |
| <div | |
| style={{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }} | |
| className="border-2 border-slate-300 rounded-lg shadow-xl bg-white flex justify-center items-center" | |
| > | |
| <p className="text-slate-500">Initializing Canvas...</p> | |
| </div> | |
| )} | |
| </div> | |
| </main> | |
| {showAiEditModal && sharedImageUrlForAi && ( | |
| <AiEditModal | |
| isOpen={showAiEditModal} | |
| onClose={handleCancelAiEdit} | |
| onGenerate={handleGenerateAiImage} | |
| onAsk={handleAskAi} | |
| imageUrl={sharedImageUrlForAi} | |
| prompt={aiPrompt} | |
| onPromptChange={handlePromptChange} | |
| isLoading={isGeneratingAiImage} | |
| isAsking={isAskingAi} | |
| error={aiEditError} | |
| askUrl={askUrl} | |
| /> | |
| )} | |
| {showZoomSlider && ( | |
| <ZoomSlider | |
| zoomLevel={zoomLevel} | |
| onZoomChange={setZoomLevel} | |
| /> | |
| )} | |
| {showSettingsPanel && ( | |
| <SettingsPanel | |
| isOpen={showSettingsPanel} | |
| onClose={handleToggleSettingsPanel} | |
| showZoomSlider={showZoomSlider} | |
| onShowZoomSliderChange={setShowZoomSlider} | |
| aiImageQuality={aiImageQuality} | |
| onAiImageQualityChange={setAiImageQuality} | |
| aiApiEndpoint={aiApiEndpoint} | |
| onAiApiEndpointChange={setAiApiEndpoint} | |
| aiDimensionsMode={aiDimensionsMode} | |
| onAiDimensionsModeChange={setAiDimensionsMode} | |
| isFullscreenActive={isFullscreenActive} | |
| onToggleFullscreen={toggleFullscreen} | |
| currentCanvasWidth={canvasWidth} | |
| currentCanvasHeight={canvasHeight} | |
| onCanvasSizeChange={handleCanvasSizeChange} | |
| /> | |
| )} | |
| {confirmModalInfo.isOpen && ( | |
| <ConfirmModal | |
| isOpen={confirmModalInfo.isOpen} | |
| title={confirmModalInfo.title} | |
| message={confirmModalInfo.message} | |
| onConfirm={confirmModalInfo.onConfirmAction} | |
| onCancel={closeConfirmModal} | |
| isConfirmDestructive={confirmModalInfo.isDestructive} | |
| confirmText={confirmModalInfo.confirmText} | |
| cancelText={confirmModalInfo.cancelText} | |
| /> | |
| )} | |
| <div className="fixed bottom-4 right-4 z-[1200] flex flex-col-reverse gap-2"> | |
| {toasts.map(toast => ( | |
| <div | |
| key={toast.id} | |
| className={`px-4 py-2 rounded-md shadow-lg text-sm font-medium text-white | |
| ${toast.type === 'success' ? 'bg-green-500' : ''} | |
| ${toast.type === 'error' ? 'bg-red-500' : ''} | |
| ${toast.type === 'info' ? 'bg-blue-500' : ''} | |
| animate-fade-in-out | |
| `} | |
| role="alert" | |
| > | |
| {toast.message} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default App; | |