diff --git a/b2b-marketing-assistant/App.tsx b/b2b-marketing-assistant/App.tsx index 1b2b7551..c37f0e89 100644 --- a/b2b-marketing-assistant/App.tsx +++ b/b2b-marketing-assistant/App.tsx @@ -57,6 +57,8 @@ const App: React.FC = () => { 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 [isEnriching, setIsEnriching] = useState(false); + // Project Persistence const [projectId, setProjectId] = useState(null); @@ -69,6 +71,43 @@ const App: React.FC = () => { const STEP_TITLES = t.stepTitles; const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages', 'customerJourney']; + const handleEnrichRow = async (productName: string, productUrl?: string) => { + setIsEnriching(true); + setError(null); + try { + const response = await fetch(`${API_BASE_URL}/enrich-product`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + productName, + productUrl, + language: inputData.language + }), + }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.details || `HTTP error! status: ${response.status}`); + } + const newRow = await response.json(); + setAnalysisData(prev => { + const currentOffer = prev.offer || { headers: [], rows: [], summary: [] }; + return { + ...prev, + offer: { + ...currentOffer, + rows: [...currentOffer.rows, newRow] + } + }; + }); + } catch (e) { + console.error(e); + setError(e instanceof Error ? `Fehler beim Anreichern: ${e.message}` : 'Unbekannter Fehler beim Anreichern.'); + } finally { + setIsEnriching(false); + } + }; + + // --- AUTO-SAVE EFFECT --- useEffect(() => { if (generationStep === 0 || !inputData.companyUrl) return; @@ -507,9 +546,10 @@ const App: React.FC = () => { const canAdd = ['offer', 'targetGroups'].includes(stepKey); const canDelete = ['offer', 'targetGroups', 'personas'].includes(stepKey); - const handleManualAdd = (newRow: string[]) => { + const handleManualAdd = () => { + const newEmptyRow = Array(step.headers.length).fill(''); const currentRows = step.rows || []; - handleDataChange(stepKey, { ...step, rows: [...currentRows, newRow] }); + handleDataChange(stepKey, { ...step, rows: [...currentRows, newEmptyRow] }); }; return ( @@ -521,8 +561,8 @@ const App: React.FC = () => { rows={step.rows} onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })} canAddRows={canAdd} - onEnrichRow={canAdd ? handleManualAdd : undefined} - isEnriching={false} + onEnrichRow={stepKey === 'offer' ? handleEnrichRow : handleManualAdd} + isEnriching={isEnriching} canDeleteRows={canDelete} onRestart={() => handleStepRestart(stepKey)} t={t} diff --git a/b2b-marketing-assistant/README.md b/b2b-marketing-assistant/README.md index 4cc79dc2..2ba133e2 100644 --- a/b2b-marketing-assistant/README.md +++ b/b2b-marketing-assistant/README.md @@ -15,6 +15,6 @@ View your app in AI Studio: https://ai.studio/apps/drive/1ZPnGbhaEnyhIyqs2rYhcPX 1. Install dependencies: `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +2. Set the `GEMINI_API_KEY` in the central `.env` file in the project's root directory. 3. Run the app: `npm run dev` diff --git a/b2b-marketing-assistant/components/StepDisplay.tsx b/b2b-marketing-assistant/components/StepDisplay.tsx index ce86baa6..df16e006 100644 --- a/b2b-marketing-assistant/components/StepDisplay.tsx +++ b/b2b-marketing-assistant/components/StepDisplay.tsx @@ -12,7 +12,7 @@ interface StepDisplayProps { onDataChange: (newRows: string[][]) => void; canAddRows?: boolean; canDeleteRows?: boolean; - onEnrichRow?: (productName: string, productUrl?: string) => Promise; + onEnrichRow?: (productName: string, productUrl?: string) => void; isEnriching?: boolean; onRestart?: () => void; t: typeof translations.de; @@ -106,12 +106,7 @@ export const StepDisplay: React.FC = ({ title, summary, header }; const handleAddRowClick = () => { - if (onEnrichRow) { - setIsAddingRow(true); - } else { - const newEmptyRow = Array(headers.length).fill(''); - onDataChange([...rows, newEmptyRow]); - } + setIsAddingRow(true); }; const handleConfirmAddRow = () => { diff --git a/b2b-marketing-assistant/server.cjs b/b2b-marketing-assistant/server.cjs index 5e246932..aefdf866 100644 --- a/b2b-marketing-assistant/server.cjs +++ b/b2b-marketing-assistant/server.cjs @@ -89,6 +89,15 @@ router.post('/next-step', (req, res) => { } catch (e) { res.status(500).json({ error: e.message }); } }); +router.post('/enrich-product', (req, res) => { + const { productName, productUrl, language } = req.body; + const args = [SCRIPT_PATH, '--mode', 'enrich_product', '--product_name', productName, '--language', language]; + if (productUrl) { + args.push('--product_url', productUrl); + } + runPythonScript(args, res); +}); + router.get('/projects', (req, res) => runPythonScript([dbScript, 'list'], res)); router.get('/projects/:id', (req, res) => runPythonScript([dbScript, 'load', req.params.id], res)); router.delete('/projects/:id', (req, res) => runPythonScript([dbScript, 'delete', req.params.id], res)); diff --git a/b2b-marketing-assistant/vite.config.ts b/b2b-marketing-assistant/vite.config.ts index 508c3760..1631dc5c 100644 --- a/b2b-marketing-assistant/vite.config.ts +++ b/b2b-marketing-assistant/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig, loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig(({ mode }) => { - const env = loadEnv(mode, '.', ''); + const env = loadEnv(mode, '../', ''); return { base: '/b2b/', server: { diff --git a/b2b_marketing_orchestrator.py b/b2b_marketing_orchestrator.py index 8641574b..450532b1 100644 --- a/b2b_marketing_orchestrator.py +++ b/b2b_marketing_orchestrator.py @@ -622,6 +622,40 @@ def next_step(language, context_file, generation_step, channels, focus_industry= summary = [re.sub(r'^\*\s*|^-\s*|^\d+\.\s*', '', s.strip()) for s in summary_match[1].split('\n') if s.strip()] if summary_match else [] return {step_key: {"summary": summary, "headers": table_data['headers'], "rows": table_data['rows']}} +def enrich_product(product_name, product_url, language): + logging.info(f"Enriching product: {product_name} ({product_url})") + api_key = load_api_key() + if not api_key: raise ValueError("Gemini API key is missing.") + + grounding_text = "" + if product_url: + grounding_text = get_text_from_url(product_url) + + prompt_text = f""" +# ANWEISUNG +Du bist ein B2B-Marketing-Analyst. Deine Aufgabe ist es, die Daten für EIN Produkt zu generieren. +Basierend auf dem Produktnamen und (optional) dem Inhalt der Produkt-URL, fülle die Spalten einer Markdown-Tabelle aus. +Die Ausgabe MUSS eine einzelne, kommaseparierte Zeile sein, die in eine Tabelle passt. KEINE Header, KEIN Markdown, nur die Werte. + +# PRODUKT +- Name: "{product_name}" +- URL-Inhalt: "{grounding_text[:3000]}..." + +# SPALTEN +Produkt/Lösung | Beschreibung (1-2 Sätze) | Kernfunktionen | Differenzierung | Primäre Quelle (URL) + +# BEISPIEL-OUTPUT +Saugroboter NR1500,Ein professioneller Saugroboter für große Büroflächen.,Autonome Navigation;Intelligente Kartierung;Lange Akkulaufzeit,Fokus auf B2B-Markt;Datenschutzkonform,https://nexaro.com/products/nr1500 + +# DEINE AUFGABE +Erstelle jetzt die kommaseparierte Zeile für das Produkt "{product_name}". +""" + + response_text = call_gemini_api(prompt_text, api_key) + + # Return as a simple list of strings + return [cell.strip() for cell in response_text.split(',')] + def main(): parser = argparse.ArgumentParser() parser.add_argument('--mode', required=True) @@ -633,10 +667,13 @@ def main(): parser.add_argument('--channels') parser.add_argument('--language', required=True) parser.add_argument('--focus_industry') # New argument + parser.add_argument('--product_name') + parser.add_argument('--product_url') 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, args.focus_industry) + elif args.mode == 'enrich_product': result = enrich_product(args.product_name, args.product_url, args.language) sys.stdout.write(json.dumps(result, ensure_ascii=False)) except Exception as e: logging.error(f"Error: {e}", exc_info=True)