import React, { useState, useEffect } from 'react'; import { WizardState, Step, GeneratedContent } from './types'; import StepContext from './components/StepContext'; import StepPlatform from './components/StepPlatform'; import StepEventType from './components/StepEventType'; import StepToneGoal from './components/StepToneGoal'; import StepDetails from './components/StepDetails'; import StepResult from './components/StepResult'; import { ChevronLeft, ExternalLink, Sparkles, User } from 'lucide-react'; import { generateStoryContent } from './services/geminiService'; const STORAGE_KEY = 'gpx-storyteller-state-v6'; // Incremented version for structure change const INITIAL_STATE: WizardState = { step: Step.CONTEXT, context: null, storyStyle: null, platform: null, eventType: null, tone: null, goal: null, title: 'Poznań Maraton 2025', description: 'Byłem totalnie nieprzygotowany. Ostatnie pół roku prawie nie ćwiczyłem, a w dodatku biegłem na antybiotykach z zapaleniem osemki. To była walka a nie przyjemność z biegania. To było głupie. Ale było warto', files: [], stats: { distance: '', duration: '', elevation: '', }, waypoints: [], tripData: { startPoint: { place: '', description: '' }, endPoint: { place: '', description: '' }, stops: [], travelMode: null, googleMapsKey: '' } }; const App: React.FC = () => { const [data, setData] = useState(INITIAL_STATE); const [generatedContent, setGeneratedContent] = useState(null); const [isGenerating, setIsGenerating] = useState(false); const [isLoaded, setIsLoaded] = useState(false); const [errorMessage, setErrorMessage] = useState(null); // Image loading states const [logoError, setLogoError] = useState(false); const [avatarError, setAvatarError] = useState(false); // Persistence Logic useEffect(() => { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { try { const parsed = JSON.parse(saved); // Reset files because File objects cannot be serialized to local storage parsed.files = []; // Ensure new fields exist if loading from old state if (!parsed.stats) parsed.stats = { distance: '', duration: '', elevation: '' }; if (!parsed.waypoints) parsed.waypoints = []; // Ensure tripData exists if (!parsed.tripData) { parsed.tripData = { ...INITIAL_STATE.tripData }; } // Defaults for new fields if migrating if (!parsed.tone) parsed.tone = null; if (!parsed.goal) parsed.goal = null; if (!parsed.storyStyle) parsed.storyStyle = null; setData(parsed); } catch (e) { console.error("Failed to load state", e); } } setIsLoaded(true); }, []); useEffect(() => { if (isLoaded) { // Exclude files from persistence to avoid quota issues and serialization errors const stateToSave = { ...data, files: [] }; localStorage.setItem(STORAGE_KEY, JSON.stringify(stateToSave)); } }, [data, isLoaded]); // UPDATED: Now supports functional updates to prevent stale state issues const updateData = (updates: Partial | ((prev: WizardState) => Partial)) => { setData(prev => { const newValues = typeof updates === 'function' ? updates(prev) : updates; return { ...prev, ...newValues }; }); }; const nextStep = () => { if (data.step < Step.RESULT) { updateData({ step: data.step + 1 }); } }; const prevStep = () => { if (data.step > Step.CONTEXT) { updateData({ step: data.step - 1 }); } }; const handleGenerate = async () => { setIsGenerating(true); setErrorMessage(null); try { const content = await generateStoryContent( data, process.env.API_KEY || '' ); setGeneratedContent(content); updateData({ step: Step.RESULT }); } catch (error: any) { console.error("Generowanie nie powiodło się:", error); setErrorMessage("Błąd generowania. Sprawdź poprawność klucza API w pliku .env oraz połączenie z siecią. " + (error.message || '')); } finally { setIsGenerating(false); } }; const handleRegenerate = async (slideCount: number, feedback: string) => { setIsGenerating(true); setErrorMessage(null); try { const content = await generateStoryContent( data, process.env.API_KEY || '', { slideCount, feedback } ); setGeneratedContent(content); } catch (error: any) { console.error("Regenerowanie nie powiodło się:", error); setErrorMessage("Błąd regenerowania: " + (error.message || '')); } finally { setIsGenerating(false); } }; const resetApp = () => { setData({ ...INITIAL_STATE }); setGeneratedContent(null); localStorage.removeItem(STORAGE_KEY); // Force reload to clear any hung states window.location.reload(); }; if (!isLoaded) return null; // Step Labels for Progress Bar (Order Changed) const stepsLabels = ['Kontekst', 'Typ', 'Platforma', 'Vibe & Cel', 'Szczegóły']; return (
{/* Header */}
{!logoError ? ( setLogoError(true)} alt="PromptStory Logo" className="w-10 h-10 object-contain" /> ) : (
)}

PromptStory

{/* Progress Bar */} {data.step !== Step.RESULT && (
{stepsLabels.map((label, idx) => ( = idx ? 'text-[#EA4420]' : 'text-gray-300'}`}> 0{idx + 1}. {label} ))}
)} {/* Dynamic Content Container */}
{/* STEP 1: CONTEXT */} {data.step === Step.CONTEXT && } {/* STEP 2: EVENT TYPE (Moved UP) */} {data.step === Step.EVENT_TYPE && } {/* STEP 3: PLATFORM (Moved DOWN) */} {data.step === Step.PLATFORM && } {/* STEP 4: TONE & GOAL (NEW) */} {data.step === Step.TONE_GOAL && } {/* STEP 5: DETAILS */} {data.step === Step.DETAILS && ( )} {/* STEP 6: RESULT */} {data.step === Step.RESULT && generatedContent && ( )} {/* Error Message */} {errorMessage && (
{errorMessage}
)}
{/* Navigation Controls (Back Only) */} {data.step > Step.CONTEXT && data.step < Step.RESULT && (
)}
{/* Footer / Author Info */}
); }; export default App;