[30a88f42] feat: Centralize API key handling and fix product enrichment\n\n- Centralized GEMINI_API_KEY loading from project root .env file.\n- Corrected product enrichment: added /enrich-product endpoint in Node.js, implemented mode in Python backend, and updated frontend to use new API for product analysis.\n- Fixed to pass product data correctly for enrichment.\n- Updated documentation ().

This commit is contained in:
2026-02-17 07:14:51 +00:00
parent 7be5d47604
commit d13d3a2f36
6 changed files with 94 additions and 13 deletions

View File

@@ -57,6 +57,8 @@ const App: React.FC = () => {
const [generationStep, setGenerationStep] = useState<number>(0); // 0: idle, 1-6: step X is complete const [generationStep, setGenerationStep] = useState<number>(0); // 0: idle, 1-6: step X is complete
const [selectedIndustry, setSelectedIndustry] = useState<string>(''); const [selectedIndustry, setSelectedIndustry] = useState<string>('');
const [batchStatus, setBatchStatus] = useState<{ current: number; total: number; industry: string } | null>(null); const [batchStatus, setBatchStatus] = useState<{ current: number; total: number; industry: string } | null>(null);
const [isEnriching, setIsEnriching] = useState<boolean>(false);
// Project Persistence // Project Persistence
const [projectId, setProjectId] = useState<string | null>(null); const [projectId, setProjectId] = useState<string | null>(null);
@@ -69,6 +71,43 @@ const App: React.FC = () => {
const STEP_TITLES = t.stepTitles; const STEP_TITLES = t.stepTitles;
const STEP_KEYS: (keyof AnalysisData)[] = ['offer', 'targetGroups', 'personas', 'painPoints', 'gains', 'messages', 'customerJourney']; 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 --- // --- AUTO-SAVE EFFECT ---
useEffect(() => { useEffect(() => {
if (generationStep === 0 || !inputData.companyUrl) return; if (generationStep === 0 || !inputData.companyUrl) return;
@@ -507,9 +546,10 @@ const App: React.FC = () => {
const canAdd = ['offer', 'targetGroups'].includes(stepKey); const canAdd = ['offer', 'targetGroups'].includes(stepKey);
const canDelete = ['offer', 'targetGroups', 'personas'].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 || []; const currentRows = step.rows || [];
handleDataChange(stepKey, { ...step, rows: [...currentRows, newRow] }); handleDataChange(stepKey, { ...step, rows: [...currentRows, newEmptyRow] });
}; };
return ( return (
@@ -521,8 +561,8 @@ const App: React.FC = () => {
rows={step.rows} rows={step.rows}
onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })} onDataChange={(newRows) => handleDataChange(stepKey, { ...step, rows: newRows })}
canAddRows={canAdd} canAddRows={canAdd}
onEnrichRow={canAdd ? handleManualAdd : undefined} onEnrichRow={stepKey === 'offer' ? handleEnrichRow : handleManualAdd}
isEnriching={false} isEnriching={isEnriching}
canDeleteRows={canDelete} canDeleteRows={canDelete}
onRestart={() => handleStepRestart(stepKey)} onRestart={() => handleStepRestart(stepKey)}
t={t} t={t}

View File

@@ -15,6 +15,6 @@ View your app in AI Studio: https://ai.studio/apps/drive/1ZPnGbhaEnyhIyqs2rYhcPX
1. Install dependencies: 1. Install dependencies:
`npm install` `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: 3. Run the app:
`npm run dev` `npm run dev`

View File

@@ -12,7 +12,7 @@ interface StepDisplayProps {
onDataChange: (newRows: string[][]) => void; onDataChange: (newRows: string[][]) => void;
canAddRows?: boolean; canAddRows?: boolean;
canDeleteRows?: boolean; canDeleteRows?: boolean;
onEnrichRow?: (productName: string, productUrl?: string) => Promise<void>; onEnrichRow?: (productName: string, productUrl?: string) => void;
isEnriching?: boolean; isEnriching?: boolean;
onRestart?: () => void; onRestart?: () => void;
t: typeof translations.de; t: typeof translations.de;
@@ -106,12 +106,7 @@ export const StepDisplay: React.FC<StepDisplayProps> = ({ title, summary, header
}; };
const handleAddRowClick = () => { const handleAddRowClick = () => {
if (onEnrichRow) { setIsAddingRow(true);
setIsAddingRow(true);
} else {
const newEmptyRow = Array(headers.length).fill('');
onDataChange([...rows, newEmptyRow]);
}
}; };
const handleConfirmAddRow = () => { const handleConfirmAddRow = () => {

View File

@@ -89,6 +89,15 @@ router.post('/next-step', (req, res) => {
} catch (e) { res.status(500).json({ error: e.message }); } } 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', (req, res) => runPythonScript([dbScript, 'list'], res));
router.get('/projects/:id', (req, res) => runPythonScript([dbScript, 'load', req.params.id], 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)); router.delete('/projects/:id', (req, res) => runPythonScript([dbScript, 'delete', req.params.id], res));

View File

@@ -3,7 +3,7 @@ import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', ''); const env = loadEnv(mode, '../', '');
return { return {
base: '/b2b/', base: '/b2b/',
server: { server: {

View File

@@ -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 [] 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']}} 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(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--mode', required=True) parser.add_argument('--mode', required=True)
@@ -633,10 +667,13 @@ def main():
parser.add_argument('--channels') parser.add_argument('--channels')
parser.add_argument('--language', required=True) parser.add_argument('--language', required=True)
parser.add_argument('--focus_industry') # New argument parser.add_argument('--focus_industry') # New argument
parser.add_argument('--product_name')
parser.add_argument('--product_url')
args = parser.parse_args() args = parser.parse_args()
try: try:
if args.mode == 'start_generation': result = start_generation(args.url, args.language, args.regions, args.focus) 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 == '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)) sys.stdout.write(json.dumps(result, ensure_ascii=False))
except Exception as e: except Exception as e:
logging.error(f"Error: {e}", exc_info=True) logging.error(f"Error: {e}", exc_info=True)