From fc79f70e9c2628ae05bb8551139e692cf3b64dfa Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 23 Dec 2025 12:50:10 +0000 Subject: [PATCH] feat(b2b): Add batch processing, industry selection, optimized PDF export, and update docs --- b2b-marketing-assistant/App.tsx | 147 +++++++++++- .../components/StepDisplay.tsx | 18 +- b2b-marketing-assistant/server.cjs | 6 +- b2b_marketing_assistant_plan.md | 214 +++++++++++++++++- b2b_marketing_orchestrator.py | 24 +- 5 files changed, 391 insertions(+), 18 deletions(-) diff --git a/b2b-marketing-assistant/App.tsx b/b2b-marketing-assistant/App.tsx index 50d2d158..71d22e10 100644 --- a/b2b-marketing-assistant/App.tsx +++ b/b2b-marketing-assistant/App.tsx @@ -22,6 +22,8 @@ const App: React.FC = () => { 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 t = translations[inputData.language]; const STEP_TITLES = t.stepTitles; @@ -37,6 +39,8 @@ const App: React.FC = () => { setError(null); setAnalysisData({}); setGenerationStep(0); + setSelectedIndustry(''); + setBatchStatus(null); try { const response = await fetch(`${API_BASE_URL}/start-generation`, { @@ -75,6 +79,11 @@ const App: React.FC = () => { const handleGenerateNextStep = useCallback(async () => { if (generationStep >= 6) return; + if (generationStep === 5 && !selectedIndustry) { + setError('Bitte wählen Sie eine Fokus-Branche aus.'); + return; + } + setIsLoading(true); setError(null); @@ -87,6 +96,7 @@ const App: React.FC = () => { language: inputData.language, channels: inputData.channels, generationStep: generationStep + 1, // Pass the step we want to generate + focusIndustry: generationStep === 5 ? selectedIndustry : undefined, }), }); @@ -111,7 +121,77 @@ const App: React.FC = () => { } finally { setIsLoading(false); } - }, [analysisData, generationStep, inputData.channels, inputData.language]); + }, [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]) { @@ -154,6 +234,59 @@ const App: React.FC = () => { 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 (
-
+
@@ -275,6 +275,22 @@ export const StepDisplay: React.FC = ({ title, summary, header
+ {/* Print-Only Block View for readable PDFs */} +
+ {filteredRows.map((row, rowIndex) => ( +
+ {headers.map((header, colIndex) => ( +
+

{header}

+
+ {row[colIndex]} +
+
+ ))} +
+ ))} +
+ {canAddRows && (
{isAddingRow ? ( diff --git a/b2b-marketing-assistant/server.cjs b/b2b-marketing-assistant/server.cjs index 1d38fe7b..cf71ad50 100644 --- a/b2b-marketing-assistant/server.cjs +++ b/b2b-marketing-assistant/server.cjs @@ -101,7 +101,7 @@ app.post('/api/start-generation', (req, res) => { // API-Endpunkt, um den nächsten Schritt zu generieren app.post('/api/next-step', (req, res) => { console.log(`[${new Date().toISOString()}] HIT: /api/next-step`); - const { analysisData, language, channels, generationStep } = req.body; + const { analysisData, language, channels, generationStep, focusIndustry } = req.body; if (!analysisData || !language || generationStep === undefined) { return res.status(400).json({ error: 'Missing required parameters: analysisData, language, generationStep.' }); @@ -128,6 +128,10 @@ app.post('/api/next-step', (req, res) => { if (channels && Array.isArray(channels)) { args.push('--channels', channels.join(',')); } + + if (focusIndustry) { + args.push('--focus_industry', focusIndustry); + } // Da die runPythonScript-Funktion res behandelt, fügen wir hier die Bereinigung hinzu const originalJson = res.json.bind(res); diff --git a/b2b_marketing_assistant_plan.md b/b2b_marketing_assistant_plan.md index 089b6c65..a61e02eb 100644 --- a/b2b_marketing_assistant_plan.md +++ b/b2b_marketing_assistant_plan.md @@ -87,16 +87,220 @@ Dieses Projekt ist der erste Schritt zur Schaffung eines einheitlichen "Strategy -## 5. Status: Produktionsbereit +## 5. Deployment & Betrieb -Das System liefert nun hochqualitative, faktenbasierte Analysen ("Grounding"), die weit über die ursprüngliche Online-Version hinausgehen. Alle bekannten Fehler (Timeouts, API 404, Copy-Paste) sind behoben. -### Nächste Schritte (Optional) -- Erweiterung auf Multi-Language Support im Frontend (aktuell DE fokussiert). +Da das Frontend (`App.tsx`) in das Docker-Image kompiliert wird, müssen Änderungen am Code durch einen **Rebuild** aktiviert werden. + + + + + + + +### Standard-Start (für Nutzung) + + + +Wenn das Image bereits aktuell ist: + + + +```bash + + + +docker run -d -p 3004:3002 --name b2b-assistant-instance \ + + + + -v "$(pwd)/b2b_marketing_orchestrator.py:/app/b2b_marketing_orchestrator.py" \ + + + + -v "$(pwd)/b2b-marketing-assistant/server.cjs:/app/server.cjs" \ + + + + -v "$(pwd)/gemini_api_key.txt:/app/gemini_api_key.txt" \ + + + + -v "$(pwd)/Log_from_docker:/app/Log_from_docker" \ + + + + b2b-marketing-assistant + + + +``` + + + +Das Tool ist dann unter `http://localhost:3004` erreichbar. Logs finden Sie im Ordner `Log_from_docker`. + + + + + + + +### Update & Rebuild (nach Code-Änderungen) + + + +Wenn Sie `App.tsx`, `index.html` oder `package.json` geändert haben, **müssen** Sie neu bauen: + + + + + + + +1. **Alten Container entfernen:** + + + + ```bash + + + + docker stop b2b-assistant-instance + + + + docker rm b2b-assistant-instance + + + + ``` + + + +2. **Image neu bauen:** + + + + ```bash + + + + docker build -t b2b-marketing-assistant -f b2b-marketing-assistant/Dockerfile . + + + + ``` + + + +3. **Neu starten:** (siehe Befehl oben). + + + + + + + +--- + + + + + + + +## 6. Roadmap: Nächste Erweiterungen + + + + + + + +### Priorität 1: Persistenz & Dashboard (SQLite) + + + +Um Datenverlust zu vermeiden und Analysen wiederaufnehmbar zu machen. + + + +* **Backend:** Integration einer SQLite-Datenbank (`projects.db`). + + + +* **API:** Endpunkte für `save_project`, `load_project`, `list_projects`. + + + +* **Frontend:** Dashboard-Ansicht ("Letzte Analysen") und Speicher-Automatik nach jedem Schritt. + + + +* **Nutzen:** Ermöglicht Batch-Runs über Nacht und das spätere Verfeinern von Analysen ohne Neustart. + + + + + + + +### Priorität 2: Asset Factory (Schritt 7) + + + +Umwandlung der strategischen Botschaften in operative Marketing-Texte. + + + +* **UI:** Neuer Bereich "Assets generieren" nach Abschluss von Schritt 6. + + + +* **Funktion:** Auswahl einer Persona und eines Formats (z.B. "LinkedIn Vernetzungsanfrage", "Cold Mail Sequenz"). + + + +* **Output:** Generierung von Copy-Paste-fertigen Texten basierend auf den Painpoints/Gains der Analyse. + + + +* **Export:** Als separater "Marketing Kit" Download oder Anhang im Markdown. + + + + + + + +### Status: Produktionsbereit (Version 1.1) + + + +Das System liefert nun hochqualitative, faktenbasierte Analysen ("Grounding") mit HTML-Struktur-Erkennung. + + + +- [x] Grounding (HTML-Parsing) & Gemini 2.5 Flash + + + +- [x] Robustheit (Retries, Timeouts) + + + +- [x] Frontend-Optimierung (PDF, Copy-Paste, Batch-Analyse) + + + +- [x] Logging (File-basiert) + + + -- Integration von SerpAPI für noch breitere Marktrecherchen (analog Market Intel). diff --git a/b2b_marketing_orchestrator.py b/b2b_marketing_orchestrator.py index 4330740c..801870f4 100644 --- a/b2b_marketing_orchestrator.py +++ b/b2b_marketing_orchestrator.py @@ -127,16 +127,16 @@ Fuehre nun **Schritt 6 - Marketingbotschaft (WIE sprechen)** durch. # Anweisungen fuer Schritt 6: Chain-of-Thought-Analyse & Texterstellung -**FOKUS:** Um die Analyse handhabbar zu machen, waehle aus Schritt 2 die **eine (1) relevanteste und vielversprechendste Zielbranche** (Primary Industry) aus. -Dein Ziel ist es, NUR fuer diese EINE Fokus-Branche eine spezifische Botschaft fuer JEDE Rolle aus Schritt 3 zu erstellen. +**FOKUS:** Erstelle die Botschaften **AUSSCHLIESSLICH** fuer die vorgegebene **Fokus-Branche: {{focus_industry}}**. +Ignoriere alle anderen Branchen. Dein Ziel ist es, fuer JEDE Rolle innerhalb dieser EINEN Branche eine spezifische Botschaft zu entwickeln. -Fuehre fuer jede **[Rolle]** innerhalb der ausgewaehlten **[Fokus-Branche]** den folgenden Denkprozess durch: +Fuehre fuer jede **[Rolle]** innerhalb der **[Fokus-Branche: {{focus_industry}}]** den folgenden Denkprozess durch: 1. **Schritt 6.1 (Analyse): Produkt-Rollen-Fit.** * Welches Produkt/welche Loesung aus der "Angebot"-Tabelle (Schritt 1) ist fuer die **[Rolle]** am relevantesten? 2. **Schritt 6.2 (Analyse): Branchen-Use-Case.** - * Was sind 1-2 typische Anwendungsfaelle fuer das ausgewaehlte Produkt in der **[Fokus-Branche]**? Was macht die **[Rolle]** damit konkret? + * Was sind 1-2 typische Anwendungsfaelle fuer das ausgewaehlte Produkt in der **{{focus_industry}}**? Was macht die **[Rolle]** damit konkret? 3. **Schritt 6.3 (Analyse): Nutzen-Quantifizierung.** * Betrachte die Painpoints (Schritt 4) und Gains (Schritt 5) fuer die **[Rolle]**. @@ -234,10 +234,10 @@ Now perform **Step 6 - Marketing Message (HOW to speak)**. # Instructions for Step 6: Chain-of-Thought Analysis & Copywriting -**FOCUS:** To make the analysis manageable, select the **one (1) most relevant and promising target industry** (Primary Industry) from Step 2. -Your goal is to create a specific message for EACH role from Step 3 ONLY for this ONE focus industry. +**FOCUS:** Create messages **EXCLUSIVELY** for the provided **Focus Industry: {{focus_industry}}**. +Ignore all other industries. Your goal is to create a specific message for EACH role within this ONE industry. -For each **[Role]** within the selected **[Focus Industry]**, perform the following thought process: +For each **[Role]** within the **[Focus Industry: {{focus_industry}}]**, perform the following thought process: 1. **Step 6.1 (Analysis): Product-Role Fit.** * Which product/solution from the "Offer" table (Step 1) is most relevant for the **[Role]**? @@ -496,7 +496,7 @@ def start_generation(url, language, regions, focus): "offer": {"summary": current_prompts['SUMMARY_TEXT_FOR_STEP1'], "headers": table_data['headers'], "rows": table_data['rows']} } -def next_step(language, context_file, generation_step, channels): +def next_step(language, context_file, generation_step, channels, focus_industry=None): logging.info(f"Starting Step {generation_step} in language: {language}") api_key = load_api_key() if not api_key: raise ValueError("Gemini API key is missing.") @@ -507,6 +507,11 @@ def next_step(language, context_file, generation_step, channels): previous_steps_markdown = format_context_for_prompt(analysis_data, language) prompt = step_prompt_template.replace('{{previous_steps_data}}', previous_steps_markdown) if '{{channels}}' in prompt: prompt = prompt.replace('{{channels}}', channels or 'LinkedIn, Kaltmail, Landingpage') + + # Inject focus industry if provided (for Step 6) + if '{{focus_industry}}' in prompt: + prompt = prompt.replace('{{focus_industry}}', focus_industry or 'Primary Industry') + initial_inputs = analysis_data.get('_initial_inputs', {}) # Helper to safely get string values even if they are None/null in the JSON @@ -546,10 +551,11 @@ def main(): parser.add_argument('--generation_step', type=int) parser.add_argument('--channels') parser.add_argument('--language', required=True) + parser.add_argument('--focus_industry') # New argument args = parser.parse_args() try: if args.mode == 'start_generation': result = start_generation(args.url, args.language, args.regions, args.focus) - elif args.mode == 'next_step': result = next_step(args.language, args.context_file, args.generation_step, args.channels) + elif args.mode == 'next_step': result = next_step(args.language, args.context_file, args.generation_step, args.channels, args.focus_industry) sys.stdout.write(json.dumps(result, ensure_ascii=False)) except Exception as e: logging.error(f"Error: {e}", exc_info=True)