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.
This commit is contained in:
@@ -1,8 +1,7 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
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 { 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 {
|
interface StepOutreachProps {
|
||||||
company: AnalysisResult;
|
company: AnalysisResult;
|
||||||
@@ -15,9 +14,14 @@ interface StepOutreachProps {
|
|||||||
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack, knowledgeBase }) => {
|
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack, knowledgeBase }) => {
|
||||||
const [fileContent, setFileContent] = useState<string>(knowledgeBase || '');
|
const [fileContent, setFileContent] = useState<string>(knowledgeBase || '');
|
||||||
const [fileName, setFileName] = useState<string>(knowledgeBase ? 'Knowledge Base from Strategy Step' : '');
|
const [fileName, setFileName] = useState<string>(knowledgeBase ? 'Knowledge Base from Strategy Step' : '');
|
||||||
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [isTranslating, setIsTranslating] = useState(false);
|
const [isTranslating, setIsTranslating] = useState(false);
|
||||||
|
const [isGeneratingSpecific, setIsGeneratingSpecific] = useState<string | null>(null); // Track which specific role is loading
|
||||||
|
|
||||||
const [emails, setEmails] = useState<EmailDraft[]>([]);
|
const [emails, setEmails] = useState<EmailDraft[]>([]);
|
||||||
|
const [availableRoles, setAvailableRoles] = useState<string[]>([]); // Suggested roles not yet generated
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@@ -45,8 +49,10 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
|||||||
if (!fileContent) return;
|
if (!fileContent) return;
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
try {
|
try {
|
||||||
const drafts = await generateOutreachCampaign(company, fileContent, language, referenceUrl);
|
// Initial generation (Top 5 + Suggestions)
|
||||||
setEmails(drafts);
|
const response: OutreachResponse = await generateOutreachCampaign(company, fileContent, language, referenceUrl);
|
||||||
|
setEmails(response.campaigns);
|
||||||
|
setAvailableRoles(response.available_roles || []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert('Failed to generate campaign. Please try again.');
|
alert('Failed to generate campaign. Please try again.');
|
||||||
@@ -55,6 +61,28 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ 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) => {
|
const handleTranslate = async (targetLang: Language) => {
|
||||||
if (emails.length === 0) return;
|
if (emails.length === 0) return;
|
||||||
setIsTranslating(true);
|
setIsTranslating(true);
|
||||||
@@ -103,7 +131,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
|||||||
|
|
||||||
// Helper to render text with bold syntax **text**
|
// Helper to render text with bold syntax **text**
|
||||||
const renderBoldText = (text: string) => {
|
const renderBoldText = (text: string) => {
|
||||||
const parts = text.split(/(\*\*.*?\*\*)/g);
|
const parts = text.split(/(\**.*?\**)/g);
|
||||||
return parts.map((part, index) => {
|
return parts.map((part, index) => {
|
||||||
if (part.startsWith('**') && part.endsWith('**')) {
|
if (part.startsWith('**') && part.endsWith('**')) {
|
||||||
return <strong key={index} className="font-bold text-slate-900">{part.slice(2, -2)}</strong>;
|
return <strong key={index} className="font-bold text-slate-900">{part.slice(2, -2)}</strong>;
|
||||||
@@ -167,33 +195,68 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
|||||||
|
|
||||||
<div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col md:flex-row min-h-[600px]">
|
<div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden flex flex-col md:flex-row min-h-[600px]">
|
||||||
{/* Sidebar Tabs */}
|
{/* Sidebar Tabs */}
|
||||||
<div className="w-full md:w-72 bg-slate-50 border-r border-slate-200 p-2 flex flex-col gap-2 overflow-y-auto max-h-[600px]">
|
<div className="w-full md:w-80 bg-slate-50 border-r border-slate-200 flex flex-col">
|
||||||
{emails.map((email, idx) => (
|
<div className="p-4 border-b border-slate-200">
|
||||||
<button
|
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider">Generated Campaigns</h3>
|
||||||
key={idx}
|
</div>
|
||||||
onClick={() => setActiveTab(idx)}
|
|
||||||
className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${
|
{/* Generated List */}
|
||||||
activeTab === idx
|
<div className="flex-1 overflow-y-auto p-2 flex flex-col gap-2">
|
||||||
? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20'
|
{emails.map((email, idx) => (
|
||||||
: 'text-slate-600 hover:bg-slate-200/50'
|
<button
|
||||||
}`}
|
key={idx}
|
||||||
>
|
onClick={() => setActiveTab(idx)}
|
||||||
<div className="flex items-center justify-between">
|
className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${activeTab === idx
|
||||||
<span className="truncate">{email.persona}</span>
|
? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20'
|
||||||
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">#{idx+1}</span>
|
: 'text-slate-600 hover:bg-slate-200/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="truncate font-semibold">{email.persona}</span>
|
||||||
|
<span className="text-[10px] bg-slate-200 text-slate-500 px-1.5 py-0.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity">#{idx+1}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-400 mt-1 truncate font-normal opacity-80">{email.subject}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Suggestions List */}
|
||||||
|
{availableRoles.length > 0 && (
|
||||||
|
<div className="border-t border-slate-200 bg-slate-100/50 flex flex-col max-h-[40%]">
|
||||||
|
<div className="p-3 border-b border-slate-200 flex items-center gap-2">
|
||||||
|
<Sparkles size={14} className="text-indigo-500" />
|
||||||
|
<h3 className="text-xs font-bold text-slate-500 uppercase tracking-wider">Other Relevant Roles</h3>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto p-2 gap-2 flex flex-col">
|
||||||
|
{availableRoles.map((role, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between p-2 bg-white border border-slate-200 rounded-lg shadow-sm">
|
||||||
|
<span className="text-xs font-medium text-slate-700 truncate max-w-[140px]" title={role}>{role}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleGenerateSpecific(role)}
|
||||||
|
disabled={!!isGeneratingSpecific}
|
||||||
|
className="bg-indigo-50 hover:bg-indigo-100 text-indigo-700 p-1.5 rounded-md transition-colors disabled:opacity-50"
|
||||||
|
title="Generate Campaign"
|
||||||
|
>
|
||||||
|
{isGeneratingSpecific === role ? (
|
||||||
|
<Loader2 size={14} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Plus size={14} strokeWidth={3} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-400 mt-1 truncate font-normal opacity-80">{email.subject}</div>
|
)}
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Area */}
|
{/* Content Area */}
|
||||||
<div className="flex-1 p-8 flex flex-col overflow-y-auto">
|
<div className="flex-1 p-8 flex flex-col overflow-y-auto bg-slate-50/30">
|
||||||
{activeEmail ? (
|
{activeEmail ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-1">Subject Line</label>
|
<label className="block text-xs font-bold text-slate-400 uppercase tracking-wider mb-1">Subject Line</label>
|
||||||
<div className="text-lg font-semibold text-slate-800 bg-slate-50 p-4 rounded-lg border border-slate-200 select-text">
|
<div className="text-lg font-semibold text-slate-800 bg-white p-4 rounded-lg border border-slate-200 select-text shadow-sm">
|
||||||
{activeEmail.subject}
|
{activeEmail.subject}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,14 +272,14 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
|||||||
{copied ? "Copied" : "Copy Content"}
|
{copied ? "Copied" : "Copy Content"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white border border-slate-200 rounded-xl p-6 text-slate-700 leading-relaxed whitespace-pre-wrap font-serif text-base select-text">
|
<div className="bg-white border border-slate-200 rounded-xl p-8 text-slate-700 leading-relaxed whitespace-pre-wrap font-serif text-base select-text shadow-sm">
|
||||||
{renderBoldText(activeEmail.body)}
|
{renderBoldText(activeEmail.body)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Checklist Section */}
|
{/* Checklist Section */}
|
||||||
{activeEmail.keyPoints && activeEmail.keyPoints.length > 0 && (
|
{activeEmail.keyPoints && activeEmail.keyPoints.length > 0 && (
|
||||||
<div className="border-t border-slate-100 pt-6">
|
<div className="border-t border-slate-200 pt-6">
|
||||||
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide mb-4 flex items-center gap-2">
|
<h3 className="text-sm font-bold text-slate-900 uppercase tracking-wide mb-4 flex items-center gap-2">
|
||||||
<CheckCircle2 size={16} className="text-emerald-500" />
|
<CheckCircle2 size={16} className="text-emerald-500" />
|
||||||
Persona & KPI Analysis
|
Persona & KPI Analysis
|
||||||
@@ -247,7 +310,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
|||||||
<div className="max-w-2xl mx-auto mt-16 px-4">
|
<div className="max-w-2xl mx-auto mt-16 px-4">
|
||||||
<div className="text-center mb-10">
|
<div className="text-center mb-10">
|
||||||
<div className="inline-block p-4 bg-indigo-100 rounded-full mb-4">
|
<div className="inline-block p-4 bg-indigo-100 rounded-full mb-4">
|
||||||
<Sparkles className="text-indigo-600" size={32} />
|
<UserPlus className="text-indigo-600" size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-slate-900 mb-4">Create Outreach Campaign</h2>
|
<h2 className="text-3xl font-bold text-slate-900 mb-4">Create Outreach Campaign</h2>
|
||||||
<p className="text-lg text-slate-600">
|
<p className="text-lg text-slate-600">
|
||||||
@@ -301,7 +364,7 @@ export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, r
|
|||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="animate-spin" /> Synthesizing Insights...
|
<Loader2 className="animate-spin" /> Identifying Roles & Drafting Campaigns...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal } from "../types";
|
import { LeadStatus, AnalysisResult, Competitor, Language, Tier, EmailDraft, SearchStrategy, SearchSignal, OutreachResponse } 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
|
|
||||||
|
|
||||||
// URL Konfiguration:
|
// URL Konfiguration:
|
||||||
// Im Production-Build (Docker/Nginx) nutzen wir den relativen Pfad '/api', da Nginx als Reverse Proxy fungiert.
|
// 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]) {
|
if (jsonMatch && jsonMatch[1]) {
|
||||||
try { return JSON.parse(jsonMatch[1]); } catch (e2) {}
|
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) {
|
if (arrayMatch) {
|
||||||
try { return JSON.parse(arrayMatch[0]); } catch (e3) {}
|
try { return JSON.parse(arrayMatch[0]); } catch (e3) {}
|
||||||
}
|
}
|
||||||
@@ -175,9 +172,10 @@ export const generateOutreachCampaign = async (
|
|||||||
companyData: AnalysisResult,
|
companyData: AnalysisResult,
|
||||||
knowledgeBase: string,
|
knowledgeBase: string,
|
||||||
language: Language,
|
language: Language,
|
||||||
referenceUrl: string
|
referenceUrl: string,
|
||||||
): Promise<EmailDraft[]> => {
|
specificRole?: string
|
||||||
console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName}...`);
|
): Promise<OutreachResponse> => {
|
||||||
|
console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName} (Role: ${specificRole || "All"})...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/generate-outreach`, {
|
const response = await fetch(`${API_BASE_URL}/generate-outreach`, {
|
||||||
@@ -188,7 +186,8 @@ export const generateOutreachCampaign = async (
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
companyData,
|
companyData,
|
||||||
knowledgeBase,
|
knowledgeBase,
|
||||||
referenceUrl
|
referenceUrl,
|
||||||
|
specific_role: specificRole
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,45 +196,57 @@ export const generateOutreachCampaign = async (
|
|||||||
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json(); // Expected: { campaigns: [...], available_roles: [...] } or single object
|
||||||
console.log(`Frontend: Outreach-Generierung für ${companyData.companyName} erfolgreich.`);
|
console.log(`Frontend: Outreach-Generierung erfolgreich.`, result);
|
||||||
|
|
||||||
// Transform new backend structure to match frontend EmailDraft interface
|
let rawCampaigns: any[] = [];
|
||||||
if (Array.isArray(result)) {
|
let availableRoles: string[] = [];
|
||||||
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)) {
|
// Handle Mode A (Batch) vs Mode B (Single) response structures
|
||||||
item.emails.forEach((mail: any, idx: number) => {
|
if (result.campaigns && Array.isArray(result.campaigns)) {
|
||||||
fullBody += `### Email ${idx + 1}: ${mail.subject}\n\n`;
|
rawCampaigns = result.campaigns;
|
||||||
fullBody += `${mail.body}\n\n`;
|
availableRoles = result.available_roles || [];
|
||||||
if (idx < item.emails.length - 1) fullBody += `\n---\n\n`;
|
} else if (result.target_role && result.emails) {
|
||||||
});
|
// Single campaign response (Mode B)
|
||||||
} else {
|
rawCampaigns = [result];
|
||||||
// Fallback for flat structure or error
|
} else if (Array.isArray(result)) {
|
||||||
fullBody = item.body || "No content generated.";
|
// Legacy fallback
|
||||||
}
|
rawCampaigns = result;
|
||||||
|
|
||||||
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[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error);
|
console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => {
|
export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => {
|
||||||
// Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden
|
// Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden
|
||||||
console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert.");
|
console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert.");
|
||||||
|
|||||||
@@ -89,3 +89,8 @@ export interface EmailDraft {
|
|||||||
body: string;
|
body: string;
|
||||||
keyPoints: string[];
|
keyPoints: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OutreachResponse {
|
||||||
|
campaigns: EmailDraft[];
|
||||||
|
available_roles: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -544,25 +544,64 @@ def analyze_company(company_name, strategy, target_market):
|
|||||||
"dataSource": "Error"
|
"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.
|
Erstellt personalisierte E-Mail-Kampagnen.
|
||||||
Generiert spezifische Ansprachen für verschiedene Rollen (Personas).
|
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')
|
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()
|
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}"
|
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"""
|
prompt = f"""
|
||||||
You are a Strategic Key Account Manager and deeply technical Industry Insider.
|
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}'.
|
Your goal is to write highly personalized, **operationally specific** outreach emails to the company '{company_name}'.
|
||||||
|
|
||||||
--- INPUT 1: YOUR IDENTITY & STRATEGY (The Sender) ---
|
--- 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}
|
{knowledge_base_content}
|
||||||
|
|
||||||
--- INPUT 2: THE TARGET COMPANY (Audit Facts) ---
|
--- 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) ---
|
--- INPUT 3: THE REFERENCE CLIENT (Social Proof) ---
|
||||||
Reference Client URL: {reference_url}
|
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).
|
CRITICAL: This 'Reference Client' is an existing happy customer of ours. You MUST mention them by name to establish trust.
|
||||||
You MUST mention this Reference Client by name (derive it from the URL, e.g., 'schindler.com' -> 'Schindler') to establish trust.
|
|
||||||
|
|
||||||
--- TASK ---
|
{task_description}
|
||||||
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.
|
|
||||||
|
|
||||||
--- TONE & STYLE GUIDELINES (CRITICAL) ---
|
--- TONE & STYLE GUIDELINES (CRITICAL) ---
|
||||||
- **Perspective:** Operational Expert & Insider. NOT generic marketing.
|
- **Perspective:** Operational Expert & Insider. NOT generic marketing.
|
||||||
- **Be Gritty & Specific:** Do NOT use fluff like "optimize efficiency" or "streamline processes" without context.
|
- **Be Gritty & Specific:** Use hard, operational keywords from the Knowledge Base (e.g., "ASNs", "8D-Reports").
|
||||||
- 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.
|
|
||||||
- **Narrative Arc:**
|
- **Narrative Arc:**
|
||||||
1. "I noticed [Fact from Audit/Tech Stack]..." (e.g., "You rely on PDF orders via Jaggaer...")
|
1. "I noticed [Fact from Audit]..."
|
||||||
2. "In [Industry], this often leads to [Operational Pain]..." (e.g., "missing ASNs causing delays at the hub.")
|
2. "In [Industry], this often leads to [Pain]..."
|
||||||
3. "We helped [Reference Client Name] solve exactly this by [Specific Solution]..."
|
3. "We helped [Reference Client] solve this..."
|
||||||
4. "Let's discuss how to get [Operational Gain] without replacing your ERP."
|
4. "Let's discuss [Gain]."
|
||||||
- **Mandatory Social Proof:** You MUST mention the Reference Client Name (from Input 3) in the email body or footer.
|
- **Language:** German.
|
||||||
- **Language:** German (as the inputs are German).
|
|
||||||
|
|
||||||
--- OUTPUT FORMAT (Strictly JSON) ---
|
{output_format}
|
||||||
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)
|
|
||||||
]
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
@@ -626,12 +634,10 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Sende Campaign-Anfrage an Gemini API...")
|
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 = requests.post(GEMINI_API_URL, json=payload, headers={'Content-Type': 'application/json'})
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
response_data = response.json()
|
response_data = response.json()
|
||||||
logger.info(f"Gemini API-Antwort erhalten (Status: {response.status_code}).")
|
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']
|
text = response_data['candidates'][0]['content']['parts'][0]['text']
|
||||||
result = _extract_json_from_text(text)
|
result = _extract_json_from_text(text)
|
||||||
@@ -642,7 +648,7 @@ def generate_outreach_campaign(company_data_json, knowledge_base_content, refere
|
|||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Campaign generation failed for {company_name}: {e}")
|
logger.error(f"Campaign generation failed for {company_name}: {e}")
|
||||||
return [{"error": str(e)}]
|
return {"error": str(e)}
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
@@ -653,7 +659,8 @@ def main():
|
|||||||
parser.add_argument("--company_name")
|
parser.add_argument("--company_name")
|
||||||
parser.add_argument("--strategy_json")
|
parser.add_argument("--strategy_json")
|
||||||
parser.add_argument("--summary_of_offer")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.mode == "generate_strategy":
|
if args.mode == "generate_strategy":
|
||||||
@@ -671,7 +678,7 @@ def main():
|
|||||||
elif args.mode == "generate_outreach":
|
elif args.mode == "generate_outreach":
|
||||||
with open(args.company_data_file, "r") as f: company_data = json.load(f)
|
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()
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user