feat(market-intel): implement role-based campaign engine and gritty reporting
- Implementierung der rollenbasierten Campaign-Engine mit operativem Fokus (Grit). - Integration von Social Proof (Referenzkunden) in die E-Mail-Generierung. - Erweiterung des Deep Tech Audits um gezielte Wettbewerber-Recherche (Technographic Search). - Fix des Lösch-Bugs in der Target-Liste und Optimierung des Frontend-States. - Erweiterung des Markdown-Exports um transparente Proof-Links und Evidenz. - Aktualisierung der Dokumentation in readme.md und market_intel_backend_plan.md.
This commit is contained in:
@@ -222,6 +222,7 @@ const App: React.FC = () => {
|
||||
language={language}
|
||||
referenceUrl={referenceUrl}
|
||||
onBack={handleBack}
|
||||
knowledgeBase={productContext}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
@@ -9,17 +9,26 @@ interface StepOutreachProps {
|
||||
language: Language;
|
||||
referenceUrl: string;
|
||||
onBack: () => void;
|
||||
knowledgeBase?: string; // New prop for pre-loaded context
|
||||
}
|
||||
|
||||
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack }) => {
|
||||
const [fileContent, setFileContent] = useState<string>('');
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack, knowledgeBase }) => {
|
||||
const [fileContent, setFileContent] = useState<string>(knowledgeBase || '');
|
||||
const [fileName, setFileName] = useState<string>(knowledgeBase ? 'Knowledge Base from Strategy Step' : '');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isTranslating, setIsTranslating] = useState(false);
|
||||
const [emails, setEmails] = useState<EmailDraft[]>([]);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// If knowledgeBase prop changes, update state (useful if it loads late)
|
||||
React.useEffect(() => {
|
||||
if (knowledgeBase && !fileContent) {
|
||||
setFileContent(knowledgeBase);
|
||||
setFileName('Knowledge Base from Strategy Step');
|
||||
}
|
||||
}, [knowledgeBase]);
|
||||
|
||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
|
||||
@@ -46,8 +46,26 @@ export const StepReport: React.FC<StepReportProps> = ({ results, strategy, onRes
|
||||
const headers = ["Company", "Prio", "Rev/Emp", "Status", ...signalHeaders, "Recommendation"];
|
||||
|
||||
const rows = sortedResults.map(r => {
|
||||
const signalValues = strategy.signals.map(s => r.dynamicAnalysis[s.id]?.value || '-');
|
||||
return `| ${r.companyName} | ${r.tier} | ${r.revenue} / ${r.employees} | ${r.status} | ${signalValues.join(" | ")} | ${r.recommendation} |`;
|
||||
const signalValues = strategy.signals.map(s => {
|
||||
const data = r.dynamicAnalysis[s.id];
|
||||
if (!data) return '-';
|
||||
|
||||
let content = data.value || '-';
|
||||
// Sanitize content pipes
|
||||
content = content.replace(/\|/g, '\\|');
|
||||
|
||||
if (data.proof) {
|
||||
// Sanitize proof pipes and newlines
|
||||
const safeProof = data.proof.replace(/\|/g, '\\|').replace(/(\r\n|\n|\r)/gm, ' ');
|
||||
content += `<br><sub>*Proof: ${safeProof}*</sub>`;
|
||||
}
|
||||
return content;
|
||||
});
|
||||
|
||||
// Helper to sanitize other fields
|
||||
const safe = (str: string) => (str || '').replace(/\|/g, '\\|').replace(/(\r\n|\n|\r)/gm, ' ');
|
||||
|
||||
return `| ${safe(r.companyName)} | ${r.tier} | ${safe(r.revenue)} / ${safe(r.employees)} | ${r.status} | ${signalValues.join(" | ")} | ${safe(r.recommendation)} |`;
|
||||
});
|
||||
|
||||
const content = `
|
||||
|
||||
@@ -30,14 +30,18 @@ export const StepReview: React.FC<StepReviewProps> = ({ competitors, categorized
|
||||
};
|
||||
|
||||
const renderCompetitorList = (comps: Competitor[], category: string) => {
|
||||
if (!comps || comps.length === 0) {
|
||||
// Filter out competitors that have been removed from the main list
|
||||
const activeIds = new Set(competitors.map(c => c.id));
|
||||
const activeComps = comps.filter(c => activeIds.has(c.id));
|
||||
|
||||
if (!activeComps || activeComps.length === 0) {
|
||||
return (
|
||||
<li className="p-4 text-center text-slate-500 italic bg-white rounded-md border border-slate-100 mb-2 last:mb-0">
|
||||
Keine {category} Konkurrenten gefunden.
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return comps.map((comp) => (
|
||||
return activeComps.map((comp) => (
|
||||
<li key={comp.id} className="flex items-start justify-between p-4 hover:bg-slate-50 transition-colors group bg-white rounded-md border border-slate-100 mb-2 last:mb-0">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -269,6 +269,89 @@ app.post('/api/analyze-company', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// API-Endpunkt für generate-outreach
|
||||
app.post('/api/generate-outreach', async (req, res) => {
|
||||
console.log(`[${new Date().toISOString()}] HIT: /api/generate-outreach`);
|
||||
const { companyData, knowledgeBase, referenceUrl } = req.body;
|
||||
|
||||
if (!companyData || !knowledgeBase) {
|
||||
console.error('Validation Error: Missing companyData or knowledgeBase for generate-outreach.');
|
||||
return res.status(400).json({ error: 'Missing companyData or knowledgeBase' });
|
||||
}
|
||||
|
||||
const tempDataFilePath = path.join(__dirname, 'tmp', `outreach_data_${Date.now()}.json`);
|
||||
const tempContextFilePath = path.join(__dirname, 'tmp', `outreach_context_${Date.now()}.md`);
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
if (!fs.existsSync(tmpDir)) {
|
||||
fs.mkdirSync(tmpDir);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(tempDataFilePath, JSON.stringify(companyData));
|
||||
fs.writeFileSync(tempContextFilePath, knowledgeBase);
|
||||
console.log(`Successfully wrote temporary files for outreach.`);
|
||||
|
||||
const pythonExecutable = path.join(__dirname, '..', '.venv', 'bin', 'python3');
|
||||
const pythonScript = path.join(__dirname, '..', 'market_intel_orchestrator.py');
|
||||
|
||||
const scriptArgs = [
|
||||
pythonScript,
|
||||
'--mode', 'generate_outreach',
|
||||
'--company_data_file', tempDataFilePath,
|
||||
'--context_file', tempContextFilePath,
|
||||
'--reference_url', referenceUrl || ''
|
||||
];
|
||||
|
||||
console.log(`Spawning Outreach Generation for ${companyData.companyName}...`);
|
||||
|
||||
const pythonProcess = spawn(pythonExecutable, scriptArgs, {
|
||||
env: { ...process.env, PYTHONPATH: path.join(__dirname, '..', '.venv', 'lib', 'python3.11', 'site-packages') }
|
||||
});
|
||||
|
||||
let pythonOutput = '';
|
||||
let pythonError = '';
|
||||
|
||||
pythonProcess.stdout.on('data', (data) => {
|
||||
pythonOutput += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.stderr.on('data', (data) => {
|
||||
pythonError += data.toString();
|
||||
});
|
||||
|
||||
pythonProcess.on('close', (code) => {
|
||||
console.log(`Outreach Generation finished with exit code: ${code}`);
|
||||
|
||||
// Clean up
|
||||
if (fs.existsSync(tempDataFilePath)) fs.unlinkSync(tempDataFilePath);
|
||||
if (fs.existsSync(tempContextFilePath)) fs.unlinkSync(tempContextFilePath);
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`Python script (generate_outreach) exited with error.`);
|
||||
return res.status(500).json({ error: 'Python script failed', details: pythonError });
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(pythonOutput);
|
||||
res.json(result);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Python output (generate_outreach) as JSON:', parseError);
|
||||
res.status(500).json({ error: 'Invalid JSON from Python script', rawOutput: pythonOutput, details: pythonError });
|
||||
}
|
||||
});
|
||||
|
||||
pythonProcess.on('error', (err) => {
|
||||
console.error(`FATAL: Failed to start python process for outreach.`, err);
|
||||
if (fs.existsSync(tempDataFilePath)) fs.unlinkSync(tempDataFilePath);
|
||||
if (fs.existsSync(tempContextFilePath)) fs.unlinkSync(tempContextFilePath);
|
||||
res.status(500).json({ error: 'Failed to start Python process', details: err.message });
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(`Internal Server Error in /api/generate-outreach: ${err.message}`);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Start des Servers
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Node.js API Bridge running on http://localhost:${PORT}`);
|
||||
|
||||
@@ -170,11 +170,65 @@ export const generateOutreachCampaign = async (
|
||||
language: Language,
|
||||
referenceUrl: string
|
||||
): Promise<EmailDraft[]> => {
|
||||
// Dieser Teil muss noch im Python-Backend implementiert werden
|
||||
console.warn("generateOutreachCampaign ist noch nicht im Python-Backend implementiert.");
|
||||
return [];
|
||||
};
|
||||
console.log(`Frontend: Starte Outreach-Generierung für ${companyData.companyName}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/generate-outreach`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
companyData,
|
||||
knowledgeBase,
|
||||
referenceUrl
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`Backend-Fehler: ${errorData.error || response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log(`Frontend: Outreach-Generierung für ${companyData.companyName} erfolgreich.`);
|
||||
|
||||
// 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.";
|
||||
}
|
||||
|
||||
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 [];
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Frontend: Outreach-Generierung fehlgeschlagen für ${companyData.companyName}`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const translateEmailDrafts = async (drafts: EmailDraft[], targetLanguage: Language): Promise<EmailDraft[]> => {
|
||||
// Dieser Teil muss noch im Python-Backend oder direkt im Frontend implementiert werden
|
||||
console.warn("translateEmailDrafts ist noch nicht im Python-Backend implementiert.");
|
||||
|
||||
Reference in New Issue
Block a user