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); const [isEnriching, setIsEnriching] = useState(false); // 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', 'customerJourney']; const handleEnrichRow = async (productName: string, productUrl?: string) => { setIsEnriching(true); setError(null); try { const response = await fetch(`${API_BASE_URL}/enrich-product`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productName, productUrl, language: inputData.language }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.details || `HTTP error! status: ${response.status}`); } const newRow = await response.json(); setAnalysisData(prev => { const currentOffer = prev.offer || { headers: [], rows: [], summary: [] }; return { ...prev, offer: { ...currentOffer, rows: [...currentOffer.rows, newRow] } }; }); } catch (e) { console.error(e); setError(e instanceof Error ? `Fehler beim Anreichern: ${e.message}` : 'Unbekannter Fehler beim Anreichern.'); } finally { setIsEnriching(false); } }; // --- 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); }; // --- IMPORT MARKDOWN --- const handleImportMarkdown = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const content = e.target?.result as string; if (!content) return; try { const newAnalysisData: Partial = {}; // Helper to parse table from a section const parseSection = (sectionTitle: string): string[][] => { const sectionRegex = new RegExp(`## ${sectionTitle}[\\s\\S]*?(?=## |$)`, 'i'); const match = content.match(sectionRegex); if (!match) return []; const lines = match[0].split('\n'); const rows: string[][] = []; let inTable = false; for (const line of lines) { if (line.trim().startsWith('|') && !line.includes('---')) { const cells = line.split('|').map(c => c.trim()).filter(c => c); if (cells.length > 0) { if (!inTable) { // Skip header row if we are just starting (assuming we have default headers or don't need them for raw data) // Actually, we usually want data rows. The first row in MD table is header. // Let's rely on standard markdown table structure: Header | Separator | Data inTable = true; } else { rows.push(cells); } } } } // Remove the header row if captured (simple heuristic: first row is usually header) if (rows.length > 0) rows.shift(); return rows; }; // Mapping MD titles to keys // Note: flexible matching for DE/EN titles newAnalysisData.offer = { summary: [], headers: t.stepTitles.offer.includes('Schritt') ? ["Produkt/Lösung", "Beschreibung", "Kernfunktionen", "Differenzierung", "Quelle"] : ["Product/Solution", "Description", "Core Features", "Differentiation", "Source"], rows: parseSection("Schritt 1") }; if (newAnalysisData.offer.rows.length === 0) newAnalysisData.offer.rows = parseSection("Step 1"); newAnalysisData.targetGroups = { summary: [], headers: t.stepTitles.targetGroups.includes('Schritt') ? ["Zielbranche", "Merkmale", "Region", "Quelle"] : ["Target Industry", "Characteristics", "Region", "Source"], rows: parseSection("Schritt 2") }; if (newAnalysisData.targetGroups.rows.length === 0) newAnalysisData.targetGroups.rows = parseSection("Step 2"); newAnalysisData.personas = { summary: [], headers: [], rows: parseSection("Schritt 3") }; if (newAnalysisData.personas.rows.length === 0) newAnalysisData.personas.rows = parseSection("Step 3"); newAnalysisData.painPoints = { summary: [], headers: [], rows: parseSection("Schritt 4") }; if (newAnalysisData.painPoints.rows.length === 0) newAnalysisData.painPoints.rows = parseSection("Step 4"); newAnalysisData.gains = { summary: [], headers: [], rows: parseSection("Schritt 5") }; if (newAnalysisData.gains.rows.length === 0) newAnalysisData.gains.rows = parseSection("Step 5"); newAnalysisData.messages = { summary: [], headers: [], rows: parseSection("Schritt 6") }; if (newAnalysisData.messages.rows.length === 0) newAnalysisData.messages.rows = parseSection("Step 6"); newAnalysisData.customerJourney = { summary: [], headers: [], rows: parseSection("Schritt 7") }; if (newAnalysisData.customerJourney.rows.length === 0) newAnalysisData.customerJourney.rows = parseSection("Step 7"); setAnalysisData(newAnalysisData); setGenerationStep(7); // Jump to end (now 7) setProjectName(file.name.replace('.md', ' (Imported)')); setProjectId(null); // Treat as new project } catch (err) { console.error("Parse error", err); setError("Fehler beim Importieren der Datei."); } }; reader.readAsText(file); }; 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 >= 7) 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 || generationStep === 6) ? 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 handleStepRestart = (stepKey: keyof AnalysisData) => { const stepIndex = STEP_KEYS.indexOf(stepKey); if (stepIndex === -1) return; // Reset steps from this index onwards // But specifically, if I restart Step 2, I want generationStep to be 1 (so I can generate Step 2 again) // If I restart Step 1 (index 0), I want generationStep to be 0 // But wait, if I restart Step 1 (Offer), that is the "Start Generation" button usually. // Actually, if I restart Step 1, I basically want to clear everything and go back to step 0. // If I restart Step 2, I want to keep Step 1, clear Step 2+, and go back to step 1 (ready to generate step 2). const newGenerationStep = stepIndex; setGenerationStep(newGenerationStep); setAnalysisData(prev => { const newData: Partial = { ...prev }; // Remove all keys starting from this step for (let i = stepIndex; i < STEP_KEYS.length; i++) { delete newData[STEP_KEYS[i]]; } return newData; }); // Also reset other state related to progress if (stepKey === 'messages' || stepKey === 'targetGroups') { // If we are resetting before Step 6, clear selection // Actually if we reset Step 6 (messages), we clear Step 6 data. if (stepIndex <= 5) { setBatchStatus(null); // We might want to keep selectedIndustry if we are just retrying Step 6? // But the prompt implies "resetting", so clearing selection is safer. setSelectedIndustry(''); } } // If resetting Step 7, clear its specific state if any (none currently) }; 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; // Allow manual addition for Offer (Step 1) and Target Groups (Step 2) const canAdd = ['offer', 'targetGroups'].includes(stepKey); const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey); const handleManualAdd = () => { const newEmptyRow = Array(step.headers.length).fill(''); const currentRows = step.rows || []; handleDataChange(stepKey, { ...step, rows: [...currentRows, newEmptyRow] }); }; return ( handleDataChange(stepKey, { ...step, rows: newRows })} canAddRows={canAdd} onEnrichRow={stepKey === 'offer' ? handleEnrichRow : handleManualAdd} isEnriching={isEnriching} canDeleteRows={canDelete} onRestart={() => handleStepRestart(stepKey)} 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)} {renderContinueButton(7)} {generationStep >= 7 && renderStep('customerJourney', STEP_TITLES.customerJourney)} {generationStep === 7 && !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;