[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:
@@ -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}
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user