From 7c35068b304f589c6f26c478fcd8f2526512c894 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 14 Jan 2026 08:42:14 +0000 Subject: [PATCH] feat(b2b-assistant): Implement Step 7 (Customer Journey) with tactical focus on Buying Center, Deal-Breakers, and Assets. Add restart functionality for individual steps. --- b2b-marketing-assistant/App.tsx | 59 +++++++++- b2b-marketing-assistant/components/Icons.tsx | 6 + .../components/StepDisplay.tsx | 50 ++++++++- b2b-marketing-assistant/constants.ts | 2 + b2b-marketing-assistant/services/export.ts | 2 +- b2b-marketing-assistant/types.ts | 1 + b2b_marketing_assistant_plan.md | 104 ++++-------------- b2b_marketing_orchestrator.py | 40 ++++++- 8 files changed, 166 insertions(+), 98 deletions(-) diff --git a/b2b-marketing-assistant/App.tsx b/b2b-marketing-assistant/App.tsx index c96401ab..1b2b7551 100644 --- a/b2b-marketing-assistant/App.tsx +++ b/b2b-marketing-assistant/App.tsx @@ -67,7 +67,7 @@ const App: React.FC = () => { const t = translations[inputData.language]; const STEP_TITLES = t.stepTitles; - const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages']; + const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages', 'customerJourney']; // --- AUTO-SAVE EFFECT --- useEffect(() => { @@ -260,9 +260,16 @@ const App: React.FC = () => { }; if (newAnalysisData.messages.rows.length === 0) newAnalysisData.messages.rows = parseSection("Step 6"); + newAnalysisData.customerJourney = { + summary: [], + headers: [], + rows: parseSection("Schritt 7") + }; + if (newAnalysisData.customerJourney.rows.length === 0) newAnalysisData.customerJourney.rows = parseSection("Step 7"); + setAnalysisData(newAnalysisData); - setGenerationStep(6); // Jump to end + setGenerationStep(7); // Jump to end (now 7) setProjectName(file.name.replace('.md', ' (Imported)')); setProjectId(null); // Treat as new project @@ -321,7 +328,7 @@ const App: React.FC = () => { }, [inputData]); const handleGenerateNextStep = useCallback(async () => { - if (generationStep >= 6) return; + if (generationStep >= 7) return; if (generationStep === 5 && !selectedIndustry) { setError('Bitte wählen Sie eine Fokus-Branche aus.'); @@ -340,7 +347,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, + focusIndustry: (generationStep === 5 || generationStep === 6) ? selectedIndustry : undefined, }), }); @@ -443,6 +450,45 @@ const App: React.FC = () => { } }; + const handleStepRestart = (stepKey: keyof AnalysisData) => { + const stepIndex = STEP_KEYS.indexOf(stepKey); + if (stepIndex === -1) return; + + // Reset steps from this index onwards + // But specifically, if I restart Step 2, I want generationStep to be 1 (so I can generate Step 2 again) + // If I restart Step 1 (index 0), I want generationStep to be 0 + + // But wait, if I restart Step 1 (Offer), that is the "Start Generation" button usually. + // Actually, if I restart Step 1, I basically want to clear everything and go back to step 0. + // If I restart Step 2, I want to keep Step 1, clear Step 2+, and go back to step 1 (ready to generate step 2). + + const newGenerationStep = stepIndex; + + setGenerationStep(newGenerationStep); + setAnalysisData(prev => { + const newData: Partial = { ...prev }; + // Remove all keys starting from this step + for (let i = stepIndex; i < STEP_KEYS.length; i++) { + delete newData[STEP_KEYS[i]]; + } + return newData; + }); + + // Also reset other state related to progress + if (stepKey === 'messages' || stepKey === 'targetGroups') { + // If we are resetting before Step 6, clear selection + // Actually if we reset Step 6 (messages), we clear Step 6 data. + if (stepIndex <= 5) { + setBatchStatus(null); + // We might want to keep selectedIndustry if we are just retrying Step 6? + // But the prompt implies "resetting", so clearing selection is safer. + setSelectedIndustry(''); + } + } + + // If resetting Step 7, clear its specific state if any (none currently) + }; + const handleDownloadMarkdown = () => { if (!analysisData) return; const markdownContent = generateMarkdown(analysisData as AnalysisData, STEP_TITLES, t.summaryTitle); @@ -478,6 +524,7 @@ const App: React.FC = () => { onEnrichRow={canAdd ? handleManualAdd : undefined} isEnriching={false} canDeleteRows={canDelete} + onRestart={() => handleStepRestart(stepKey)} t={t} /> ); @@ -661,9 +708,11 @@ const App: React.FC = () => { {generationStep >= 5 && renderStep('gains', STEP_TITLES.gains)} {renderContinueButton(6)} {generationStep >= 6 && renderStep('messages', STEP_TITLES.messages)} + {renderContinueButton(7)} + {generationStep >= 7 && renderStep('customerJourney', STEP_TITLES.customerJourney)} - {generationStep === 6 && !isLoading && ( + {generationStep === 7 && !isLoading && (
diff --git a/b2b-marketing-assistant/components/Icons.tsx b/b2b-marketing-assistant/components/Icons.tsx index 70d9014c..81849655 100644 --- a/b2b-marketing-assistant/components/Icons.tsx +++ b/b2b-marketing-assistant/components/Icons.tsx @@ -83,3 +83,9 @@ export const XMarkIcon: React.FC<{ className?: string }> = ({ className = '' }) ); + +export const RefreshIcon: React.FC<{ className?: string }> = ({ className = '' }) => ( + + + +); diff --git a/b2b-marketing-assistant/components/StepDisplay.tsx b/b2b-marketing-assistant/components/StepDisplay.tsx index 1438d0cc..ce86baa6 100644 --- a/b2b-marketing-assistant/components/StepDisplay.tsx +++ b/b2b-marketing-assistant/components/StepDisplay.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; -import { CopyIcon, ClipboardTableIcon, SearchIcon, TrashIcon, LoadingSpinner, CheckIcon, XMarkIcon } from './Icons'; +import { CopyIcon, ClipboardTableIcon, SearchIcon, TrashIcon, LoadingSpinner, CheckIcon, XMarkIcon, RefreshIcon } from './Icons'; import { convertArrayToTsv } from '../services/export'; import { translations } from '../constants'; @@ -14,16 +14,18 @@ interface StepDisplayProps { canDeleteRows?: boolean; onEnrichRow?: (productName: string, productUrl?: string) => Promise; isEnriching?: boolean; + onRestart?: () => void; t: typeof translations.de; } -export const StepDisplay: React.FC = ({ title, summary, headers, rows, onDataChange, canAddRows = false, canDeleteRows = false, onEnrichRow, isEnriching = false, t }) => { +export const StepDisplay: React.FC = ({ title, summary, headers, rows, onDataChange, canAddRows = false, canDeleteRows = false, onEnrichRow, isEnriching = false, onRestart, t }) => { const [copySuccess, setCopySuccess] = useState(''); const [filterQuery, setFilterQuery] = useState(''); const [isAddingRow, setIsAddingRow] = useState(false); const [newRowValue, setNewRowValue] = useState(''); const [newRowUrl, setNewRowUrl] = useState(''); const inputRef = useRef(null); + const [showRestartConfirm, setShowRestartConfirm] = useState(false); const filteredRows = useMemo(() => { if (!filterQuery) { @@ -140,6 +142,15 @@ export const StepDisplay: React.FC = ({ title, summary, header const newRows = rows.filter((_, index) => index !== rowIndexToDelete); onDataChange(newRows); }; + + const handleRestartClick = () => { + setShowRestartConfirm(true); + }; + + const handleRestartConfirm = () => { + setShowRestartConfirm(false); + if (onRestart) onRestart(); + }; const getColumnStyle = (header: string): React.CSSProperties => { const lowerHeader = header.toLowerCase(); @@ -169,9 +180,40 @@ export const StepDisplay: React.FC = ({ title, summary, header const isLoadingCell = (cell: string) => cell.toLowerCase().includes(loadingText.toLowerCase()); return ( -
+
-

{title}

+
+

{title}

+ {onRestart && ( +
+ {!showRestartConfirm ? ( + + ) : ( +
+ Wirklich neu starten? + + +
+ )} +
+ )} +