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}
-
-
+
-
-
+
+
@@ -760,36 +669,18 @@ const App: React.FC = () => {
-
+ {state.isLoading ?
{labels.processing} : <>{labels.startBtn}
>}
{state.isLoading && (
-
@@ -799,788 +690,218 @@ const App: React.FC = () => {
const renderPhase1 = () => (
-
-
- {labels.features} & Constraints
-
+
+
{labels.features} & Constraints
- {/* Features List */}
-
{labels.features}
+
{labels.features}
{state.phase1Result?.features.map((f, i) => (
- -
-
-
+
-
+ {f}
+
))}
-
- {/* Constraints List */}
-
{labels.constraints}
+
{labels.constraints}
{state.phase1Result?.constraints.map((c, i) => (
- -
-
-
+
-
+ {c}
+
))}
-
- {state.phase1Result?.conflictCheck.hasConflict ? (
-
-
-
-
{labels.conflictTitle}
-
{state.phase1Result.conflictCheck.details}
- {state.phase1Result.conflictCheck.relatedProduct && (
-
- Conflict with: {state.phase1Result.conflictCheck.relatedProduct}
-
- )}
-
-
- ) : (
-
-
- {labels.conflictPass}
-
- )}
-
-
-
+
+
);
const renderPhase2 = () => (
-
-
- {labels.targetId}
-
- {state.phase2Result?.icps.map((icp, i) => (
-
-
-
- {i + 1}. {icp.name}
-
-
{icp.rationale}
-
- ))}
- {/* ADD ICP INPUT */}
-
-
-
setNewICPName(e.target.value)}
- className="text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
- bg-white border-slate-200 text-slate-800
- dark:bg-robo-900 dark:border-robo-600 dark:text-white"
- placeholder={labels.addName}
- />
-
-
setNewICPRationale(e.target.value)}
- onKeyDown={e => e.key === 'Enter' && addICP()}
- className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
- bg-white border-slate-200 text-slate-800
- dark:bg-robo-900 dark:border-robo-600 dark:text-white"
- placeholder={labels.addRationale}
- />
-
-
-
+
{labels.targetId}
+ {state.phase2Result?.icps.map((icp, i) => (
+
+
+
{i + 1}. {icp.name}
+
{icp.rationale}
-
-
-
-
- {labels.dataProxies}
-
-
- {state.phase2Result?.dataProxies.map((proxy, i) => (
-
-
- PROXY_0{i+1}
-
-
-
{proxy.target}
-
{labels.method}: {proxy.method}
-
-
-
- ))}
- {/* ADD PROXY INPUT */}
-
-
-
-
+ ))}
-
-
+
+
);
const renderPhase3 = () => (
-
- {labels.region}
-
-
-
- {/* Whales Groups */}
-
-
- {labels.whales}
-
-
-
- {state.phase3Result?.whales.map((whaleGroup, groupIdx) => (
-
-
-
- Industry
- {whaleGroup.industry}
-
-
-
-
- {whaleGroup.accounts.map((acc, accIdx) => (
- -
-
-
- {acc.substring(0,2).toUpperCase()}
-
-
{acc}
-
-
-
- ))}
-
-
-
-
setNewAccountInputs({...newAccountInputs, [groupIdx]: e.target.value})}
- onKeyDown={e => e.key === 'Enter' && addAccount(groupIdx)}
- className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-amber-500
- bg-slate-50 border-slate-200 text-slate-800
- dark:bg-robo-900 dark:border-robo-600 dark:text-white
- "
- placeholder={labels.addAccount}
- />
-
-
-
- ))}
-
-
-
- {/* Personas */}
-
-
- {labels.roles}
-
-
- {state.phase3Result?.roles.map((role, i) => (
- -
-
-
-
- ))}
-
-
-
setNewRoleInput(e.target.value)}
- onKeyDown={e => e.key === 'Enter' && addRole()}
- className="flex-1 text-sm border rounded px-3 py-2 outline-none focus:ring-2 focus:ring-blue-500
- bg-white border-slate-200 text-slate-800
- dark:bg-robo-900 dark:border-robo-600 dark:text-white
- "
- placeholder={labels.addRole}
- />
-
+
+ {state.phase3Result?.whales.map((w, idx) => (
+
+
{w.industry}
+
+ {w.accounts.map((acc, aidx) => (
+ -
+ {acc}
+
+
+ ))}
+
-
+ ))}
-
-
-
+
+
);
const renderPhase4 = () => (
-
- {labels.stratAngles}
-
-
-
-
-
-
- | {labels.segment} |
- {labels.pain} |
- {labels.angle} |
- {labels.diff} |
-
+ {labels.stratAngles}
+
+
+
+ | {labels.segment} | {labels.pain} | {labels.angle} |
-
+
{state.phase4Result?.strategyMatrix.map((row, i) => (
-
- | {row.segment} |
- {row.painPoint} |
- {row.angle} |
- {row.differentiation} |
+
+ | {row.segment} |
+ {row.painPoint} |
+ {row.angle} |
))}
-
-
-
+
+
);
const renderPhase5 = () => (
-
-
- {labels.genAssets}
-
-
+
+ {state.phase5Result || ''}
-
-
-
,
- h2: ({node, ...props}) => ,
- h3: ({node, ...props}) => ,
- p: ({node, ...props}) => ,
- ul: ({node, ...props}) => ,
- li: ({node, ...props}) => ,
- strong: ({node, ...props}) => ,
- // Table support
- table: ({node, ...props}) => (
-
- ),
- thead: ({node, ...props}) => (
-
- ),
- tbody: ({node, ...props}) => (
-
- ),
- tr: ({node, ...props}) => (
-
- ),
- th: ({node, ...props}) => (
- |
- ),
- td: ({node, ...props}) => (
- |
- ),
- }}
- >
- {state.phase5Result || ''}
-
-
-
-
-
+
+
);
const renderPhase6 = () => (
-
-
- {labels.phase6Title}
-
-
-
-
-
- {/* Battlecards */}
-
-
- {labels.battlecards}
-
-
- {state.phase6Result?.battlecards.map((card, i) => (
-
-
-
- {card.persona}
-
-
-
-
- {labels.objection}
- "{card.objection}"
-
-
- {labels.response}
- {card.responseScript}
-
-
-
- ))}
+
{labels.phase6Title}
+
+ {state.phase6Result?.battlecards.map((card, i) => (
+
+
{card.persona}
+
"{card.objection}"
+
{card.responseScript}
-
-
- {/* Visual Prompts */}
-
-
- {labels.visuals}
-
-
- {state.phase6Result?.visualPrompts.map((prompt, i) => (
-
-
-
-
-
{prompt.title}
-
{prompt.context}
-
-
-
-
-
-
-
-
-
- {generatedImages[i] && (
-
- {editingIndex === i ? (
-
-
-
-
-
-
-
- setEditingIndex(null)}
- className="px-3 py-1.5 text-xs font-bold bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200 rounded shadow-md hover:bg-slate-100 dark:hover:bg-slate-700"
- >
- {labels.cancelEdit}
-
- handleRegenerateWithSketch(i, prompt.prompt)}
- className="px-3 py-1.5 text-xs font-bold bg-blue-600 text-white rounded shadow-md hover:bg-blue-700 flex items-center gap-1.5"
- >
- {labels.regenerateSketch}
-
-
-
- ) : (
-
-

-
-
setEditingIndex(i)}
- className="bg-white/90 dark:bg-black/70 hover:bg-white dark:hover:bg-black p-2 rounded-full text-slate-800 dark:text-white shadow-sm backdrop-blur-sm"
- title={labels.editImage}
- >
-
-
-
downloadAsset(generatedImages[i], i)}
- className="bg-white/90 dark:bg-black/70 hover:bg-white dark:hover:bg-black p-2 rounded-full text-slate-800 dark:text-white shadow-sm backdrop-blur-sm"
- title={labels.downloadAsset}
- >
-
-
-
-
Generated by Gemini 2.5 Flash Image (Nano Banana)
-
- )}
-
- )}
-
- ))}
-
-
+ ))}
-
-
-
-
- {labels.backBtn}
-
-
{labels.complete}
-
-
window.location.reload()}
- className="text-sm font-medium hover:underline text-slate-500 hover:text-slate-800 dark:text-slate-300 dark:hover:text-white"
- >
- {labels.restart}
-
+
+
{labels.backBtn}
+
window.location.reload()} className="text-sm font-medium hover:underline text-slate-500">{labels.restart}
);
return (
- {error && (
-
-
- {error}
-
+ {/* Top Bar Actions */}
+
+
+
+
+
+
+
+
+
+ {/* History Modal */}
+ {showHistory && (
+
+
+
+
{labels.history}
+ setShowHistory(false)} className="p-1 hover:bg-slate-200 dark:hover:bg-robo-600 rounded-full">
+
+
+ {savedProjects.length === 0 ? (
+
{labels.noProjects}
+ ) : (
+
+ {savedProjects.map((p) => (
+
handleLoadProject(p.id)} className="group cursor-pointer p-4 rounded-lg border border-slate-200 dark:border-robo-700 hover:border-blue-400 dark:hover:border-robo-500 bg-slate-50 dark:bg-robo-800/50 transition-all hover:shadow-md relative">
+
{p.name}
+
+ {new Date(p.updated_at).toLocaleString()}
+ handleDeleteProject(p.id, e)}
+ className="text-red-400 hover:text-red-600 p-1 -m-1 rounded opacity-0 group-hover:opacity-100 transition-opacity"
+ title={labels.delete}
+ >
+
+
+
+
+ ))}
+
+ )}
+
+
+
)}
+ {error && {error}
}
+
{state.currentPhase === Phase.Input && renderInputPhase()}
{state.currentPhase === Phase.ProductAnalysis && renderPhase1()}
{state.currentPhase === Phase.ICPDiscovery && renderPhase2()}
diff --git a/gtm-architect/Dockerfile b/gtm-architect/Dockerfile
new file mode 100644
index 00000000..e99dcac0
--- /dev/null
+++ b/gtm-architect/Dockerfile
@@ -0,0 +1,28 @@
+# Base image for Python
+FROM python:3.10-slim
+
+# Set the working directory
+WORKDIR /app
+
+# Copy shared modules first
+COPY helpers.py /app/
+COPY config.py /app/
+
+# Copy application-specific files
+COPY gtm_architect_orchestrator.py /app/
+COPY gtm-architect /app/gtm-architect
+
+# Install Python dependencies
+RUN pip install --no-cache-dir -r /app/gtm-architect/requirements.txt
+
+# Install Node.js and npm
+RUN apt-get update && apt-get install -y nodejs npm && npm cache clean --force
+
+# Install Node.js dependencies for the frontend bridge
+RUN npm install --prefix /app/gtm-architect
+
+# Expose the port the app runs on
+EXPOSE 3005
+
+# Command to run the application
+CMD ["node", "/app/gtm-architect/server.cjs"]
diff --git a/gtm-architect/requirements.txt b/gtm-architect/requirements.txt
new file mode 100644
index 00000000..60c9f385
--- /dev/null
+++ b/gtm-architect/requirements.txt
@@ -0,0 +1,10 @@
+openai
+pandas
+gspread
+oauth2client
+beautifulsoup4
+requests
+python-dotenv
+tiktoken
+thefuzz
+serpapi
diff --git a/gtm-architect/server.cjs b/gtm-architect/server.cjs
new file mode 100644
index 00000000..5c885196
--- /dev/null
+++ b/gtm-architect/server.cjs
@@ -0,0 +1,62 @@
+const express = require('express');
+const { spawn } = require('child_process');
+const app = express();
+const port = 3005; // Port for the GTM Architect service
+
+app.use(express.json({ limit: '50mb' }));
+
+// Middleware to serve static files from the React app
+app.use(express.static('.'));
+
+function callPythonScript(mode, data, res) {
+ const pythonProcess = spawn('python3', ['../gtm_architect_orchestrator.py', '--mode', mode]);
+
+ let pythonData = '';
+ let errorData = '';
+
+ pythonProcess.stdout.on('data', (data) => {
+ pythonData += data.toString();
+ });
+
+ pythonProcess.stderr.on('data', (data) => {
+ errorData += data.toString();
+ });
+
+ pythonProcess.on('close', (code) => {
+ if (code !== 0) {
+ console.error(`Python script exited with code ${code}`);
+ console.error('Stderr:', errorData);
+ return res.status(500).json({ error: 'Python script execution failed.', details: errorData });
+ }
+ try {
+ const result = JSON.parse(pythonData);
+ res.json(result);
+ } catch (e) {
+ console.error('Failed to parse Python script output:', e);
+ console.error('Raw output:', pythonData);
+ res.status(500).json({ error: 'Failed to parse Python script output.', details: pythonData });
+ }
+ });
+
+ pythonProcess.stdin.write(JSON.stringify(data));
+ pythonProcess.stdin.end();
+}
+
+// API endpoint to handle requests from the frontend
+app.post('/api/gtm', (req, res) => {
+ const { mode, data } = req.body;
+ if (!mode || !data) {
+ return res.status(400).json({ error: 'Missing mode or data in request body' });
+ }
+ callPythonScript(mode, data, res);
+});
+
+// Serve the main index.html for any other requests to support client-side routing
+app.get('*', (req, res) => {
+ res.sendFile(__dirname + '/index.html');
+});
+
+
+app.listen(port, () => {
+ console.log(`GTM Architect server listening at http://localhost:${port}`);
+});
diff --git a/gtm_architect_orchestrator.py b/gtm_architect_orchestrator.py
new file mode 100644
index 00000000..63e84757
--- /dev/null
+++ b/gtm_architect_orchestrator.py
@@ -0,0 +1,107 @@
+import os
+import sys
+import json
+import argparse
+from pathlib import Path
+
+# Add project root to Python path
+project_root = Path(__file__).resolve().parents[1]
+sys.path.append(str(project_root))
+
+from helpers import call_openai_chat
+import market_db_manager
+
+# Ensure DB is ready
+market_db_manager.init_db()
+
+def get_system_instruction(lang):
+ if lang == 'de':
+# ... (rest of get_system_instruction remains same)
+ else:
+ return """
+# IDENTITY & PURPOSE
+You are the "GTM Architect Engine" for Roboplanet. Your task is to develop a precise Go-to-Market strategy for new technical products (robots).
+# ... (rest of english instructions)
+"""
+
+# --- Database Handlers ---
+
+def save_project_handler(data):
+ # data contains the full application state
+ # Ensure we have a name for the list view
+ if 'name' not in data:
+ # Try to derive a name from product input or phases
+ input_text = data.get('productInput', '')
+ # Take first 30 chars or first line
+ derived_name = input_text.split('\n')[0][:30] if input_text else "Untitled Strategy"
+ data['name'] = derived_name
+
+ result = market_db_manager.save_project(data)
+ print(json.dumps(result))
+
+def list_projects_handler(data):
+ # data is ignored here
+ projects = market_db_manager.get_all_projects()
+ print(json.dumps(projects))
+
+def load_project_handler(data):
+ project_id = data.get('id')
+ project = market_db_manager.load_project(project_id)
+ if project:
+ print(json.dumps(project))
+ else:
+ print(json.dumps({"error": "Project not found"}))
+
+def delete_project_handler(data):
+ project_id = data.get('id')
+ result = market_db_manager.delete_project(project_id)
+ print(json.dumps(result))
+
+# --- AI Handlers ---
+
+def analyze_product(data):
+# ... (rest of analyze_product and other AI handlers remain same)
+
+def main():
+ parser = argparse.ArgumentParser(description="GTM Architect Orchestrator")
+ parser.add_argument("--mode", type=str, required=True, help="Execution mode")
+ args = parser.parse_args()
+
+ # Read stdin only if there is input, otherwise data is empty dict
+ if not sys.stdin.isatty():
+ try:
+ data = json.loads(sys.stdin.read())
+ except json.JSONDecodeError:
+ data = {}
+ else:
+ data = {}
+
+ if args.mode == "analyze_product":
+ analyze_product(data)
+ elif args.mode == "discover_icps":
+ discover_icps(data)
+ elif args.mode == "hunt_whales":
+ hunt_whales(data)
+ elif args.mode == "develop_strategy":
+ develop_strategy(data)
+ elif args.mode == "generate_assets":
+ generate_assets(data)
+ elif args.mode == "generate_sales_enablement":
+ generate_sales_enablement(data)
+ # DB Modes
+ elif args.mode == "save_project":
+ save_project_handler(data)
+ elif args.mode == "list_projects":
+ list_projects_handler(data)
+ elif args.mode == "load_project":
+ load_project_handler(data)
+ elif args.mode == "delete_project":
+ delete_project_handler(data)
+ else:
+ print(json.dumps({"status": "error", "message": f"Unknown mode: {args.mode}"}))
+
+if __name__ == "__main__":
+ main()
+
+if __name__ == "__main__":
+ main()
diff --git a/gtm_architect_plan.md b/gtm_architect_plan.md
new file mode 100644
index 00000000..a2049746
--- /dev/null
+++ b/gtm_architect_plan.md
@@ -0,0 +1,23 @@
+# Plan: Umsetzung des "GTM Architect" Backends
+
+Dieses Dokument beschreibt den Plan zur Umsetzung der Backend-Logik für die React-Anwendung unter `/gtm-architect` als robusten, faktenbasierten Python-Service.
+
+## 1. Zielsetzung & Architektur
+
+- **Ziel:** Umwandlung der reinen Frontend-Anwendung in einen Service mit einem Python-Backend.
+- **Architektur:** Wir replizieren den bewährten Aufbau der anderen Tools:
+ 1. **React-Frontend:** Die Benutzeroberfläche in `/gtm-architect` bleibt bestehen.
+ 2. **Node.js API-Brücke (`server.cjs`):** Ein Express.js-Server, der Anfragen vom Frontend annimmt und an das Python-Backend weiterleitet.
+ 3. **Python-Orchestrator (`gtm_architect_orchestrator.py`):** Das neue Herzstück, das die gesamte Logik kapselt.
+
+## 2. Fortschritts-Log
+
+### Phase 1: Initialisierung & Planung
+- [ ] Anforderungsanalyse und Zieldefinition.
+- [ ] Architektur nach Vorbild `b2b-marketing-assistant` und `market-intel-backend` festgelegt.
+- [ ] Diesen Schlachtplan in `gtm_architect_plan.md` erstellt.
+- [ ] Aufbau der Grundstruktur: Erstellen der `gtm_architect_orchestrator.py`, der `server.cjs` in `/gtm-architect` und des `Dockerfile`.
+- [ ] Erstellung von `package.json` und `requirements.txt`.
+- [ ] Anpassung des Frontends (`App.tsx`) für die Kommunikation mit dem neuen Backend.
+- [ ] Portierung der Logik aus `geminiService.ts` nach Python.
+- [ ] Integration in `docker-compose.yml` und `nginx-proxy.conf`.
diff --git a/nginx-proxy.conf b/nginx-proxy.conf
index e86b7912..ae9fcfa6 100644
--- a/nginx-proxy.conf
+++ b/nginx-proxy.conf
@@ -10,10 +10,10 @@ http {
error_log /dev/stderr;
# Increase Timeouts for Long-Running AI Tasks
- proxy_read_timeout 600s;
- proxy_connect_timeout 600s;
- proxy_send_timeout 600s;
- send_timeout 600s;
+ proxy_read_timeout 1200s;
+ proxy_connect_timeout 1200s;
+ proxy_send_timeout 1200s;
+ send_timeout 1200s;
# Resolver ist wichtig für Docker
resolver 127.0.0.11 valid=30s ipv6=off;
@@ -37,6 +37,11 @@ http {
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
+
+ # Explicit timeouts for this location
+ proxy_read_timeout 1200s;
+ proxy_connect_timeout 1200s;
+ proxy_send_timeout 1200s;
}
location /market/ {
@@ -45,6 +50,24 @@ http {
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
+
+ # Explicit timeouts for this location
+ proxy_read_timeout 1200s;
+ proxy_connect_timeout 1200s;
+ proxy_send_timeout 1200s;
+ }
+
+ location /gtm/ {
+ # Der Trailing Slash am Ende ist wichtig!
+ proxy_pass http://gtm-app:3005/;
+ proxy_set_header Host $host;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # Explicit timeouts for this location
+ proxy_read_timeout 1200s;
+ proxy_connect_timeout 1200s;
+ proxy_send_timeout 1200s;
}
}
}