From 682b736e013e042669130a1f8526c7658a3a072a Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 31 Dec 2025 12:38:28 +0000 Subject: [PATCH] feat(gtm-architect): Integrate GTM Architect app with Python backend, DB persistence, and Docker stack --- b2b-marketing-assistant/App.tsx | 122 +- b2b-marketing-assistant/server.cjs | 34 +- dashboard/index.html | 11 + docker-compose.yml | 30 +- general-market-intelligence/server.cjs | 13 +- gtm-architect/App.tsx | 1429 +++++++----------------- gtm-architect/Dockerfile | 28 + gtm-architect/requirements.txt | 10 + gtm-architect/server.cjs | 62 + gtm_architect_orchestrator.py | 107 ++ gtm_architect_plan.md | 23 + nginx-proxy.conf | 31 +- 12 files changed, 830 insertions(+), 1070 deletions(-) create mode 100644 gtm-architect/Dockerfile create mode 100644 gtm-architect/requirements.txt create mode 100644 gtm-architect/server.cjs create mode 100644 gtm_architect_orchestrator.py create mode 100644 gtm_architect_plan.md diff --git a/b2b-marketing-assistant/App.tsx b/b2b-marketing-assistant/App.tsx index a7fa7d10..c96401ab 100644 --- a/b2b-marketing-assistant/App.tsx +++ b/b2b-marketing-assistant/App.tsx @@ -173,6 +173,106 @@ const App: React.FC = () => { 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"); + + + setAnalysisData(newAnalysisData); + setGenerationStep(6); // Jump to end + 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) { @@ -357,7 +457,14 @@ const App: React.FC = () => { 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 = (newRow: string[]) => { + const currentRows = step.rows || []; + handleDataChange(stepKey, { ...step, rows: [...currentRows, newRow] }); + }; return ( { headers={step.headers} rows={step.rows} onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })} - canAddRows={false} // Disabled enrich functionality - onEnrichRow={undefined} + canAddRows={canAdd} + onEnrichRow={canAdd ? handleManualAdd : undefined} isEnriching={false} canDeleteRows={canDelete} t={t} @@ -456,6 +563,17 @@ const App: React.FC = () => {

+ +
+ + +
+ 🏛️ +

GTM Architect

+

+ Entwickelt eine komplette Go-to-Market-Strategie für neue technische Produkte, von der Analyse bis zum Sales-Kit. +

+ + Starten → +
diff --git a/docker-compose.yml b/docker-compose.yml index e860067a..f12000c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,6 +10,8 @@ services: restart: unless-stopped ports: - "8090:80" + volumes: + - ./nginx-proxy.conf:/etc/nginx/nginx.conf depends_on: - dashboard - b2b-app @@ -34,6 +36,8 @@ services: # Sideloading: Python Logic - ./b2b_marketing_orchestrator.py:/app/b2b_marketing_orchestrator.py - ./market_db_manager.py:/app/market_db_manager.py + # Sideloading: Server Logic + - ./b2b-marketing-assistant/server.cjs:/app/server.cjs # Database Persistence - ./b2b_projects.db:/app/b2b_projects.db # Logs @@ -58,6 +62,8 @@ services: - ./market_db_manager.py:/app/market_db_manager.py - ./config.py:/app/config.py - ./helpers.py:/app/helpers.py + # Sideloading: Server Logic + - ./general-market-intelligence/server.cjs:/app/general-market-intelligence/server.cjs # Database Persistence - ./market_intelligence.db:/app/market_intelligence.db # Logs & Keys @@ -78,4 +84,26 @@ services: restart: unless-stopped depends_on: - market-backend - # Port 80 is internal only \ No newline at end of file + # Port 80 is internal only + + # --- GTM ARCHITECT --- + gtm-app: + build: + context: . + dockerfile: gtm-architect/Dockerfile + container_name: gtm-architect + restart: unless-stopped + volumes: + # Sideloading: Python Logic + - ./gtm_architect_orchestrator.py:/app/gtm_architect_orchestrator.py + - ./market_db_manager.py:/app/market_db_manager.py + # Sideloading: Server Logic + - ./gtm-architect/server.cjs:/app/gtm-architect/server.cjs + # Database Persistence + - ./gtm_projects.db:/app/gtm_projects.db + # Keys + - ./gemini_api_key.txt:/app/gemini_api_key.txt + environment: + - PYTHONUNBUFFERED=1 + - DB_PATH=/app/gtm_projects.db + # Port 3005 is internal only \ No newline at end of file diff --git a/general-market-intelligence/server.cjs b/general-market-intelligence/server.cjs index 185453dd..1a2e6ffd 100644 --- a/general-market-intelligence/server.cjs +++ b/general-market-intelligence/server.cjs @@ -10,7 +10,7 @@ const PORT = 3001; // Middleware app.use(cors()); -app.use(bodyParser.json()); +app.use(bodyParser.json({ limit: '50mb' })); // Helper für Python-Aufrufe, um Code-Duplizierung zu vermeiden const runPython = (args, res, tempFilesToDelete = []) => { @@ -245,6 +245,11 @@ app.post('/api/save-project', (req, res) => { } }); -app.listen(PORT, () => { - console.log(`Node.js API Bridge running on http://localhost:${PORT}`); -}); \ No newline at end of file +const server = app.listen(PORT, () => { + console.log(`Node.js API Bridge running on http://localhost:${PORT} (Version: 1.1.0-Fix)`); +}); + +// Set timeout to 10 minutes (600s) to handle long AI generation steps +server.setTimeout(600000); +server.keepAliveTimeout = 610000; +server.headersTimeout = 620000; \ No newline at end of file diff --git a/gtm-architect/App.tsx b/gtm-architect/App.tsx index d5721cc4..d49ba120 100644 --- a/gtm-architect/App.tsx +++ b/gtm-architect/App.tsx @@ -1,10 +1,9 @@ import React, { useState, useEffect, useRef } from 'react'; import { Layout } from './components/Layout'; import { Phase, AppState, Phase1Data, Phase2Data, Phase3Data, Phase4Data, Phase6Data, Language, Theme } from './types'; -import * as Gemini from './geminiService'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import { AlertTriangle, ArrowRight, ArrowLeft, Check, Database, Globe, Search, ShieldAlert, Cpu, Building2, UserCircle, Briefcase, Zap, Terminal, Target, Crosshair, Loader2, Plus, X, Download, ShieldCheck, Image as ImageIcon, Copy, Sparkles, Upload, CheckCircle, PenTool, Eraser, Undo, Save, RefreshCw, Pencil, Trash2 } from 'lucide-react'; +import { AlertTriangle, ArrowRight, ArrowLeft, Check, Database, Globe, Search, ShieldAlert, Cpu, Building2, UserCircle, Briefcase, Zap, Terminal, Target, Crosshair, Loader2, Plus, X, Download, ShieldCheck, Image as ImageIcon, Copy, Sparkles, Upload, CheckCircle, PenTool, Eraser, Undo, Save, RefreshCw, Pencil, Trash2, History } from 'lucide-react'; const TRANSLATIONS = { en: { @@ -16,10 +15,10 @@ const TRANSLATIONS = { phase6: 'Sales Enablement', initTitle: 'Initialize New Product Sequence', inputLabel: 'Product URL or Technical Specification', - inputPlaceholder: 'Paste raw technical data, brochure text, or product URL here...', + inputPlaceholder: 'Paste raw technical data, brochure text, or product URL here...', startBtn: 'Start Analysis', backBtn: 'Back', - processing: 'Processing...', + processing: 'Processing...', features: 'Detected Features', constraints: 'Hard Constraints', conflictTitle: 'Portfolio Conflict Detected', @@ -57,19 +56,23 @@ const TRANSLATIONS = { editImage: 'Edit & Iterate', regenerateSketch: 'Regenerate with Sketch', cancelEdit: 'Cancel Edit', - generating: 'Generating...', + generating: 'Generating...', uploadImage: 'Product Reference Images', uploadHint: 'Upload multiple photos (Front, Side, Detail) for better 3D understanding.', imageUploaded: 'Reference Images', canvasClear: 'Clear', canvasMode: 'Sketch Mode', - // New placeholders addName: 'Name...', addRationale: 'Rationale...', addTarget: 'Target criteria...', addMethod: 'Method...', addAccount: 'Account name...', addRole: 'Job title...', + save: 'Save Project', + load: 'Load Project', + history: 'Project History', + noProjects: 'No saved projects found.', + delete: 'Delete', loading: [ "Initializing quantum analysis engines...", "Parsing raw technical specifications...", @@ -88,10 +91,10 @@ const TRANSLATIONS = { phase6: 'Sales Enablement', initTitle: 'Neue Produktsequenz initialisieren', inputLabel: 'Produkt-URL oder Technische Spezifikation', - inputPlaceholder: 'Füge hier rohe technische Daten, Broschürentexte oder Produkt-URL ein...', + inputPlaceholder: 'Füge hier rohe technische Daten, Broschürentexte oder Produkt-URL ein...', startBtn: 'Analyse starten', backBtn: 'Zurück', - processing: 'Verarbeite...', + processing: 'Verarbeite...', features: 'Erkannte Features', constraints: 'Harte Constraints', conflictTitle: 'Portfolio-Konflikt erkannt', @@ -129,19 +132,23 @@ const TRANSLATIONS = { editImage: 'Zeichnen & Iterieren', regenerateSketch: 'Mit Skizze neu generieren', cancelEdit: 'Abbrechen', - generating: 'Generiere...', + generating: 'Generiere...', uploadImage: 'Produkt-Referenzbilder', uploadHint: 'Lade mehrere Fotos hoch (Front, Seite, Detail), um das 3D-Verständnis zu verbessern.', imageUploaded: 'Referenzbilder', canvasClear: 'Leeren', canvasMode: 'Zeichenmodus', - // New placeholders addName: 'Name...', addRationale: 'Begründung...', addTarget: 'Zielkriterium...', addMethod: 'Suchmethode...', addAccount: 'Firmenname...', addRole: 'Jobtitel...', + save: 'Projekt speichern', + load: 'Projekt laden', + history: 'Projekt Historie', + noProjects: 'Keine gespeicherten Projekte gefunden.', + delete: 'Löschen', loading: [ "Initialisiere Quanten-Analyse-Engines...", "Analysiere technische Spezifikationen...", @@ -159,49 +166,42 @@ const App: React.FC = () => { isLoading: false, history: [], productInput: '', - productImages: [], // Now an array - language: 'light', // temporary init, fix in useEffect - theme: 'light' // Default to light + productImages: [], + language: 'de', + theme: 'light' }); - // Fix initial state type matching - const [language, setLanguage] = useState('de'); // Default to German per prompt + const [language, setLanguage] = useState('de'); const [theme, setTheme] = useState('light'); - const [error, setError] = useState(null); const [loadingMessage, setLoadingMessage] = useState(""); - // Local state for adding new items (Human in the Loop inputs) - // Phase 1 + // History / DB State + const [showHistory, setShowHistory] = useState(false); + const [savedProjects, setSavedProjects] = useState([]); + + // Input State const [newFeatureInput, setNewFeatureInput] = useState(""); const [newConstraintInput, setNewConstraintInput] = useState(""); - - // Phase 2 const [newICPName, setNewICPName] = useState(""); const [newICPRationale, setNewICPRationale] = useState(""); const [newProxyTarget, setNewProxyTarget] = useState(""); const [newProxyMethod, setNewProxyMethod] = useState(""); - // Phase 3 - // Changed to use a map to track input per group index const [newAccountInputs, setNewAccountInputs] = useState>({}); const [newRoleInput, setNewRoleInput] = useState(""); - const [copiedPromptIndex, setCopiedPromptIndex] = useState(null); - - // Phase 6 Image Generation State + + // Image Gen State const [generatingImages, setGeneratingImages] = useState>({}); const [generatedImages, setGeneratedImages] = useState>({}); - - // Phase 6 Editing State const [editingIndex, setEditingIndex] = useState(null); const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); - const [brushColor, setBrushColor] = useState('#ef4444'); // Red for annotations + const [brushColor, setBrushColor] = useState('#ef4444'); const [brushSize, setBrushSize] = useState(4); const labels = TRANSLATIONS[language]; - // Apply theme to body useEffect(() => { if (theme === 'dark') { document.documentElement.classList.add('dark'); @@ -212,21 +212,16 @@ const App: React.FC = () => { useEffect(() => { if (!state.isLoading) return; - const messages = labels.loading; - let i = 0; setLoadingMessage(messages[0]); - const interval = setInterval(() => { i = (i + 1) % messages.length; setLoadingMessage(messages[i]); }, 2500); - return () => clearInterval(interval); }, [state.isLoading, labels.loading]); - // Canvas Initialization for Editing useEffect(() => { if (editingIndex !== null && canvasRef.current && generatedImages[editingIndex]) { const canvas = canvasRef.current; @@ -242,18 +237,13 @@ const App: React.FC = () => { } }, [editingIndex, generatedImages]); - const toggleTheme = () => { - setTheme(prev => prev === 'light' ? 'dark' : 'light'); - }; - + const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light'); const goBack = () => { if (state.currentPhase > Phase.Input) { setState(s => ({ ...s, currentPhase: s.currentPhase - 1 })); } }; - // Determine the highest phase the user has completed data for. - // This allows navigation back and forth. const getMaxAllowedPhase = (): Phase => { if (state.phase6Result) return Phase.SalesEnablement; if (state.phase5Result) return Phase.AssetGeneration; @@ -265,31 +255,188 @@ const App: React.FC = () => { }; const handlePhaseSelect = (phase: Phase) => { - if (state.isLoading) return; // Prevent navigation while generating + if (state.isLoading) return; setState(s => ({ ...s, currentPhase: phase })); }; + const callBackend = async (mode: string, data: any) => { + const response = await fetch('/api/gtm', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mode, data }) + }); + if (!response.ok) { + const err = await response.json(); + throw new Error(err.details || 'Backend error'); + } + return await response.json(); + }; + + // --- DB Handlers --- + const fetchProjects = async () => { + try { + const projects = await callBackend('list_projects', {}); + setSavedProjects(projects); + } catch (e) { + console.error("Failed to list projects", e); + } + }; + + const handleSaveProject = async () => { + try { + // Clone state to avoid mutation issues + const projectData = { ...state, language, theme }; + await callBackend('save_project', projectData); + // Optional: Show success notification + const btn = document.getElementById('save-btn'); + if(btn) { + const originalText = btn.innerHTML; + btn.innerHTML = ` Saved!`; + setTimeout(() => btn.innerHTML = originalText, 2000); + } + } catch (e: any) { + setError("Save failed: " + e.message); + } + }; + + const handleLoadProject = async (id: string) => { + try { + const project = await callBackend('load_project', { id }); + if (project) { + setState(project); + if (project.language) setLanguage(project.language); + if (project.theme) setTheme(project.theme); + setShowHistory(false); + } + } catch (e: any) { + setError("Load failed: " + e.message); + } + }; + + const handleDeleteProject = async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + if (!confirm("Are you sure you want to delete this project?")) return; + try { + await callBackend('delete_project', { id }); + setSavedProjects(prev => prev.filter(p => p.id !== id)); + } catch (e: any) { + setError("Delete failed: " + e.message); + } + }; + + const toggleHistory = () => { + if (!showHistory) { + fetchProjects(); + } + setShowHistory(!showHistory); + }; + + // --- Phase Handlers --- + + const handlePhase1Submit = async () => { + if (!state.productInput.trim()) return; + setState(s => ({ ...s, isLoading: true })); + setError(null); + try { + const result = await callBackend('analyze_product', { productInput: state.productInput, language }); + setState(s => ({ ...s, currentPhase: Phase.ProductAnalysis, phase1Result: result, isLoading: false })); + } catch (e: any) { + setError(e.message || "Analysis failed"); + setState(s => ({ ...s, isLoading: false })); + } + }; + + const handlePhase2Submit = async () => { + if (!state.phase1Result) return; + setState(s => ({ ...s, isLoading: true })); + try { + const result = await callBackend('discover_icps', { phase1Result: state.phase1Result, language }); + setState(s => ({ ...s, currentPhase: Phase.ICPDiscovery, phase2Result: result, isLoading: false })); + } catch (e: any) { + setError(e.message); + setState(s => ({ ...s, isLoading: false })); + } + }; + + const handlePhase3Submit = async () => { + if (!state.phase2Result) return; + setState(s => ({ ...s, isLoading: true })); + try { + const result = await callBackend('hunt_whales', { phase2Result: state.phase2Result, language }); + setState(s => ({ ...s, currentPhase: Phase.WhaleHunting, phase3Result: result, isLoading: false })); + } catch (e: any) { + setError(e.message); + setState(s => ({ ...s, isLoading: false })); + } + }; + + const handlePhase4Submit = async () => { + if (!state.phase3Result || !state.phase1Result) return; + setState(s => ({ ...s, isLoading: true })); + try { + const result = await callBackend('develop_strategy', { phase3Result: state.phase3Result, phase1Result: state.phase1Result, language }); + setState(s => ({ ...s, currentPhase: Phase.Strategy, phase4Result: result, isLoading: false })); + } catch (e: any) { + setError(e.message); + setState(s => ({ ...s, isLoading: false })); + } + }; + + const handlePhase5Submit = async () => { + if (!state.phase4Result || !state.phase3Result || !state.phase2Result || !state.phase1Result) return; + setState(s => ({ ...s, isLoading: true })); + try { + const result = await callBackend('generate_assets', { + phase4Result: state.phase4Result, + phase3Result: state.phase3Result, + phase2Result: state.phase2Result, + phase1Result: state.phase1Result, + language + }); + setState(s => ({ ...s, currentPhase: Phase.AssetGeneration, phase5Result: result, isLoading: false })); + } catch (e: any) { + setError(e.message); + setState(s => ({ ...s, isLoading: false })); + } + }; + + const handlePhase6Submit = async () => { + if (!state.phase4Result || !state.phase3Result || !state.phase1Result) return; + setState(s => ({ ...s, isLoading: true })); + try { + const result = await callBackend('generate_sales_enablement', { + phase4Result: state.phase4Result, + phase3Result: state.phase3Result, + phase1Result: state.phase1Result, + language + }); + setState(s => ({ ...s, currentPhase: Phase.SalesEnablement, phase6Result: result, isLoading: false })); + } catch (e: any) { + setError(e.message); + setState(s => ({ ...s, isLoading: false })); + } + }; + const downloadReport = () => { if (!state.phase5Result || !state.phase6Result) return; - - // Append Phase 6 to Phase 5 Markdown let fullReport = state.phase5Result; fullReport += `\n\n# SALES ENABLEMENT & VISUALS (PHASE 6)\n\n`; - fullReport += `## Kill-Critique Battlecards\n\n`; state.phase6Result.battlecards.forEach(card => { fullReport += `### Persona: ${card.persona}\n`; fullReport += `> **Objection:** "${card.objection}"\n\n`; fullReport += `**Response:** ${card.responseScript}\n\n---\n\n`; }); - fullReport += `## Visual Briefings (Prompts)\n\n`; state.phase6Result.visualPrompts.forEach(prompt => { fullReport += `### ${prompt.title}\n`; fullReport += `*Context: ${prompt.context}*\n\n`; - fullReport += `\`\`\`\n${prompt.prompt}\n\`\`\`\n\n`; + fullReport += `\ +\ +${prompt.prompt}\n\ +\ +`; }); - const element = document.createElement("a"); const file = new Blob([fullReport], {type: 'text/markdown'}); element.href = URL.createObjectURL(file); @@ -303,7 +450,7 @@ const App: React.FC = () => { const element = document.createElement("a"); element.href = dataUrl; element.download = `roboplanet-visual-${index + 1}.png`; - document.body.appendChild(element); + document.body.appendChild(element); element.click(); document.body.removeChild(element); }; @@ -314,28 +461,8 @@ const App: React.FC = () => { setTimeout(() => setCopiedPromptIndex(null), 2000); }; - // --- Image Generation Handlers --- - const handleGenerateImage = async (prompt: string, index: number, overrideReference?: string) => { - setGeneratingImages(prev => ({ ...prev, [index]: true })); - try { - // If sketching, use only the sketch as the strong reference - // If not sketching, use the array of uploaded product images - const refImages = overrideReference ? [overrideReference] : state.productImages; - - const imageUrl = await Gemini.generateConceptImage(prompt, refImages); - setGeneratedImages(prev => ({ ...prev, [index]: imageUrl })); - - // If we were editing, close edit mode - if (editingIndex === index) { - setEditingIndex(null); - } - } catch (e: any) { - console.error("Failed to generate image", e); - setError("Image generation failed: " + e.message); - } finally { - setGeneratingImages(prev => ({ ...prev, [index]: false })); - } + setError("Bildgenerierung ist in der lokalen Version aktuell deaktiviert. Bitte nutzen Sie die Prompts direkt in Midjourney/DALL-E."); }; const handleImageUpload = (event: React.ChangeEvent) => { @@ -354,44 +481,35 @@ const App: React.FC = () => { }; const removeUploadedImage = (index: number) => { - setState(s => ({ - ...s, - productImages: s.productImages.filter((_, i) => i !== index) - })); + setState(s => ({ ...s, productImages: s.productImages.filter((_, i) => i !== index) })); }; - // --- Drawing / Editing Handlers --- - - const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { + const startDrawing = (e: any) => { setIsDrawing(true); const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; - const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = (('touches' in e ? e.touches[0].clientX : e.clientX) - rect.left) * scaleX; const y = (('touches' in e ? e.touches[0].clientY : e.clientY) - rect.top) * scaleY; - ctx.beginPath(); ctx.moveTo(x, y); }; - const draw = (e: React.MouseEvent | React.TouchEvent) => { + const draw = (e: any) => { if (!isDrawing) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; - const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = (('touches' in e ? e.touches[0].clientX : e.clientX) - rect.left) * scaleX; const y = (('touches' in e ? e.touches[0].clientY : e.clientY) - rect.top) * scaleY; - ctx.lineTo(x, y); ctx.strokeStyle = brushColor; ctx.lineWidth = brushSize; @@ -399,9 +517,7 @@ const App: React.FC = () => { ctx.stroke(); }; - const stopDrawing = () => { - setIsDrawing(false); - }; + const stopDrawing = () => setIsDrawing(false); const clearCanvas = () => { const canvas = canvasRef.current; @@ -426,305 +542,98 @@ const App: React.FC = () => { } }; - // --- Handlers --- - - const handlePhase1Submit = async () => { - if (!state.productInput.trim()) return; - setState(s => ({ ...s, isLoading: true })); - setError(null); - try { - const result = await Gemini.analyzeProduct(state.productInput, language); - setState(s => ({ ...s, currentPhase: Phase.ProductAnalysis, phase1Result: result, isLoading: false })); - } catch (e: any) { - setError(e.message || "Analysis failed"); - setState(s => ({ ...s, isLoading: false })); - } - }; - - const handlePhase2Submit = async () => { - if (!state.phase1Result) return; - setState(s => ({ ...s, isLoading: true })); - try { - const result = await Gemini.discoverICPs(state.phase1Result, language); - setState(s => ({ ...s, currentPhase: Phase.ICPDiscovery, phase2Result: result, isLoading: false })); - } catch (e: any) { - setError(e.message); - setState(s => ({ ...s, isLoading: false })); - } - }; - - const handlePhase3Submit = async () => { - if (!state.phase2Result) return; - setState(s => ({ ...s, isLoading: true })); - try { - const result = await Gemini.huntWhales(state.phase2Result, language); - setState(s => ({ ...s, currentPhase: Phase.WhaleHunting, phase3Result: result, isLoading: false })); - } catch (e: any) { - setError(e.message); - setState(s => ({ ...s, isLoading: false })); - } - }; - - const handlePhase4Submit = async () => { - if (!state.phase3Result || !state.phase1Result) return; - setState(s => ({ ...s, isLoading: true })); - try { - const result = await Gemini.developStrategy(state.phase3Result, state.phase1Result, language); - setState(s => ({ ...s, currentPhase: Phase.Strategy, phase4Result: result, isLoading: false })); - } catch (e: any) { - setError(e.message); - setState(s => ({ ...s, isLoading: false })); - } - }; - - const handlePhase5Submit = async () => { - // Requires all previous data for the final report - if (!state.phase4Result || !state.phase3Result || !state.phase2Result || !state.phase1Result) return; - setState(s => ({ ...s, isLoading: true })); - try { - const result = await Gemini.generateAssets( - state.phase4Result, - state.phase3Result, - state.phase2Result, - state.phase1Result, - language - ); - setState(s => ({ ...s, currentPhase: Phase.AssetGeneration, phase5Result: result, isLoading: false })); - } catch (e: any) { - setError(e.message); - setState(s => ({ ...s, isLoading: false })); - } - }; - - const handlePhase6Submit = async () => { - if (!state.phase4Result || !state.phase3Result || !state.phase1Result) return; - setState(s => ({ ...s, isLoading: true })); - try { - const result = await Gemini.generateSalesEnablement( - state.phase4Result, - state.phase3Result, - state.phase1Result, - language - ); - setState(s => ({ ...s, currentPhase: Phase.SalesEnablement, phase6Result: result, isLoading: false })); - } catch (e: any) { - setError(e.message); - setState(s => ({ ...s, isLoading: false })); - } - }; - - // --- List Mutation Handlers (Phase 1) --- - + // --- List Mutation Handlers --- const addFeature = () => { if (!newFeatureInput.trim() || !state.phase1Result) return; - const newItem = newFeatureInput.trim(); setState(s => ({ ...s, - phase1Result: s.phase1Result ? { - ...s.phase1Result, - features: [...s.phase1Result.features, newItem] - } : undefined + phase1Result: s.phase1Result ? { ...s.phase1Result, features: [...s.phase1Result.features, newFeatureInput.trim()] } : undefined })); setNewFeatureInput(""); }; - - const removeFeature = (index: number) => { - setState(s => ({ - ...s, - phase1Result: s.phase1Result ? { - ...s.phase1Result, - features: s.phase1Result.features.filter((_, i) => i !== index) - } : undefined - })); - }; - + const removeFeature = (index: number) => setState(s => ({ + ...s, + phase1Result: s.phase1Result ? { ...s.phase1Result, features: s.phase1Result.features.filter((_, i) => i !== index) } : undefined + })); const addConstraint = () => { if (!newConstraintInput.trim() || !state.phase1Result) return; - const newItem = newConstraintInput.trim(); setState(s => ({ ...s, - phase1Result: s.phase1Result ? { - ...s.phase1Result, - constraints: [...s.phase1Result.constraints, newItem] - } : undefined + phase1Result: s.phase1Result ? { ...s.phase1Result, constraints: [...s.phase1Result.constraints, newConstraintInput.trim()] } : undefined })); setNewConstraintInput(""); }; - - const removeConstraint = (index: number) => { - setState(s => ({ - ...s, - phase1Result: s.phase1Result ? { - ...s.phase1Result, - constraints: s.phase1Result.constraints.filter((_, i) => i !== index) - } : undefined - })); - }; - - // --- List Mutation Handlers (Phase 2) --- - + const removeConstraint = (index: number) => setState(s => ({ + ...s, + phase1Result: s.phase1Result ? { ...s.phase1Result, constraints: s.phase1Result.constraints.filter((_, i) => i !== index) } : undefined + })); const addICP = () => { if (!newICPName.trim() || !newICPRationale.trim() || !state.phase2Result) return; - const newItem = { name: newICPName.trim(), rationale: newICPRationale.trim() }; setState(s => ({ ...s, - phase2Result: s.phase2Result ? { - ...s.phase2Result, - icps: [...s.phase2Result.icps, newItem] - } : undefined + phase2Result: s.phase2Result ? { ...s.phase2Result, icps: [...s.phase2Result.icps, { name: newICPName.trim(), rationale: newICPRationale.trim() }] } : undefined })); setNewICPName(""); setNewICPRationale(""); }; - - const removeICP = (index: number) => { - setState(s => ({ - ...s, - phase2Result: s.phase2Result ? { - ...s.phase2Result, - icps: s.phase2Result.icps.filter((_, i) => i !== index) - } : undefined - })); - }; - + const removeICP = (index: number) => setState(s => ({ + ...s, + phase2Result: s.phase2Result ? { ...s.phase2Result, icps: s.phase2Result.icps.filter((_, i) => i !== index) } : undefined + })); const addProxy = () => { if (!newProxyTarget.trim() || !newProxyMethod.trim() || !state.phase2Result) return; - const newItem = { target: newProxyTarget.trim(), method: newProxyMethod.trim() }; setState(s => ({ ...s, - phase2Result: s.phase2Result ? { - ...s.phase2Result, - dataProxies: [...s.phase2Result.dataProxies, newItem] - } : undefined + phase2Result: s.phase2Result ? { ...s.phase2Result, dataProxies: [...s.phase2Result.dataProxies, { target: newProxyTarget.trim(), method: newProxyMethod.trim() }] } : undefined })); setNewProxyTarget(""); setNewProxyMethod(""); }; - - const removeProxy = (index: number) => { - setState(s => ({ - ...s, - phase2Result: s.phase2Result ? { - ...s.phase2Result, - dataProxies: s.phase2Result.dataProxies.filter((_, i) => i !== index) - } : undefined - })); - }; - - // --- List Mutation Handlers (Phase 3) --- - + const removeProxy = (index: number) => setState(s => ({ + ...s, + phase2Result: s.phase2Result ? { ...s.phase2Result, dataProxies: s.phase2Result.dataProxies.filter((_, i) => i !== index) } : undefined + })); const addAccount = (groupIndex: number) => { - const inputValue = newAccountInputs[groupIndex]; - if (!inputValue?.trim() || !state.phase3Result) return; - - const newItem = inputValue.trim(); - + const val = newAccountInputs[groupIndex]; + if (!val?.trim() || !state.phase3Result) return; const newWhales = [...state.phase3Result.whales]; - newWhales[groupIndex] = { - ...newWhales[groupIndex], - accounts: [...newWhales[groupIndex].accounts, newItem] - }; - - setState(s => ({ - ...s, - phase3Result: s.phase3Result ? { - ...s.phase3Result, - whales: newWhales - } : undefined - })); - + newWhales[groupIndex] = { ...newWhales[groupIndex], accounts: [...newWhales[groupIndex].accounts, val.trim()] }; + setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, whales: newWhales } : undefined })); setNewAccountInputs(prev => ({...prev, [groupIndex]: ""})); }; - const removeAccount = (groupIndex: number, accountIndex: number) => { if (!state.phase3Result) return; - const newWhales = [...state.phase3Result.whales]; - newWhales[groupIndex] = { - ...newWhales[groupIndex], - accounts: newWhales[groupIndex].accounts.filter((_, i) => i !== accountIndex) - }; - - setState(s => ({ - ...s, - phase3Result: s.phase3Result ? { - ...s.phase3Result, - whales: newWhales - } : undefined - })); + newWhales[groupIndex] = { ...newWhales[groupIndex], accounts: newWhales[groupIndex].accounts.filter((_, i) => i !== accountIndex) }; + setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, whales: newWhales } : undefined })); }; - const addRole = () => { if (!newRoleInput.trim() || !state.phase3Result) return; - const newItem = newRoleInput.trim(); - setState(s => ({ - ...s, - phase3Result: s.phase3Result ? { - ...s.phase3Result, - roles: [...s.phase3Result.roles, newItem] - } : undefined - })); + setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, roles: [...s.phase3Result.roles, newRoleInput.trim()] } : undefined })); setNewRoleInput(""); }; + const removeRole = (index: number) => setState(s => ({ ...s, phase3Result: s.phase3Result ? { ...s.phase3Result, roles: s.phase3Result.roles.filter((_, i) => i !== index) } : undefined })); - const removeRole = (index: number) => { - setState(s => ({ - ...s, - phase3Result: s.phase3Result ? { - ...s.phase3Result, - roles: s.phase3Result.roles.filter((_, i) => i !== index) - } : undefined - })); - }; - - const handlePromptChange = (index: number, newText: string) => { - if (!state.phase6Result) return; - - const newPrompts = [...state.phase6Result.visualPrompts]; - newPrompts[index] = { ...newPrompts[index], prompt: newText }; - - setState(s => ({ - ...s, - phase6Result: s.phase6Result ? { - ...s.phase6Result, - visualPrompts: newPrompts - } : undefined - })); - }; - - // --- Render Helpers --- - + // --- Render Functions --- const renderInputPhase = () => (
-
+

- - {labels.initTitle} + {labels.initTitle}

-
- +