feat(b2b-assistant): Implement Step 7 (Customer Journey) with tactical focus on Buying Center, Deal-Breakers, and Assets. Add restart functionality for individual steps.

This commit is contained in:
2026-01-14 08:42:14 +00:00
parent 5283f8a84b
commit 7c35068b30
8 changed files with 166 additions and 98 deletions

View File

@@ -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<AnalysisData> = { ...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 && (
<div className="p-8 bg-sky-50 dark:bg-sky-900/20 rounded-2xl border border-sky-100 dark:border-sky-800 text-center print:hidden">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-sky-100 dark:bg-sky-800 mb-4">
<SparklesIcon className="h-6 w-6 text-sky-600 dark:text-sky-300" />

View File

@@ -83,3 +83,9 @@ export const XMarkIcon: React.FC<{ className?: string }> = ({ className = '' })
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
);
export const RefreshIcon: React.FC<{ className?: string }> = ({ className = '' }) => (
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={`h-6 w-6 ${className}`}>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
);

View File

@@ -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<void>;
isEnriching?: boolean;
onRestart?: () => void;
t: typeof translations.de;
}
export const StepDisplay: React.FC<StepDisplayProps> = ({ title, summary, headers, rows, onDataChange, canAddRows = false, canDeleteRows = false, onEnrichRow, isEnriching = false, t }) => {
export const StepDisplay: React.FC<StepDisplayProps> = ({ 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<HTMLInputElement>(null);
const [showRestartConfirm, setShowRestartConfirm] = useState(false);
const filteredRows = useMemo(() => {
if (!filterQuery) {
@@ -140,6 +142,15 @@ export const StepDisplay: React.FC<StepDisplayProps> = ({ 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<StepDisplayProps> = ({ title, summary, header
const isLoadingCell = (cell: string) => cell.toLowerCase().includes(loadingText.toLowerCase());
return (
<section className="bg-white dark:bg-slate-800/50 rounded-2xl shadow-lg p-6 md:p-8 border border-slate-200 dark:border-slate-700 print:shadow-none print:border-none print:[break-inside:avoid]">
<section className="bg-white dark:bg-slate-800/50 rounded-2xl shadow-lg p-6 md:p-8 border border-slate-200 dark:border-slate-700 print:shadow-none print:border-none print:[break-inside:avoid] relative">
<div className="flex flex-col sm:flex-row justify-between sm:items-start mb-6 gap-4">
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">{title}</h2>
<div className="flex items-center gap-4">
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">{title}</h2>
{onRestart && (
<div className="relative print:hidden">
{!showRestartConfirm ? (
<button
onClick={handleRestartClick}
className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-full transition-colors"
title="Diesen Schritt neu starten (löscht nachfolgende Schritte)"
>
<RefreshIcon className="h-5 w-5" />
</button>
) : (
<div className="absolute top-0 left-0 z-10 flex items-center bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-lg rounded-lg p-1 animate-fade-in whitespace-nowrap">
<span className="text-xs font-semibold text-slate-700 dark:text-slate-300 mx-2">Wirklich neu starten?</span>
<button
onClick={handleRestartConfirm}
className="p-1 text-green-600 hover:bg-green-50 dark:hover:bg-green-900/30 rounded"
>
<CheckIcon className="h-4 w-4" />
</button>
<button
onClick={() => setShowRestartConfirm(false)}
className="p-1 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded"
>
<XMarkIcon className="h-4 w-4" />
</button>
</div>
)}
</div>
)}
</div>
<div className="relative ml-auto">
<button
onClick={handleCopyTable}

View File

@@ -53,6 +53,7 @@ export const translations = {
painPoints: 'Schritt 4: Painpoints je Rolle (WARUM)',
gains: 'Schritt 5: Gains & Nutzen je Rolle (WARUM wechseln)',
messages: 'Schritt 6: Marketingbotschaften je Segment & Rolle (WIE sprechen)',
customerJourney: 'Schritt 7: Customer Journey & Buying Center',
}
},
en: {
@@ -101,6 +102,7 @@ export const translations = {
painPoints: 'Step 4: Pain Points per Role (WHY)',
gains: 'Step 5: Gains & Benefits per Role (WHY switch)',
messages: 'Step 6: Marketing Messages per Segment & Role (HOW to speak)',
customerJourney: 'Step 7: Customer Journey & Buying Center',
}
}
};

View File

@@ -17,7 +17,7 @@ export const tableToMarkdown = (step: AnalysisStep): string => {
export const generateMarkdown = (data: AnalysisData, titles: Record<keyof AnalysisData, string>, summaryTitle: string): string => {
let markdownContent = '# B2B Marketing Analysis Report\n\n';
const stepOrder: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages'];
const stepOrder: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages', 'customerJourney'];
for (const key of stepOrder) {
const step = data[key];

View File

@@ -20,6 +20,7 @@ export interface AnalysisData {
painPoints?: AnalysisStep;
gains?: AnalysisStep;
messages?: AnalysisStep;
customerJourney?: AnalysisStep;
searchStrategyICP?: AnalysisStep;
digitalSignals?: AnalysisStep;
targetPages?: AnalysisStep;