feat(b2b): Add batch processing, industry selection, optimized PDF export, and update docs
This commit is contained in:
@@ -22,6 +22,8 @@ const App: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [generationStep, setGenerationStep] = useState<number>(0); // 0: idle, 1-6: step X is complete
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
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 = <K extends keyof AnalysisData>(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 (
|
||||
<div className="my-8 max-w-2xl mx-auto bg-white dark:bg-slate-800 p-6 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 print:hidden">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-white mb-4 text-center">
|
||||
Wählen Sie eine Fokus-Branche für Schritt 6 (Botschaften):
|
||||
</h3>
|
||||
|
||||
<div className="flex justify-center mb-6">
|
||||
<button
|
||||
onClick={handleBatchGenerate}
|
||||
className="flex items-center px-4 py-2 bg-slate-800 hover:bg-slate-700 text-white rounded-md text-sm font-medium transition-colors shadow-sm"
|
||||
>
|
||||
<SparklesIcon className="mr-2 h-4 w-4 text-yellow-400" />
|
||||
Alle {industries.length} Branchen analysieren (Batch)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-xs text-slate-500 mb-2 uppercase font-semibold tracking-wider">- ODER EINZELN -</div>
|
||||
|
||||
<div className="space-y-2 mb-6 max-h-60 overflow-y-auto pr-2">
|
||||
{industries.map((ind, idx) => (
|
||||
<label key={idx} className={`flex items-center p-3 rounded-lg border cursor-pointer transition-all ${selectedIndustry === ind ? 'bg-sky-50 border-sky-500 ring-1 ring-sky-500 dark:bg-sky-900/20 dark:border-sky-400' : 'border-slate-200 hover:border-slate-300 dark:border-slate-700 dark:hover:border-slate-600'}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="industry"
|
||||
value={ind}
|
||||
checked={selectedIndustry === ind}
|
||||
onChange={(e) => { setSelectedIndustry(e.target.value); setError(null); }}
|
||||
className="h-4 w-4 text-sky-600 focus:ring-sky-500 border-gray-300 dark:bg-slate-700 dark:border-slate-600"
|
||||
/>
|
||||
<span className="ml-3 block text-sm font-medium text-slate-700 dark:text-slate-200 break-words">
|
||||
{ind}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={handleGenerateNextStep}
|
||||
disabled={!selectedIndustry}
|
||||
className="flex items-center justify-center w-full sm:w-auto mx-auto px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:bg-slate-400 dark:disabled:bg-slate-600 disabled:cursor-not-allowed transition-colors duration-200"
|
||||
>
|
||||
<SparklesIcon className="mr-2 h-5 w-5" />
|
||||
{t.continueButton.replace('{{step}}', (stepNumber - 1).toString()).replace('{{nextStep}}', stepNumber.toString())}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-8 text-center print:hidden">
|
||||
<button
|
||||
@@ -201,8 +334,18 @@ const App: React.FC = () => {
|
||||
<div className="flex flex-col items-center justify-center text-center p-8">
|
||||
<LoadingSpinner />
|
||||
<p className="mt-4 text-lg text-slate-600 dark:text-slate-400 animate-pulse">
|
||||
{t.generatingStep.replace('{{stepTitle}}', STEP_TITLES[STEP_KEYS[generationStep]])}
|
||||
{batchStatus
|
||||
? `Analysiere Branche ${batchStatus.current} von ${batchStatus.total}: ${batchStatus.industry}...`
|
||||
: t.generatingStep.replace('{{stepTitle}}', STEP_TITLES[STEP_KEYS[generationStep]])}
|
||||
</p>
|
||||
{batchStatus && (
|
||||
<div className="w-64 h-2 bg-slate-200 rounded-full mt-4 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-sky-500 transition-all duration-500 ease-out"
|
||||
style={{ width: `${(batchStatus.current / batchStatus.total) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user