import React, { useState, useCallback, useEffect } from 'react'; import { InputForm } from './components/InputForm'; import { StepDisplay } from './components/StepDisplay'; import { LoadingSpinner, BotIcon, SparklesIcon, MarkdownIcon, PrintIcon } from './components/Icons'; import { ExportMenu } from './components/ExportMenu'; import { translations } from './constants'; import type { AnalysisStep, AnalysisData, InputData, Project, ProjectMetadata } from './types'; import { generateMarkdown, downloadFile } from './services/export'; import { History, Clock, Trash2, X, ArrowRight, FolderOpen } from 'lucide-react'; const API_BASE_URL = 'api'; // --- DB HELPERS --- const listProjects = async (): Promise => { const response = await fetch(`${API_BASE_URL}/projects`); if (!response.ok) throw new Error("Failed to list projects"); return await response.json(); }; const loadProjectData = async (id: string): Promise => { const response = await fetch(`${API_BASE_URL}/projects/${id}`); if (!response.ok) throw new Error("Failed to load project"); return await response.json(); }; const saveProject = async (data: any): Promise => { const response = await fetch(`${API_BASE_URL}/save-project`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) throw new Error("Failed to save project"); return await response.json(); }; const deleteProject = async (id: string): Promise => { const response = await fetch(`${API_BASE_URL}/projects/${id}`, { method: 'DELETE' }); if (!response.ok) throw new Error("Failed to delete project"); return await response.json(); }; const App: React.FC = () => { const [inputData, setInputData] = useState({ companyUrl: '', language: 'de', regions: '', focus: '', channels: ['LinkedIn', 'Kaltmail', 'Landingpage'] }); const [analysisData, setAnalysisData] = useState>({}); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [generationStep, setGenerationStep] = useState(0); // 0: idle, 1-6: step X is complete const [selectedIndustry, setSelectedIndustry] = useState(''); const [batchStatus, setBatchStatus] = useState<{ current: number; total: number; industry: string } | null>(null); // Project Persistence const [projectId, setProjectId] = useState(null); const [projectName, setProjectName] = useState(''); const [showHistory, setShowHistory] = useState(false); const [recentProjects, setRecentProjects] = useState([]); const [isLoadingProjects, setIsLoadingProjects] = useState(false); const t = translations[inputData.language]; const STEP_TITLES = t.stepTitles; const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages']; // --- AUTO-SAVE EFFECT --- useEffect(() => { if (generationStep === 0 || !inputData.companyUrl) return; const saveData = async () => { let dynamicName = projectName; try { const urlObj = new URL(inputData.companyUrl.startsWith('http') ? inputData.companyUrl : `https://${inputData.companyUrl}`); const host = urlObj.hostname.replace('www.', ''); if (generationStep >= 1) { dynamicName = `${host} (Step ${generationStep})`; } else { dynamicName = `Draft: ${host}`; } } catch (e) { dynamicName = projectName || "Untitled Project"; } const dataToSave = { id: projectId, name: dynamicName, currentStep: generationStep, language: inputData.language, inputs: inputData, analysisData: analysisData }; try { const result = await saveProject(dataToSave); if (result.id && !projectId) { setProjectId(result.id); } } catch (e) { console.error("Auto-save failed", e); } }; const timer = setTimeout(saveData, 2000); return () => clearTimeout(timer); }, [generationStep, analysisData, inputData, projectId, projectName]); // --- DB ACTIONS --- const fetchProjects = useCallback(async () => { setIsLoadingProjects(true); try { const projects = await listProjects(); setRecentProjects(projects); } catch (e) { console.error("Failed to load projects", e); } finally { setIsLoadingProjects(false); } }, []); useEffect(() => { if (showHistory) fetchProjects(); }, [showHistory, fetchProjects]); const handleProjectSelect = async (id: string) => { setIsLoading(true); setError(null); try { const project = await loadProjectData(id); setProjectId(project.id); setProjectName(project.name); setInputData(project.inputs); setAnalysisData(project.analysisData); setGenerationStep(project.currentStep); setShowHistory(false); } catch (e) { console.error(e); setError("Fehler beim Laden des Projekts."); } finally { setIsLoading(false); } }; const handleDeleteProject = async (e: React.MouseEvent, id: string) => { e.stopPropagation(); if (confirm(t.language === 'de' ? "Projekt wirklich löschen?" : "Delete project?")) { try { await deleteProject(id); fetchProjects(); if (id === projectId) { handleRestart(); } } catch (e) { console.error(e); alert("Fehler beim Löschen."); } } }; const handleRestart = () => { setProjectId(null); setProjectName(''); setAnalysisData({}); setGenerationStep(0); setSelectedIndustry(''); setBatchStatus(null); setError(null); }; const handleStartGeneration = useCallback(async () => { if (!inputData.companyUrl) { setError('Bitte geben Sie eine Unternehmens-URL ein.'); return; } setIsLoading(true); setError(null); setAnalysisData({}); setGenerationStep(0); setSelectedIndustry(''); setBatchStatus(null); try { const response = await fetch(`${API_BASE_URL}/start-generation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ companyUrl: inputData.companyUrl, language: inputData.language, regions: inputData.regions, focus: inputData.focus, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.details || `HTTP error! status: ${response.status}`); } const parsedData = await response.json(); if (parsedData.error) { throw new Error(parsedData.error); } setAnalysisData(parsedData); setGenerationStep(1); } catch (e) { console.error(e); setError(e instanceof Error ? `Ein Fehler ist aufgetreten: ${e.message}` : 'Ein unbekannter Fehler ist aufgetreten.'); } finally { setIsLoading(false); } }, [inputData]); const handleGenerateNextStep = useCallback(async () => { if (generationStep >= 6) return; if (generationStep === 5 && !selectedIndustry) { setError('Bitte wählen Sie eine Fokus-Branche aus.'); return; } setIsLoading(true); setError(null); try { const response = await fetch(`${API_BASE_URL}/next-step`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysisData, language: inputData.language, channels: inputData.channels, generationStep: generationStep + 1, // Pass the step we want to generate focusIndustry: generationStep === 5 ? selectedIndustry : undefined, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.details || `HTTP error! status: ${response.status}`); } const parsedData = await response.json(); if (parsedData.error) { throw new Error(parsedData.error); } setAnalysisData(prev => ({ ...prev, ...parsedData })); setGenerationStep(prev => prev + 1); } catch (e) { console.error(e); setError(e instanceof Error ? `Ein Fehler ist aufgetreten: ${e.message}` : 'Ein unbekannter Fehler ist aufgetreten.'); setGenerationStep(prev => prev); // Stay on the current step if error } finally { setIsLoading(false); } }, [analysisData, generationStep, inputData.channels, inputData.language, selectedIndustry]); const handleBatchGenerate = useCallback(async () => { if (!analysisData.targetGroups?.rows) return; const industries = analysisData.targetGroups.rows.map(row => row[0]); if (industries.length === 0) return; setIsLoading(true); setError(null); setGenerationStep(6); // Show the Step 6 container (will be filled incrementally) // Initialize Step 6 data container setAnalysisData(prev => ({ ...prev, messages: { summary: ["Batch-Analyse aller Branchen läuft..."], headers: ["Fokus-Branche", "Rolle", "Kernbotschaft", "Kanäle"], // Default headers, will be overwritten/verified rows: [] } })); let aggregatedRows: string[][] = []; let capturedHeaders: string[] = []; for (let i = 0; i < industries.length; i++) { const industry = industries[i]; setBatchStatus({ current: i + 1, total: industries.length, industry }); try { const response = await fetch(`${API_BASE_URL}/next-step`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysisData, // Pass full context language: inputData.language, channels: inputData.channels, generationStep: 6, focusIndustry: industry, }), }); if (!response.ok) throw new Error(`HTTP error for ${industry}`); const data = await response.json(); if (data.messages && data.messages.rows) { if (capturedHeaders.length === 0 && data.messages.headers) { capturedHeaders = data.messages.headers; } aggregatedRows = [...aggregatedRows, ...data.messages.rows]; // Update state incrementally so user sees results growing setAnalysisData(prev => ({ ...prev, messages: { summary: ["Vollständige Analyse über alle identifizierten Branchen."], headers: capturedHeaders.length > 0 ? capturedHeaders : (prev?.messages?.headers || []), rows: aggregatedRows } })); } } catch (e) { console.error(`Error processing industry ${industry}:`, e); // We continue with next industry even if one fails } } setIsLoading(false); setBatchStatus(null); }, [analysisData, inputData]); const handleDataChange = (step: K, newData: AnalysisData[K]) => { if (analysisData[step]) { setAnalysisData(prev => prev ? { ...prev, [step]: newData } : { [step]: newData }); } }; const handleDownloadMarkdown = () => { if (!analysisData) return; const markdownContent = generateMarkdown(analysisData as AnalysisData, STEP_TITLES, t.summaryTitle); downloadFile(markdownContent, 'b2b-marketing-analysis.md', 'text/markdown;charset=utf-8'); }; const handlePrint = () => { window.print(); }; const renderStep = (stepKey: keyof AnalysisData, title: string) => { const step = analysisData[stepKey] as AnalysisStep | undefined; if (!step) return null; const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey); return ( handleDataChange(stepKey, { ...step, rows: newRows })} canAddRows={false} // Disabled enrich functionality onEnrichRow={undefined} isEnriching={false} canDeleteRows={canDelete} t={t} /> ); }; const renderContinueButton = (stepNumber: number) => { if (isLoading || generationStep !== stepNumber - 1) return null; // Industry Selector Logic for Step 6 if (stepNumber === 6 && analysisData.targetGroups?.rows) { const industries = analysisData.targetGroups.rows.map(row => row[0]); // Assume Col 0 is Industry return (

Wählen Sie eine Fokus-Branche für Schritt 6 (Botschaften):

- ODER EINZELN -
{industries.map((ind, idx) => ( ))}
); } return (
); } return (

{t.appTitle}

{t.appSubtitle}

{generationStep > 0 && ( )}
{error && (
{t.errorTitle} {error}
)}
{isLoading && (

{batchStatus ? `Analysiere Branche ${batchStatus.current} von ${batchStatus.total}: ${batchStatus.industry}...` : t.generatingStep.replace('{{stepTitle}}', STEP_TITLES[STEP_KEYS[generationStep]])}

{batchStatus && (
)}
)} {!isLoading && generationStep === 0 && !error && (

{t.readyTitle}

{t.readyText}

)} {generationStep > 0 && ( <> {generationStep >= 6 && (
)} {renderStep('offer', STEP_TITLES.offer)} {renderContinueButton(2)} {generationStep >= 2 && renderStep('targetGroups', STEP_TITLES.targetGroups)} {renderContinueButton(3)} {generationStep >= 3 && renderStep('personas', STEP_TITLES.personas)} {renderContinueButton(4)} {generationStep >= 4 && renderStep('painPoints', STEP_TITLES.painPoints)} {renderContinueButton(5)} {generationStep >= 5 && renderStep('gains', STEP_TITLES.gains)} {renderContinueButton(6)} {generationStep >= 6 && renderStep('messages', STEP_TITLES.messages)} {generationStep === 6 && !isLoading && (

{t.analysisCompleteTitle}

{t.analysisCompleteText.replace('{{otherLanguage}}', t.otherLanguage)}

Ergebnis verfeinern?

)} )}
{/* History Modal */} {showHistory && (

{inputData.language === 'de' ? 'Projekt-Historie' : 'Project History'}

{isLoadingProjects ? (

Lade Projekte...

) : recentProjects.length > 0 ? ( recentProjects.map((p) => ( )) ) : (

{inputData.language === 'de' ? 'Keine vergangenen Projekte gefunden.' : 'No past projects found.'}

)}
)}
); }; export default App;