224 lines
8.6 KiB
TypeScript
224 lines
8.6 KiB
TypeScript
|
|
import React, { useState, useCallback } from 'react';
|
|
import { UploadedImage, AppStep, GenerationResult } from './types';
|
|
import { expandPrompt, generateImage, refineImage } from './services/geminiService';
|
|
import ImageUploader from './components/ImageUploader';
|
|
import ImageSegmenter from './components/ImageSegmenter';
|
|
import PromptCustomizer from './components/PromptCustomizer';
|
|
import ImageResult from './components/ImageResult';
|
|
import StepIndicator from './components/StepIndicator';
|
|
import { SparklesIcon } from './components/icons/SparklesIcon';
|
|
import { applyMask } from './utils/canvasUtils';
|
|
import { LoggingProvider, useLogger } from './contexts/LoggingContext';
|
|
import DebugConsole from './components/DebugConsole';
|
|
|
|
const AppContent: React.FC = () => {
|
|
const [step, setStep] = useState<AppStep>(AppStep.Upload);
|
|
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([]);
|
|
const [masterPrompt, setMasterPrompt] = useState<string>('');
|
|
const [generationResult, setGenerationResult] = useState<GenerationResult | null>(null);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [loadingMessage, setLoadingMessage] = useState<string>('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const { log } = useLogger();
|
|
|
|
const handleImagesUploaded = (images: UploadedImage[]) => {
|
|
log('info', `${images.length} images uploaded. Moving to segmentation step.`);
|
|
setUploadedImages(images);
|
|
setStep(AppStep.Segment);
|
|
setError(null);
|
|
};
|
|
|
|
const handleSegmentationComplete = (imagesWithMasks: UploadedImage[]) => {
|
|
log('success', `Segmentation complete for all ${imagesWithMasks.length} images. Moving to prompt step.`);
|
|
setUploadedImages(imagesWithMasks);
|
|
setStep(AppStep.Prompt);
|
|
setError(null);
|
|
};
|
|
|
|
const handlePromptExpanded = useCallback(async (scenario: string, userInstruction: string) => {
|
|
setIsLoading(true);
|
|
setLoadingMessage('Expanding your idea into a master prompt...');
|
|
setError(null);
|
|
log('info', `Expanding prompt with scenario: "${scenario}"`);
|
|
try {
|
|
const prompt = await expandPrompt(scenario, userInstruction, uploadedImages);
|
|
setMasterPrompt(prompt);
|
|
log('success', 'Master prompt created successfully.');
|
|
} catch (e) {
|
|
const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred during prompt expansion.';
|
|
log('error', `Prompt expansion failed: ${errorMessage}`);
|
|
setError(errorMessage);
|
|
} finally {
|
|
setIsLoading(false);
|
|
setLoadingMessage('');
|
|
}
|
|
}, [uploadedImages, log]);
|
|
|
|
const handleFinalGeneration = useCallback(async (finalPrompt: string) => {
|
|
setIsLoading(true);
|
|
setLoadingMessage('Generating your K-Pop thumbnail...');
|
|
setError(null);
|
|
log('info', 'Starting final image generation.');
|
|
try {
|
|
log('info', 'Applying masks to create segmented images...');
|
|
const segmentedImages = await Promise.all(
|
|
uploadedImages.map(async (image) => {
|
|
if (!image.maskDataUrl) {
|
|
throw new Error(`Mask is missing for image: ${image.file.name}`);
|
|
}
|
|
const segmentedData = await applyMask(image.previewUrl, image.maskDataUrl);
|
|
return { ...image, segmentedDataUrl: `data:image/png;base64,${segmentedData}` };
|
|
})
|
|
);
|
|
log('success', 'Masks applied successfully.');
|
|
|
|
const result = await generateImage(finalPrompt, segmentedImages);
|
|
setGenerationResult({
|
|
baseImage: result,
|
|
currentImage: result,
|
|
history: [result],
|
|
});
|
|
setStep(AppStep.Result);
|
|
log('success', 'Thumbnail generated successfully.');
|
|
} catch (e) {
|
|
const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred during image generation.';
|
|
log('error', `Final image generation failed: ${errorMessage}`);
|
|
setError(errorMessage);
|
|
setStep(AppStep.Prompt);
|
|
} finally {
|
|
setIsLoading(false);
|
|
setLoadingMessage('');
|
|
}
|
|
}, [uploadedImages, log]);
|
|
|
|
const handleImageRefinement = useCallback(async (refinementPrompt: string) => {
|
|
if (!generationResult) return;
|
|
setIsLoading(true);
|
|
setLoadingMessage('Applying your refinements...');
|
|
setError(null);
|
|
log('info', `Refining image with prompt: "${refinementPrompt}"`);
|
|
try {
|
|
const newImage = await refineImage(refinementPrompt, generationResult.currentImage);
|
|
setGenerationResult(prev => {
|
|
if (!prev) return null;
|
|
const newHistory = [...prev.history, newImage];
|
|
return {
|
|
baseImage: prev.baseImage,
|
|
currentImage: newImage,
|
|
history: newHistory,
|
|
};
|
|
});
|
|
log('success', 'Image refined successfully.');
|
|
} catch (e) {
|
|
const errorMessage = e instanceof Error ? e.message : 'An unknown error occurred during image refinement.';
|
|
log('error', `Image refinement failed: ${errorMessage}`);
|
|
setError(errorMessage);
|
|
} finally {
|
|
setIsLoading(false);
|
|
setLoadingMessage('');
|
|
}
|
|
}, [generationResult, log]);
|
|
|
|
const handleBack = () => {
|
|
setError(null);
|
|
if (step === AppStep.Result) {
|
|
log('info', 'Navigating back from Result to Prompt step.');
|
|
setMasterPrompt('');
|
|
setStep(AppStep.Prompt);
|
|
} else if (step === AppStep.Prompt) {
|
|
log('info', 'Navigating back from Prompt to Segment step.');
|
|
setMasterPrompt('');
|
|
setStep(AppStep.Segment);
|
|
} else if (step === AppStep.Segment) {
|
|
log('info', 'Navigating back from Segment to Upload step.');
|
|
setStep(AppStep.Upload);
|
|
}
|
|
};
|
|
|
|
const handleStartOver = () => {
|
|
log('info', 'Starting over. Resetting application state.');
|
|
setStep(AppStep.Upload);
|
|
setUploadedImages([]);
|
|
setMasterPrompt('');
|
|
setGenerationResult(null);
|
|
setIsLoading(false);
|
|
setLoadingMessage('');
|
|
setError(null);
|
|
}
|
|
|
|
const renderStep = () => {
|
|
switch (step) {
|
|
case AppStep.Upload:
|
|
return <ImageUploader onImagesUploaded={handleImagesUploaded} />;
|
|
case AppStep.Segment:
|
|
return <ImageSegmenter
|
|
images={uploadedImages}
|
|
onComplete={handleSegmentationComplete}
|
|
onBack={handleBack}
|
|
/>;
|
|
case AppStep.Prompt:
|
|
return <PromptCustomizer
|
|
onPromptExpanded={handlePromptExpanded}
|
|
onFinalSubmit={handleFinalGeneration}
|
|
isLoading={isLoading}
|
|
loadingMessage={loadingMessage}
|
|
uploadedImages={uploadedImages}
|
|
onBack={handleBack}
|
|
masterPrompt={masterPrompt}
|
|
setMasterPrompt={setMasterPrompt}
|
|
/>;
|
|
case AppStep.Result:
|
|
return <ImageResult
|
|
result={generationResult}
|
|
onRefine={handleImageRefinement}
|
|
masterPrompt={masterPrompt}
|
|
isLoading={isLoading}
|
|
loadingMessage={loadingMessage}
|
|
onStartOver={handleStartOver}
|
|
/>;
|
|
default:
|
|
return <ImageUploader onImagesUploaded={handleImagesUploaded} />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-900 text-gray-100 flex flex-col items-center p-4 sm:p-6 lg:p-8 pb-32">
|
|
<header className="w-full max-w-6xl text-center mb-6">
|
|
<div className="flex items-center justify-center gap-3">
|
|
<SparklesIcon className="w-10 h-10 text-purple-400" />
|
|
<h1 className="text-4xl sm:text-5xl md:text-6xl font-teko tracking-wider uppercase bg-gradient-to-r from-purple-400 to-pink-500 text-transparent bg-clip-text">
|
|
K-Pop Thumbnail Genie
|
|
</h1>
|
|
</div>
|
|
<p className="text-gray-400 mt-2 text-sm sm:text-base">Create stunning, emotional YouTube thumbnails with the magic of AI</p>
|
|
</header>
|
|
|
|
<main className="w-full max-w-6xl flex-grow">
|
|
<StepIndicator currentStep={step} />
|
|
<div className="mt-8 bg-gray-800/50 p-6 sm:p-8 rounded-2xl shadow-2xl shadow-purple-900/10 border border-gray-700">
|
|
{error && (
|
|
<div className="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-6 text-center">
|
|
<p><span className="font-bold">Error:</span> {error}</p>
|
|
</div>
|
|
)}
|
|
{renderStep()}
|
|
</div>
|
|
</main>
|
|
<footer className="w-full max-w-6xl text-center mt-8 text-gray-500 text-xs">
|
|
<p>Powered by Google Gemini. Designed for K-Pop content creators.</p>
|
|
</footer>
|
|
<DebugConsole />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
|
|
const App: React.FC = () => (
|
|
<LoggingProvider>
|
|
<AppContent />
|
|
</LoggingProvider>
|
|
);
|
|
|
|
export default App;
|