xpaintdev / App.tsx
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;