- 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.
323 lines
14 KiB
TypeScript
323 lines
14 KiB
TypeScript
|
|
import React, { useState } from 'react';
|
|
import { AnalysisResult, EmailDraft, Language } from '../types';
|
|
import { generateOutreachCampaign, translateEmailDrafts } from '../services/geminiService';
|
|
import { Upload, FileText, Sparkles, Copy, Check, Loader2, ArrowRight, CheckCircle2, Download, Languages } from 'lucide-react';
|
|
|
|
interface StepOutreachProps {
|
|
company: AnalysisResult;
|
|
language: Language;
|
|
referenceUrl: string;
|
|
onBack: () => void;
|
|
knowledgeBase?: string; // New prop for pre-loaded context
|
|
}
|
|
|
|
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) {
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
setFileContent(event.target?.result as string);
|
|
setFileName(file.name);
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
|
|
const handleGenerate = async () => {
|
|
if (!fileContent) return;
|
|
setIsProcessing(true);
|
|
try {
|
|
const drafts = await generateOutreachCampaign(company, fileContent, language, referenceUrl);
|
|
setEmails(drafts);
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('Failed to generate campaign. Please try again.');
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
};
|
|
|
|
const handleTranslate = async (targetLang: Language) => {
|
|
if (emails.length === 0) return;
|
|
setIsTranslating(true);
|
|
try {
|
|
const translated = await translateEmailDrafts(emails, targetLang);
|
|
setEmails(translated);
|
|
} catch (e) {
|
|
console.error("Translation failed", e);
|
|
alert("Translation failed.");
|
|
} finally {
|
|
setIsTranslating(false);
|
|
}
|
|
}
|
|
|
|
const handleDownloadMD = () => {
|
|
if (emails.length === 0) return;
|
|
|
|
let content = `# Hyper-Personalized Campaign: ${company.companyName}\n\n`;
|
|
content += `Generated by parcelLab Intel\n\n`;
|
|
|
|
emails.forEach((email, index) => {
|
|
content += `## Variant ${index + 1}: ${email.persona}\n\n`;
|
|
content += `**Subject:** ${email.subject}\n\n`;
|
|
content += `**Body:**\n\n${email.body}\n\n`;
|
|
content += `**Analysis:**\n`;
|
|
email.keyPoints.forEach(kp => content += `- ${kp}\n`);
|
|
content += `\n---\n\n`;
|
|
});
|
|
|
|
const blob = new Blob([content], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `campaign_${company.companyName.replace(/\s+/g, '_')}_variants.md`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
// Helper to render text with bold syntax **text**
|
|
const renderBoldText = (text: string) => {
|
|
const parts = text.split(/(\*\*.*?\*\*)/g);
|
|
return parts.map((part, index) => {
|
|
if (part.startsWith('**') && part.endsWith('**')) {
|
|
return <strong key={index} className="font-bold text-slate-900">{part.slice(2, -2)}</strong>;
|
|
}
|
|
return <span key={index}>{part}</span>;
|
|
});
|
|
};
|
|
|
|
if (emails.length > 0) {
|
|
const activeEmail = emails[activeTab];
|
|
return (
|
|
<div className="max-w-6xl mx-auto mt-8 px-4 pb-24">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-slate-900">Hyper-Personalized Campaign</h2>
|
|
<p className="text-slate-600">Target: <span className="font-semibold text-indigo-600">{company.companyName}</span></p>
|
|
</div>
|
|
<button onClick={onBack} className="text-slate-500 hover:text-slate-800 font-medium text-sm">
|
|
Close & Return
|
|
</button>
|
|
</div>
|
|
|
|
{/* Toolbar */}
|
|
<div className="flex items-center gap-4 mb-4 justify-end">
|
|
<div className="flex items-center bg-white border border-slate-200 rounded-lg p-1 shadow-sm">
|
|
<span className="text-xs font-bold text-slate-400 uppercase px-2 flex items-center gap-1">
|
|
<Languages size={14} /> Translate
|
|
</span>
|
|
<div className="h-4 w-px bg-slate-200 mx-1"></div>
|
|
<button
|
|
onClick={() => handleTranslate('de')}
|
|
disabled={isTranslating}
|
|
className="px-3 py-1 text-sm font-medium hover:bg-slate-100 rounded text-slate-700 disabled:opacity-50"
|
|
>
|
|
DE
|
|
</button>
|
|
<button
|
|
onClick={() => handleTranslate('en')}
|
|
disabled={isTranslating}
|
|
className="px-3 py-1 text-sm font-medium hover:bg-slate-100 rounded text-slate-700 disabled:opacity-50"
|
|
>
|
|
EN
|
|
</button>
|
|
<button
|
|
onClick={() => handleTranslate('fr')}
|
|
disabled={isTranslating}
|
|
className="px-3 py-1 text-sm font-medium hover:bg-slate-100 rounded text-slate-700 disabled:opacity-50"
|
|
>
|
|
FR
|
|
</button>
|
|
{isTranslating && <Loader2 className="animate-spin ml-2 text-indigo-600" size={14} />}
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleDownloadMD}
|
|
className="bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 font-medium py-2 px-4 rounded-lg flex items-center gap-2 text-sm shadow-sm transition-colors"
|
|
>
|
|
<Download size={16} /> Download All Variants (.md)
|
|
</button>
|
|
</div>
|
|
|
|
<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 */}
|
|
<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]">
|
|
{emails.map((email, idx) => (
|
|
<button
|
|
key={idx}
|
|
onClick={() => setActiveTab(idx)}
|
|
className={`text-left p-3 rounded-lg text-sm font-medium transition-all group ${
|
|
activeTab === idx
|
|
? 'bg-white shadow-sm text-indigo-700 border border-indigo-100 ring-1 ring-indigo-500/20'
|
|
: 'text-slate-600 hover:bg-slate-200/50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="truncate">{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>
|
|
|
|
{/* Content Area */}
|
|
<div className="flex-1 p-8 flex flex-col overflow-y-auto">
|
|
{activeEmail ? (
|
|
<>
|
|
<div className="mb-6">
|
|
<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">
|
|
{activeEmail.subject}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-col mb-8">
|
|
<div className="flex justify-between items-end mb-1">
|
|
<label className="block text-xs font-bold text-slate-400 uppercase tracking-wider">Email Body</label>
|
|
<button
|
|
onClick={() => copyToClipboard(`${activeEmail.subject}\n\n${activeEmail.body}`)}
|
|
className="flex items-center gap-1.5 text-xs font-bold text-indigo-600 hover:text-indigo-800 bg-indigo-50 hover:bg-indigo-100 px-3 py-1.5 rounded-full transition-colors"
|
|
>
|
|
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
{copied ? "Copied" : "Copy Content"}
|
|
</button>
|
|
</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">
|
|
{renderBoldText(activeEmail.body)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Checklist Section */}
|
|
{activeEmail.keyPoints && activeEmail.keyPoints.length > 0 && (
|
|
<div className="border-t border-slate-100 pt-6">
|
|
<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" />
|
|
Persona & KPI Analysis
|
|
</h3>
|
|
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{activeEmail.keyPoints.map((point, i) => (
|
|
<li key={i} className="flex items-start gap-3 bg-emerald-50/50 p-3 rounded-lg border border-emerald-100/50">
|
|
<div className="mt-0.5 bg-emerald-100 text-emerald-600 rounded-full p-0.5">
|
|
<Check size={12} strokeWidth={3} />
|
|
</div>
|
|
<span className="text-sm text-slate-700">{point}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full text-slate-400">Select a variant</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto mt-16 px-4">
|
|
<div className="text-center mb-10">
|
|
<div className="inline-block p-4 bg-indigo-100 rounded-full mb-4">
|
|
<Sparkles className="text-indigo-600" size={32} />
|
|
</div>
|
|
<h2 className="text-3xl font-bold text-slate-900 mb-4">Create Outreach Campaign</h2>
|
|
<p className="text-lg text-slate-600">
|
|
Generating emails for <span className="font-bold text-slate-900">{company.companyName}</span>.
|
|
<br/>Please upload your marketing knowledge base (Markdown/Text) to begin.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-2xl shadow-xl border border-slate-200 p-8">
|
|
{!fileContent ? (
|
|
<div className="border-2 border-dashed border-slate-300 rounded-xl p-12 text-center hover:border-indigo-400 hover:bg-slate-50 transition-all">
|
|
<input
|
|
type="file"
|
|
accept=".md,.txt,.markdown"
|
|
onChange={handleFileUpload}
|
|
id="file-upload"
|
|
className="hidden"
|
|
/>
|
|
<label htmlFor="file-upload" className="cursor-pointer flex flex-col items-center gap-4">
|
|
<div className="bg-slate-100 p-4 rounded-full text-slate-400">
|
|
<Upload size={32} />
|
|
</div>
|
|
<div>
|
|
<p className="text-lg font-semibold text-slate-900">Click to upload Knowledge Base</p>
|
|
<p className="text-sm text-slate-500 mt-1">Supported files: .md, .txt</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center gap-4 p-4 bg-emerald-50 border border-emerald-100 rounded-xl">
|
|
<div className="bg-emerald-100 p-2 rounded-lg">
|
|
<FileText className="text-emerald-600" size={24} />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="font-semibold text-emerald-900">Knowledge Base Loaded</p>
|
|
<p className="text-sm text-emerald-700">{fileName}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => { setFileContent(''); setFileName(''); }}
|
|
className="text-emerald-600 hover:text-emerald-800 text-sm font-medium"
|
|
>
|
|
Change
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleGenerate}
|
|
disabled={isProcessing}
|
|
className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white font-bold py-4 rounded-xl shadow-lg shadow-indigo-600/20 transition-all transform active:scale-[0.99] flex items-center justify-center gap-2 text-lg"
|
|
>
|
|
{isProcessing ? (
|
|
<>
|
|
<Loader2 className="animate-spin" /> Synthesizing Insights...
|
|
</>
|
|
) : (
|
|
<>
|
|
Generate Campaign <ArrowRight size={20} />
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="mt-8 text-center">
|
|
<button onClick={onBack} className="text-slate-500 hover:text-slate-800 font-medium">
|
|
Cancel and go back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|