Dateien nach "b2b-marketing-assistant" hochladen
This commit is contained in:
388
b2b-marketing-assistant/App.tsx
Normal file
388
b2b-marketing-assistant/App.tsx
Normal file
@@ -0,0 +1,388 @@
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import { GoogleGenAI, Chat } from "@google/genai";
|
||||
import { InputForm } from './components/InputForm';
|
||||
import { StepDisplay } from './components/StepDisplay';
|
||||
import { LoadingSpinner, BotIcon, SparklesIcon, MarkdownIcon, PrintIcon } from './components/Icons';
|
||||
import { ExportMenu } from './components/ExportMenu';
|
||||
import { PROMPTS, translations } from './constants';
|
||||
import type { AnalysisStep, AnalysisData, InputData } from './types';
|
||||
import { parseGeminiStepResponse } from './services/parser';
|
||||
import { generateMarkdown, downloadFile, tableToMarkdown } from './services/export';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [inputData, setInputData] = useState<InputData>({
|
||||
companyUrl: '',
|
||||
language: 'de',
|
||||
regions: '',
|
||||
focus: '',
|
||||
channels: ['LinkedIn', 'Kaltmail', 'Landingpage']
|
||||
});
|
||||
const [analysisData, setAnalysisData] = useState<Partial<AnalysisData>>({});
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isEnriching, setIsEnriching] = 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 chatRef = useRef<Chat | null>(null);
|
||||
|
||||
const t = translations[inputData.language];
|
||||
const STEP_TITLES = t.stepTitles;
|
||||
const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages'];
|
||||
|
||||
|
||||
const handleStartGeneration = useCallback(async () => {
|
||||
if (!inputData.companyUrl) {
|
||||
setError('Bitte geben Sie eine Unternehmens-URL ein.');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setAnalysisData({});
|
||||
setGenerationStep(0);
|
||||
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const currentPrompts = PROMPTS[inputData.language];
|
||||
|
||||
const newChat = ai.chats.create({
|
||||
model: 'gemini-2.5-pro',
|
||||
config: {
|
||||
systemInstruction: currentPrompts.SYSTEM_PROMPT.replace('{{language}}', inputData.language)
|
||||
}
|
||||
});
|
||||
chatRef.current = newChat;
|
||||
|
||||
let prompt = currentPrompts.STEP_PROMPTS[0];
|
||||
prompt = prompt.replace('{{company_url}}', inputData.companyUrl);
|
||||
prompt = prompt.replace('{{language}}', inputData.language);
|
||||
prompt = prompt.replace('{{regions}}', inputData.regions);
|
||||
prompt = prompt.replace('{{focus}}', inputData.focus);
|
||||
|
||||
const response = await newChat.sendMessage({ message: prompt });
|
||||
|
||||
const parsedData = parseGeminiStepResponse(response.text);
|
||||
setAnalysisData(parsedData);
|
||||
setGenerationStep(1);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e instanceof Error ? `Ein Fehler ist aufgetreten: ${e.message}` : 'Ein unbekannter Fehler ist aufgetreten.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [inputData]);
|
||||
|
||||
const handleGenerateNextStep = useCallback(async () => {
|
||||
if (!chatRef.current || generationStep >= 6) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const nextStepIndex = generationStep;
|
||||
|
||||
try {
|
||||
let context = '';
|
||||
for (let i = 0; i < generationStep; i++) {
|
||||
const stepKey = STEP_KEYS[i];
|
||||
const stepObject = analysisData[stepKey];
|
||||
if (stepObject) {
|
||||
context += `\n\n## ${STEP_TITLES[stepKey]}\n\n`;
|
||||
const summary = stepObject.summary && stepObject.summary.length > 0 ? `**${t.summaryTitle}**\n${stepObject.summary.map(s => `* ${s}`).join('\n')}\n\n` : '';
|
||||
const table = tableToMarkdown(stepObject);
|
||||
context += `${summary}${table}`;
|
||||
}
|
||||
}
|
||||
|
||||
const currentPrompts = PROMPTS[inputData.language];
|
||||
let prompt = currentPrompts.STEP_PROMPTS[nextStepIndex];
|
||||
prompt = prompt.replace('{{previous_steps_data}}', context);
|
||||
|
||||
if (nextStepIndex === 5) { // Step 6 is index 5
|
||||
prompt = prompt.replace('{{channels}}', inputData.channels.join(', '));
|
||||
}
|
||||
|
||||
const response = await chatRef.current.sendMessage({ message: prompt });
|
||||
const parsedData = parseGeminiStepResponse(response.text);
|
||||
|
||||
setAnalysisData(prev => ({ ...prev, ...parsedData }));
|
||||
setGenerationStep(prev => prev + 1);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(e instanceof Error ? `Ein Fehler ist aufgetreten: ${e.message}` : 'Ein unbekannter Fehler ist aufgetreten.');
|
||||
setGenerationStep(prev => prev); // Stay on the current step if error
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [analysisData, generationStep, inputData.channels, inputData.language, STEP_KEYS, STEP_TITLES, t.summaryTitle]);
|
||||
|
||||
const handleDataChange = <K extends keyof AnalysisData>(step: K, newData: AnalysisData[K]) => {
|
||||
if (analysisData[step]) {
|
||||
setAnalysisData(prev => prev ? { ...prev, [step]: newData } : { [step]: newData });
|
||||
}
|
||||
};
|
||||
|
||||
const getEnrichPrompt = (lang: 'de' | 'en', url: string, productName: string) => {
|
||||
if (lang === 'en') {
|
||||
return `# Task
|
||||
Fill in the information for the following product/solution based on the website ${url}. Respond ONLY with the content for the remaining 4 columns, separated by '|||'.
|
||||
|
||||
# Product/Solution
|
||||
${productName}
|
||||
|
||||
# Column Order
|
||||
1. Description (1–2 sentences)
|
||||
2. Core Features
|
||||
3. Differentiation
|
||||
4. Primary Source (URL)
|
||||
|
||||
# Important
|
||||
Respond *only* with the text for the 4 columns, separated by '|||'. Do not output any headers or explanations.`;
|
||||
}
|
||||
// German (original)
|
||||
return `# Aufgabe
|
||||
Fülle die Informationen für das folgende Produkt/Lösung basierend auf der Webseite ${url} aus. Antworte NUR mit dem Inhalt für die restlichen 4 Spalten, getrennt durch '|||'.
|
||||
|
||||
# Produkt/Lösung
|
||||
${productName}
|
||||
|
||||
# Spalten-Reihenfolge
|
||||
1. Beschreibung (1–2 Sätze)
|
||||
2. Kernfunktionen
|
||||
3. Differenzierung
|
||||
4. Primäre Quelle (URL)
|
||||
|
||||
# Wichtig
|
||||
Antworte *nur* mit dem Text für die 4 Spalten, getrennt durch '|||'. Gib keine Überschriften oder Erklärungen aus.`;
|
||||
};
|
||||
|
||||
|
||||
const handleEnrichOfferRow = useCallback(async (productName: string, productUrl?: string) => {
|
||||
if (!analysisData.offer) return;
|
||||
|
||||
setIsEnriching(true);
|
||||
setError(null);
|
||||
|
||||
const loadingText = t.loadingButton.replace('...', '');
|
||||
const placeholderRow = [productName, loadingText, loadingText, loadingText, loadingText];
|
||||
|
||||
setAnalysisData(prev => {
|
||||
if (!prev.offer) return prev;
|
||||
const currentOffer = prev.offer;
|
||||
const updatedRows = [...currentOffer.rows, placeholderRow];
|
||||
return {
|
||||
...prev,
|
||||
offer: {
|
||||
...currentOffer,
|
||||
rows: updatedRows,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
// Use the specific product URL if provided, otherwise fallback to the main company URL
|
||||
const targetUrl = productUrl && productUrl.trim() !== '' ? productUrl.trim() : inputData.companyUrl;
|
||||
|
||||
const enrichPrompt = getEnrichPrompt(inputData.language, targetUrl, productName);
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: enrichPrompt
|
||||
});
|
||||
const text = response.text || '';
|
||||
const enrichedParts = text.split('|||').map(s => s.trim());
|
||||
|
||||
const finalEnrichedData = Array(4).fill('');
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (enrichedParts[i]) {
|
||||
finalEnrichedData[i] = enrichedParts[i];
|
||||
}
|
||||
}
|
||||
|
||||
const newRow = [productName, ...finalEnrichedData];
|
||||
|
||||
setAnalysisData(prev => {
|
||||
const offerData = prev.offer;
|
||||
if (!offerData) return prev;
|
||||
const finalRows = offerData.rows.map(row =>
|
||||
row[0] === productName && row[1] === loadingText ? newRow : row
|
||||
);
|
||||
return { ...prev, offer: { ...offerData, rows: finalRows } };
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// On error, clear loading text so user can edit manually
|
||||
setAnalysisData(prev => {
|
||||
const offerData = prev.offer;
|
||||
if (!offerData) return prev;
|
||||
const emptyRow = [productName, '', '', '', ''];
|
||||
const finalRows = offerData.rows.map(row =>
|
||||
row[0] === productName && row[1] === loadingText ? emptyRow : row
|
||||
);
|
||||
return { ...prev, offer: { ...offerData, rows: finalRows } };
|
||||
});
|
||||
} finally {
|
||||
setIsEnriching(false);
|
||||
}
|
||||
}, [analysisData.offer, inputData.companyUrl, inputData.language, t.loadingButton]);
|
||||
|
||||
const handleDownloadMarkdown = () => {
|
||||
if (!analysisData) return;
|
||||
const markdownContent = generateMarkdown(analysisData as AnalysisData, STEP_TITLES, t.summaryTitle);
|
||||
downloadFile(markdownContent, 'b2b-marketing-analysis.md', 'text/markdown;charset=utf-8');
|
||||
};
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print();
|
||||
};
|
||||
|
||||
const renderStep = (stepKey: keyof AnalysisData, title: string) => {
|
||||
const step = analysisData[stepKey] as AnalysisStep | undefined;
|
||||
if (!step) return null;
|
||||
|
||||
const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey);
|
||||
|
||||
return (
|
||||
<StepDisplay
|
||||
key={stepKey}
|
||||
title={title}
|
||||
summary={step.summary}
|
||||
headers={step.headers}
|
||||
rows={step.rows}
|
||||
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
|
||||
canAddRows={stepKey === 'offer'}
|
||||
onEnrichRow={stepKey === 'offer' ? handleEnrichOfferRow : undefined}
|
||||
isEnriching={isEnriching}
|
||||
canDeleteRows={canDelete}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContinueButton = (stepNumber: number) => {
|
||||
if (isLoading || generationStep !== stepNumber - 1) return null;
|
||||
return (
|
||||
<div className="my-8 text-center print:hidden">
|
||||
<button
|
||||
onClick={handleGenerateNextStep}
|
||||
className="flex items-center justify-center 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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-800 dark:text-slate-200 font-sans">
|
||||
<main className="container mx-auto px-4 py-8 md:py-12">
|
||||
<header className="text-center mb-10 print:hidden">
|
||||
<h1 className="text-4xl md:text-5xl font-extrabold text-slate-900 dark:text-white">
|
||||
{t.appTitle}
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-slate-600 dark:text-slate-400">
|
||||
{t.appSubtitle}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="max-w-4xl mx-auto 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:hidden">
|
||||
<InputForm
|
||||
inputData={inputData}
|
||||
setInputData={setInputData}
|
||||
onGenerate={handleStartGeneration}
|
||||
isLoading={isLoading && generationStep === 0}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="max-w-4xl mx-auto mt-8 bg-red-100 dark:bg-red-900/50 border border-red-400 dark:border-red-600 text-red-700 dark:text-red-200 px-4 py-3 rounded-lg print:hidden" role="alert">
|
||||
<strong className="font-bold">{t.errorTitle} </strong>
|
||||
<span className="block sm:inline">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-12 space-y-12">
|
||||
{isLoading && (
|
||||
<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]])}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && generationStep === 0 && !error && (
|
||||
<div className="text-center p-8 bg-white dark:bg-slate-800/50 rounded-2xl shadow-md border border-slate-200 dark:border-slate-700 print:hidden">
|
||||
<BotIcon className="mx-auto h-16 w-16 text-sky-500" />
|
||||
<h3 className="mt-4 text-xl font-semibold text-slate-900 dark:text-white">{t.readyTitle}</h3>
|
||||
<p className="mt-2 text-slate-600 dark:text-slate-400">{t.readyText}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{generationStep > 0 && (
|
||||
<>
|
||||
{generationStep >= 6 && (
|
||||
<div className="flex justify-end mb-8 print:hidden">
|
||||
<ExportMenu
|
||||
onDownloadMarkdown={handleDownloadMarkdown}
|
||||
onPrint={handlePrint}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderStep('offer', STEP_TITLES.offer)}
|
||||
{renderContinueButton(2)}
|
||||
{generationStep >= 2 && renderStep('targetGroups', STEP_TITLES.targetGroups)}
|
||||
{renderContinueButton(3)}
|
||||
{generationStep >= 3 && renderStep('personas', STEP_TITLES.personas)}
|
||||
{renderContinueButton(4)}
|
||||
{generationStep >= 4 && renderStep('painPoints', STEP_TITLES.painPoints)}
|
||||
{renderContinueButton(5)}
|
||||
{generationStep >= 5 && renderStep('gains', STEP_TITLES.gains)}
|
||||
{renderContinueButton(6)}
|
||||
{generationStep >= 6 && renderStep('messages', STEP_TITLES.messages)}
|
||||
|
||||
|
||||
{generationStep === 6 && !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" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2">
|
||||
{t.analysisCompleteTitle}
|
||||
</h3>
|
||||
<p className="text-slate-600 dark:text-slate-300 mb-8 max-w-2xl mx-auto">
|
||||
{t.analysisCompleteText.replace('{{otherLanguage}}', t.otherLanguage)}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-center items-center gap-4">
|
||||
<button
|
||||
onClick={handleDownloadMarkdown}
|
||||
className="w-full sm:w-auto flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-white bg-slate-900 dark:bg-sky-600 hover:bg-slate-800 dark:hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 shadow-md transition-all"
|
||||
>
|
||||
<MarkdownIcon className="mr-2 h-5 w-5" />
|
||||
{t.exportAsMarkdown}
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePrint}
|
||||
className="w-full sm:w-auto flex items-center justify-center px-6 py-3 border border-slate-300 dark:border-slate-600 text-base font-medium rounded-lg text-slate-700 dark:text-slate-200 bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 shadow-sm transition-all"
|
||||
>
|
||||
<PrintIcon className="mr-2 h-5 w-5" />
|
||||
{t.exportAsPdf}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user