From 1220a858f2f7a35908a40145345b5f4bffe2a43d Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 29 Dec 2025 13:59:20 +0000 Subject: [PATCH] feat: Enhanced Outreach UI - Top 5 default + specific role generation - market_intel_orchestrator.py: Updated generate_outreach_campaign to identify all relevant roles, generate top 5, and return remaining as suggestions. Added specific_role mode. - types.ts: Added OutreachResponse interface. - geminiService.ts: Updated to handle new response structure and specificRole parameter. - StepOutreach.tsx: Added sidebar section for Suggested Roles with on-demand generation buttons. --- .../components/StepOutreach.tsx | 127 +++++++++++++----- .../services/geminiService.ts | 93 +++++++------ general-market-intelligence/types.ts | 5 + market_intel_orchestrator.py | 111 ++++++++------- 4 files changed, 211 insertions(+), 125 deletions(-) diff --git a/general-market-intelligence/components/StepOutreach.tsx b/general-market-intelligence/components/StepOutreach.tsx index 7a8767d2..6a36ab3d 100644 --- a/general-market-intelligence/components/StepOutreach.tsx +++ b/general-market-intelligence/components/StepOutreach.tsx @@ -1,8 +1,7 @@ - import React, { useState } from 'react'; -import { AnalysisResult, EmailDraft, Language } from '../types'; +import { AnalysisResult, EmailDraft, Language, OutreachResponse } from '../types'; import { generateOutreachCampaign, translateEmailDrafts } from '../services/geminiService'; -import { Upload, FileText, Sparkles, Copy, Check, Loader2, ArrowRight, CheckCircle2, Download, Languages } from 'lucide-react'; +import { Upload, FileText, Sparkles, Copy, Check, Loader2, ArrowRight, CheckCircle2, Download, Languages, Plus, UserPlus } from 'lucide-react'; interface StepOutreachProps { company: AnalysisResult; @@ -15,9 +14,14 @@ interface StepOutreachProps { export const StepOutreach: React.FC = ({ company, language, referenceUrl, onBack, knowledgeBase }) => { const [fileContent, setFileContent] = useState(knowledgeBase || ''); const [fileName, setFileName] = useState(knowledgeBase ? 'Knowledge Base from Strategy Step' : ''); + const [isProcessing, setIsProcessing] = useState(false); const [isTranslating, setIsTranslating] = useState(false); + const [isGeneratingSpecific, setIsGeneratingSpecific] = useState(null); // Track which specific role is loading + const [emails, setEmails] = useState([]); + const [availableRoles, setAvailableRoles] = useState([]); // Suggested roles not yet generated + const [activeTab, setActiveTab] = useState(0); const [copied, setCopied] = useState(false); @@ -45,8 +49,10 @@ export const StepOutreach: React.FC = ({ company, language, r if (!fileContent) return; setIsProcessing(true); try { - const drafts = await generateOutreachCampaign(company, fileContent, language, referenceUrl); - setEmails(drafts); + // Initial generation (Top 5 + Suggestions) + const response: OutreachResponse = await generateOutreachCampaign(company, fileContent, language, referenceUrl); + setEmails(response.campaigns); + setAvailableRoles(response.available_roles || []); } catch (e) { console.error(e); alert('Failed to generate campaign. Please try again.'); @@ -55,6 +61,28 @@ export const StepOutreach: React.FC = ({ company, language, r } }; + const handleGenerateSpecific = async (role: string) => { + if (!fileContent) return; + setIsGeneratingSpecific(role); + try { + // Generate single specific campaign + const response: OutreachResponse = await generateOutreachCampaign(company, fileContent, language, referenceUrl, role); + if (response.campaigns && response.campaigns.length > 0) { + // Add new campaign to list + setEmails(prev => [...prev, response.campaigns[0]]); + // Remove from available roles + setAvailableRoles(prev => prev.filter(r => r !== role)); + // Switch view to new campaign + setActiveTab(emails.length); + } + } catch (e) { + console.error("Failed to generate specific role", e); + alert(`Failed to generate campaign for ${role}`); + } finally { + setIsGeneratingSpecific(null); + } + }; + const handleTranslate = async (targetLang: Language) => { if (emails.length === 0) return; setIsTranslating(true); @@ -103,7 +131,7 @@ export const StepOutreach: React.FC = ({ company, language, r // Helper to render text with bold syntax **text** const renderBoldText = (text: string) => { - const parts = text.split(/(\*\*.*?\*\*)/g); + const parts = text.split(/(\**.*?\**)/g); return parts.map((part, index) => { if (part.startsWith('**') && part.endsWith('**')) { return {part.slice(2, -2)}; @@ -167,33 +195,68 @@ export const StepOutreach: React.FC = ({ company, language, r
{/* Sidebar Tabs */} -
- {emails.map((email, idx) => ( - + ))} +
+ + {/* Suggestions List */} + {availableRoles.length > 0 && ( +
+
+ +

Other Relevant Roles

+
+
+ {availableRoles.map((role, i) => ( +
+ {role} + +
+ ))} +
-
{email.subject}
- - ))} + )}
{/* Content Area */} -
+
{activeEmail ? ( <>
-
+
{activeEmail.subject}
@@ -209,14 +272,14 @@ export const StepOutreach: React.FC = ({ company, language, r {copied ? "Copied" : "Copy Content"}
-
+
{renderBoldText(activeEmail.body)}
{/* Checklist Section */} {activeEmail.keyPoints && activeEmail.keyPoints.length > 0 && ( -
+

Persona & KPI Analysis @@ -247,7 +310,7 @@ export const StepOutreach: React.FC = ({ company, language, r
- +

Create Outreach Campaign

@@ -261,7 +324,7 @@ export const StepOutreach: React.FC = ({ company, language, r

= ({ company, language, r

{fileName}

); -}; +}; \ No newline at end of file diff --git a/general-market-intelligence/services/geminiService.ts b/general-market-intelligence/services/geminiService.ts index 34cae3c1..06bd8c74 100644 --- a/general-market-intelligence/services/geminiService.ts +++ b/general-market-intelligence/services/geminiService.ts @@ -1,7 +1,4 @@ -import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal } from "../types"; - -// const apiKey = process.env.API_KEY; // Nicht mehr direkt im Frontend verwendet -// const ai = new GoogleGenAI({ apiKey: apiKey || '' }); // Nicht mehr direkt im Frontend verwendet +import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } from "../types"; // URL Konfiguration: // Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert. @@ -19,7 +16,7 @@ const extractJson = (text: string): any => { if (jsonMatch && jsonMatch[1]) { try { return JSON.parse(jsonMatch[1]); } catch (e2) {} } - const arrayMatch = text.match(/\[\s*[\s\S]*\s*\]/); + const arrayMatch = text.match(/\\[\s\S]*\\s*\]/); if (arrayMatch) { try { return JSON.parse(arrayMatch[0]); } catch (e3) {} } @@ -129,7 +126,7 @@ export const analyzeCompanyWithStrategy = async ( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ + body: JSON.stringify({ companyName, strategy, targetMarket: language === 'de' ? 'Germany' : 'USA' // Einfache Ableitung, kann verfeinert werden @@ -175,9 +172,10 @@ export const generateOutreachCampaign = async ( companyData: AnalysisResult, knowledgeBase: string, language: Language, - referenceUrl: string -): Promise => { - console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName}...`); + referenceUrl: string, + specificRole?: string +): Promise => { + console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName} (Role: ${specificRole || "All"})...`); try { const response = await fetch(`${API_BASE_URL}/generate-outreach`, { @@ -185,10 +183,11 @@ export const generateOutreachCampaign = async ( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ + body: JSON.stringify({ companyData, knowledgeBase, - referenceUrl + referenceUrl, + specific_role: specificRole }), }); @@ -197,45 +196,57 @@ export const generateOutreachCampaign = async ( throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`); } - const result = await response.json(); - console.log(`Frontend: Outreach-Generierung für ${companyData.companyName} erfolgreich.`); + const result = await response.json(); // Expected: { campaigns: [...], available_roles: [...] } or single object + console.log(`Frontend: Outreach-Generierung erfolgreich.`, result); - // Transform new backend structure to match frontend EmailDraft interface - if (Array.isArray(result)) { - return result.map((item: any) => { - // Construct a body that shows the sequence - let fullBody = ""; - const firstSubject = item.emails?.[0]?.subject || "No Subject"; - - if (item.emails && Array.isArray(item.emails)) { - item.emails.forEach((mail: any, idx: number) => { - fullBody += `### Email ${idx + 1}: ${mail.subject}\n\n`; - fullBody += `${mail.body}\n\n`; - if (idx < item.emails.length - 1) fullBody += `\n---\n\n`; - }); - } else { - // Fallback for flat structure or error - fullBody = item.body || "No content generated."; - } + let rawCampaigns: any[] = []; + let availableRoles: string[] = []; - return { - persona: item.target_role || "Unknown Role", - subject: firstSubject, - body: fullBody, - keyPoints: item.rationale ? [item.rationale] : [] - }; - }); - } else if (result.campaign && Array.isArray(result.campaign)) { - return result.campaign as EmailDraft[]; + // Handle Mode A (Batch) vs Mode B (Single) response structures + if (result.campaigns && Array.isArray(result.campaigns)) { + rawCampaigns = result.campaigns; + availableRoles = result.available_roles || []; + } else if (result.target_role && result.emails) { + // Single campaign response (Mode B) + rawCampaigns = [result]; + } else if (Array.isArray(result)) { + // Legacy fallback + rawCampaigns = result; } - - return []; + + const processedCampaigns: EmailDraft[] = rawCampaigns.map((item: any) => { + let fullBody = ""; + const firstSubject = item.emails?.[0]?.subject || "No Subject"; + + if (item.emails && Array.isArray(item.emails)) { + item.emails.forEach((mail: any, idx: number) => { + fullBody += `### Email ${idx + 1}: ${mail.subject}\n\n`; + fullBody += `${mail.body}\n\n`; + if (idx < item.emails.length - 1) fullBody += `\n---\n\n`; + }); + } else { + fullBody = item.body || "No content generated."; + } + + return { + persona: item.target_role || "Unknown Role", + subject: firstSubject, + body: fullBody, + keyPoints: item.rationale ? [item.rationale] : [] + }; + }); + + return { + campaigns: processedCampaigns, + available_roles: availableRoles + }; } catch (error) { console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error); throw error; } }; + export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise => { // Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert."); diff --git a/general-market-intelligence/types.ts b/general-market-intelligence/types.ts index fba0a005..78f10c08 100644 --- a/general-market-intelligence/types.ts +++ b/general-market-intelligence/types.ts @@ -89,3 +89,8 @@ export interface EmailDraft { body: string; keyPoints: string[]; } + +export interface OutreachResponse { + campaigns: EmailDraft[]; + available_roles: string[]; +} diff --git a/market_intel_orchestrator.py b/market_intel_orchestrator.py index 015f4944..52c7a3dd 100644 --- a/market_intel_orchestrator.py +++ b/market_intel_orchestrator.py @@ -544,25 +544,64 @@ def analyze_company(company_name, strategy, target_market): "dataSource": "Error" } -def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url): +def generate_outreach_campaign(company_data_json, knowledge_base_content, reference_url, specific_role=None): """ - Erstellt personalisierte E-Mail-Kampagnen basierend auf Audit-Daten und einer strukturierten Wissensdatenbank. - Generiert spezifische Ansprachen für verschiedene Rollen (Personas). + Erstellt personalisierte E-Mail-Kampagnen. + Modus A (Default): Generiert Top 5 Kampagnen + Liste weiterer relevanter Rollen. + Modus B (specific_role): Generiert nur Kampagne für diese eine Rolle. """ company_name = company_data_json.get('companyName', 'Unknown') - logger.info(f"--- STARTING ROLE-BASED OUTREACH GENERATION FOR: {company_name} ---") + logger.info(f"--- STARTING OUTREACH GENERATION FOR: {company_name} (Role: {specific_role if specific_role else 'Top 5'}) ---") api_key = load_gemini_api_key() - # Switch to stable 2.5-pro model GEMINI_API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key={api_key}" + if specific_role: + # --- MODE B: SINGLE ROLE GENERATION --- + task_description = f""" + --- TASK --- + 1. **Focus**: Create a highly specific 3-step email campaign ONLY for the role: '{specific_role}'. + 2. **Analyze**: Use the Audit Facts to find specific hooks for this role. + 3. **Draft**: Write the sequence (Opening, Follow-up, Break-up). + """ + output_format = """ + --- OUTPUT FORMAT (Strictly JSON) --- + { + "target_role": "The requested role", + "rationale": "Why this fits...", + "emails": [ ... ] + } + """ + else: + # --- MODE A: INITIAL BATCH (TOP 5 + SUGGESTIONS) --- + task_description = f""" + --- TASK --- + 1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1). + 2. **Identify Roles**: Identify ALL relevant 'Rollen' (Personas) from the Knowledge Base that fit this company. + 3. **Select Top 5**: Choose the 5 most promising roles for immediate outreach based on the Audit findings. + 4. **Draft Campaigns**: For EACH of the Top 5 roles, write a 3-step email sequence. + 5. **List Others**: List the names of the other relevant roles that you identified but did NOT generate campaigns for yet. + """ + output_format = """ + --- OUTPUT FORMAT (Strictly JSON) --- + { + "campaigns": [ + { + "target_role": "Role Name", + "rationale": "Why selected...", + "emails": [ ... ] + }, + ... (Top 5) + ], + "available_roles": [ "Role 6", "Role 7", ... ] + } + """ + prompt = f""" You are a Strategic Key Account Manager and deeply technical Industry Insider. Your goal is to write highly personalized, **operationally specific** outreach emails to the company '{company_name}'. --- INPUT 1: YOUR IDENTITY & STRATEGY (The Sender) --- - The following Markdown contains your company's identity, products, and strategy. - You act as the sales representative for the company described here: {knowledge_base_content} --- INPUT 2: THE TARGET COMPANY (Audit Facts) --- @@ -571,52 +610,21 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere --- INPUT 3: THE REFERENCE CLIENT (Social Proof) --- Reference Client URL: {reference_url} - CRITICAL: This 'Reference Client' is an existing happy customer of ours. They are the "Seed Company" used to find the Target Company (Lookalike). - You MUST mention this Reference Client by name (derive it from the URL, e.g., 'schindler.com' -> 'Schindler') to establish trust. + CRITICAL: This 'Reference Client' is an existing happy customer of ours. You MUST mention them by name to establish trust. - --- TASK --- - 1. **Analyze**: Match the Target Company (Input 2) to the most relevant 'Zielbranche/Segment' from the Knowledge Base (Input 1). - 2. **Select Roles**: Identify **up to 5** of the most distinct and relevant 'Rollen' (Personas) from the Knowledge Base for this specific company situation. - - Prioritize roles where the audit findings (e.g., specific competitor tech, growth pains) offer the strongest hook. - - *Example:* If the audit says they use a competitor (risk of lock-in), select a role like "Strategic Purchaser" or "Head of R&D". - 3. **Draft Campaigns**: For **EACH** of the selected roles, write a 3-step email sequence. + {task_description} --- TONE & STYLE GUIDELINES (CRITICAL) --- - **Perspective:** Operational Expert & Insider. NOT generic marketing. - - **Be Gritty & Specific:** Do NOT use fluff like "optimize efficiency" or "streamline processes" without context. - - Use **hard, operational keywords** from the Knowledge Base (e.g., "ASNs", "VMI", "8D-Reports", "Maverick Buying", "Bandstillstand", "Sonderfahrten", "PPAP"). - - Show you understand their daily pain. + - **Be Gritty & Specific:** Use hard, operational keywords from the Knowledge Base (e.g., "ASNs", "8D-Reports"). - **Narrative Arc:** - 1. "I noticed [Fact from Audit/Tech Stack]..." (e.g., "You rely on PDF orders via Jaggaer...") - 2. "In [Industry], this often leads to [Operational Pain]..." (e.g., "missing ASNs causing delays at the hub.") - 3. "We helped [Reference Client Name] solve exactly this by [Specific Solution]..." - 4. "Let's discuss how to get [Operational Gain] without replacing your ERP." - - **Mandatory Social Proof:** You MUST mention the Reference Client Name (from Input 3) in the email body or footer. - - **Language:** German (as the inputs are German). + 1. "I noticed [Fact from Audit]..." + 2. "In [Industry], this often leads to [Pain]..." + 3. "We helped [Reference Client] solve this..." + 4. "Let's discuss [Gain]." + - **Language:** German. - --- OUTPUT FORMAT (Strictly JSON) --- - Returns a list of campaigns. - [ - {{ - "target_role": "Name of the Role (e.g. Leiter F&E)", - "rationale": "Why this role? (e.g. Because the audit found dependency on Competitor X...)", - "emails": [ - {{ - "subject": "Specific Subject Line", - "body": "Email Body..." - }}, - {{ - "subject": "Re: Subject", - "body": "Follow-up Body..." - }}, - {{ - "subject": "Final Check", - "body": "Final Body..." - }} - ] - }}, - ... (Second Role) - ] + {output_format} """ payload = { @@ -626,12 +634,10 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere try: logger.info("Sende Campaign-Anfrage an Gemini API...") - # logger.debug(f"Rohe Gemini API-Anfrage (JSON): {json.dumps(payload, indent=2)}") response = requests.post(GEMINI_API_URL, json=payload, headers={'Content-Type': 'application/json'}) response.raise_for_status() response_data = response.json() logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).") - # logger.debug(f"Rohe API-Antwort (JSON): {json.dumps(response_data, indent=2)}") text = response_data['candidates'][0]['content']['parts'][0]['text'] result = _extract_json_from_text(text) @@ -642,7 +648,7 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere return result except Exception as e: logger.error(f"Campaign generation failed for {company_name}: {e}") - return [{"error": str(e)}] + return {"error": str(e)} def main(): parser = argparse.ArgumentParser() @@ -653,7 +659,8 @@ def main(): parser.add_argument("--company_name") parser.add_argument("--strategy_json") parser.add_argument("--summary_of_offer") - parser.add_argument("--company_data_file") # For generate_outreach + parser.add_argument("--company_data_file") + parser.add_argument("--specific_role") # New argument args = parser.parse_args() if args.mode == "generate_strategy": @@ -671,7 +678,7 @@ def main(): elif args.mode == "generate_outreach": with open(args.company_data_file, "r") as f: company_data = json.load(f) with open(args.context_file, "r") as f: knowledge_base = f.read() - print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url))) + print(json.dumps(generate_outreach_campaign(company_data, knowledge_base, args.reference_url, args.specific_role))) if __name__ == "__main__":