Dateien nach "general-market-intelligence/components" hochladen

This commit is contained in:
2025-12-20 20:58:40 +00:00
parent f60ce3c356
commit 68d52a9896
5 changed files with 948 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
import React, { useState } from 'react';
import { Search, ArrowRight, Loader2, Globe, Link as LinkIcon, Languages, Upload, FileText, X, FolderOpen, FileUp } from 'lucide-react';
import { Language, AnalysisResult, SearchStrategy } from '../types';
import { parseMarkdownReport } from '../utils/reportParser';
interface StepInputProps {
onSearch: (url: string, context: string, market: string, language: Language) => void;
onLoadReport: (strategy: SearchStrategy, results: AnalysisResult[]) => void;
isLoading: boolean;
}
const COUNTRIES = [
"Germany", "Austria", "Switzerland", "United Kingdom", "France", "Spain", "Italy", "Netherlands", "Europe (General)", "USA"
];
export const StepInput: React.FC<StepInputProps> = ({ onSearch, onLoadReport, isLoading }) => {
const [activeMode, setActiveMode] = useState<'new' | 'load'>('new');
const [url, setUrl] = useState('');
const [fileContent, setFileContent] = useState('');
const [fileName, setFileName] = useState('');
const [market, setMarket] = useState(COUNTRIES[0]);
const [language, setLanguage] = useState<Language>('de');
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 handleLoadReport = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (event) => {
const content = event.target?.result as string;
const parsed = parseMarkdownReport(content);
if (parsed) {
onLoadReport(parsed.strategy, parsed.results);
} else {
alert("Could not parse report. Please make sure it's a valid ProspectIntel MD file.");
}
};
reader.readAsText(file);
}
};
const handleRemoveFile = () => {
setFileContent('');
setFileName('');
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (url.trim() && fileContent.trim()) {
onSearch(url.trim(), fileContent.trim(), market, language);
}
};
return (
<div className="max-w-2xl mx-auto mt-12 p-6">
<div className="text-center mb-10">
<h2 className="text-3xl font-bold text-slate-900 mb-4">Market Intelligence Agent</h2>
<div className="flex justify-center gap-2 mb-6">
<button
onClick={() => setActiveMode('new')}
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${activeMode === 'new' ? 'bg-indigo-600 text-white shadow-md' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
>
Start New Audit
</button>
<button
onClick={() => setActiveMode('load')}
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${activeMode === 'load' ? 'bg-indigo-600 text-white shadow-md' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}
>
Load Existing Audit
</button>
</div>
{activeMode === 'new' ? (
<p className="text-lg text-slate-600">
Upload your <strong>Strategy Document</strong> to let AI design the perfect market audit.
</p>
) : (
<p className="text-lg text-slate-600">
Select an exported <strong>.md Report</strong> to continue working on an existing analysis.
</p>
)}
</div>
<div className="bg-white shadow-xl rounded-2xl p-8 border border-slate-100">
{activeMode === 'new' ? (
<form onSubmit={handleSubmit} className="space-y-8">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-3">
1. Strategic Context (Markdown/Text)
<span className="block font-normal text-slate-400 text-xs mt-1">Expected: Offer, Target Groups, Personas, Pain Points, Benefits.</span>
</label>
{!fileContent ? (
<div className="border-2 border-dashed border-slate-300 rounded-xl p-8 text-center hover:border-indigo-400 hover:bg-slate-50 transition-all group">
<input
type="file"
accept=".md,.txt,.markdown"
onChange={handleFileUpload}
id="strategy-upload"
className="hidden"
/>
<label htmlFor="strategy-upload" className="cursor-pointer flex flex-col items-center gap-3 w-full">
<div className="bg-indigo-50 p-3 rounded-full text-indigo-500 group-hover:bg-indigo-100 transition-colors">
<Upload size={24} />
</div>
<div>
<p className="font-semibold text-slate-700">Upload Strategy File</p>
<p className="text-xs text-slate-400 mt-1">.md or .txt files</p>
</div>
</label>
</div>
) : (
<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 shrink-0">
<FileText className="text-emerald-600" size={24} />
</div>
<div className="flex-1 min-w-0">
<p className="font-semibold text-emerald-900 truncate">{fileName}</p>
<p className="text-xs text-emerald-700">Context loaded successfully</p>
</div>
<button
type="button"
onClick={handleRemoveFile}
className="p-2 hover:bg-emerald-100 rounded-full text-emerald-600 transition-colors"
>
<X size={18} />
</button>
</div>
)}
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">2. Reference Company URL</label>
<div className="relative">
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="e.g. https://www.reference-customer.com"
className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-lg"
required
/>
<LinkIcon className="absolute left-4 top-3.5 text-slate-400" size={20} />
</div>
<p className="text-xs text-slate-400 mt-2">Used to calibrate the search and find lookalikes.</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">Target Market</label>
<div className="relative">
<select
value={market}
onChange={(e) => setMarket(e.target.value)}
className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-lg appearance-none bg-white cursor-pointer"
>
{COUNTRIES.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
<Globe className="absolute left-4 top-3.5 text-slate-400" size={20} />
</div>
</div>
<div>
<label className="block text-sm font-semibold text-slate-700 mb-2">Report Language</label>
<div className="relative">
<select
value={language}
onChange={(e) => setLanguage(e.target.value as Language)}
className="w-full pl-12 pr-4 py-3 rounded-xl border border-slate-200 focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all text-lg appearance-none bg-white cursor-pointer"
>
<option value="de">German</option>
<option value="en">English</option>
</select>
<Languages className="absolute left-4 top-3.5 text-slate-400" size={20} />
</div>
</div>
</div>
<button
type="submit"
disabled={isLoading || !url || !fileContent}
className="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-300 disabled:cursor-not-allowed text-white font-bold py-4 rounded-xl shadow-lg shadow-indigo-600/20 transition-all transform active:scale-[0.98] flex items-center justify-center gap-2 text-lg"
>
{isLoading ? (
<>
<Loader2 className="animate-spin" /> Analyzing Strategy...
</>
) : (
<>
Analyze Context & Build Strategy <ArrowRight size={20} />
</>
)}
</button>
</form>
) : (
<div className="space-y-6">
<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 group">
<input
type="file"
accept=".md"
onChange={handleLoadReport}
id="report-load"
className="hidden"
/>
<label htmlFor="report-load" className="cursor-pointer flex flex-col items-center gap-4 w-full">
<div className="bg-indigo-50 p-4 rounded-full text-indigo-500 group-hover:bg-indigo-100 transition-colors">
<FolderOpen size={32} />
</div>
<div>
<p className="text-xl font-semibold text-slate-900">Upload Markdown Report</p>
<p className="text-sm text-slate-500 mt-1">Reconstruct your analysis from a .md file</p>
</div>
<div className="mt-4 bg-white border border-slate-200 px-6 py-2 rounded-lg font-bold text-slate-700 shadow-sm flex items-center gap-2 group-hover:bg-slate-50">
<FileUp size={18} /> Browse Files
</div>
</label>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100">
<h4 className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">Note:</h4>
<p className="text-sm text-slate-600">
Loading an existing audit will take you directly to the Report view. You can then trigger new outreach campaigns for any company in the list.
</p>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,313 @@
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;
}
export const StepOutreach: React.FC<StepOutreachProps> = ({ company, language, referenceUrl, onBack }) => {
const [fileContent, setFileContent] = useState<string>('');
const [fileName, setFileName] = useState<string>('');
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);
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>
);
};

View File

@@ -0,0 +1,65 @@
import React, { useEffect, useRef } from 'react';
import { Loader, TrendingUp, Search, Eye } from 'lucide-react';
import { AnalysisState } from '../types';
interface StepProcessingProps {
state: AnalysisState;
}
export const StepProcessing: React.FC<StepProcessingProps> = ({ state }) => {
const percentage = Math.round((state.completed / state.total) * 100) || 0;
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, [state.completed, state.currentCompany]);
return (
<div className="max-w-3xl mx-auto mt-12 px-4">
<div className="text-center mb-8">
<div className="inline-block p-3 bg-indigo-50 rounded-full mb-4">
<Loader className="animate-spin text-indigo-600" size={32} />
</div>
<h2 className="text-2xl font-bold text-slate-900">Running Market Audit</h2>
<p className="text-slate-600">Analyzing digital traces based on your strategy...</p>
</div>
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200 mb-6">
<div className="flex justify-between text-sm font-medium text-slate-700 mb-2">
<span>Progress</span>
<span>{percentage}% ({state.completed}/{state.total})</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-4 overflow-hidden">
<div className="bg-indigo-600 h-4 rounded-full transition-all duration-500 ease-out" style={{ width: `${percentage}%` }}></div>
</div>
</div>
<div className="bg-slate-900 rounded-xl shadow-xl overflow-hidden font-mono text-sm">
<div className="bg-slate-800 px-4 py-2 flex items-center gap-2 border-b border-slate-700">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span className="ml-2 text-slate-400 text-xs">audit_agent.exe</span>
</div>
<div ref={terminalRef} className="p-6 h-64 overflow-y-auto text-slate-300 space-y-2 scroll-smooth">
<div className="text-emerald-400">$ load_strategy --context="custom"</div>
{state.completed > 0 && <div className="text-slate-500">... {state.completed} companies analyzed.</div>}
{state.currentCompany && (
<>
<div className="animate-pulse text-indigo-300">{`> Analyzing: ${state.currentCompany}`}</div>
<div className="pl-4 border-l-2 border-slate-700 space-y-1 mt-2">
<div className="flex items-center gap-2 text-emerald-400"><TrendingUp size={14} /> Fetching Firmographics (Revenue/Size)...</div>
<div className="flex items-center gap-2"><Eye size={14} /> Scanning Custom Signals...</div>
<div className="flex items-center gap-2"><Search size={14} /> Validating against ICP...</div>
</div>
</>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,204 @@
import React, { useMemo } from 'react';
import { LeadStatus, AnalysisResult, Language, Tier, SearchStrategy } from '../types';
import { ExternalLink, AlertCircle, CheckCircle, XCircle, RefreshCcw, ArrowLeft, Clock, Euro, Download, FileText, ShoppingBag, Star, TrendingUp, Users, Database, Send, Server, Truck, Eye, Search } from 'lucide-react';
interface StepReportProps {
results: AnalysisResult[];
strategy: SearchStrategy;
onRestart: () => void;
onStartOutreach: (company: AnalysisResult) => void;
language: Language;
}
export const StepReport: React.FC<StepReportProps> = ({ results, strategy, onRestart, onStartOutreach, language }) => {
const sortedResults = useMemo(() => {
return [...results].sort((a, b) => {
const tierOrder = { [Tier.TIER_1]: 3, [Tier.TIER_2]: 2, [Tier.TIER_3]: 1 };
const tierDiff = tierOrder[b.tier] - tierOrder[a.tier];
if (tierDiff !== 0) return tierDiff;
if (a.status === LeadStatus.POTENTIAL && b.status !== LeadStatus.POTENTIAL) return -1;
if (a.status !== LeadStatus.POTENTIAL && b.status === LeadStatus.POTENTIAL) return 1;
return 0;
});
}, [results]);
const getStatusColor = (status: LeadStatus) => {
switch (status) {
case LeadStatus.CUSTOMER: return 'bg-blue-50 text-blue-700 border-blue-200';
case LeadStatus.COMPETITOR: return 'bg-red-50 text-red-700 border-red-200';
case LeadStatus.POTENTIAL: return 'bg-emerald-50 text-emerald-700 border-emerald-200';
default: return 'bg-slate-50 text-slate-700 border-slate-200';
}
};
const getTierColor = (tier: Tier) => {
switch (tier) {
case Tier.TIER_1: return 'bg-purple-100 text-purple-800 border-purple-200';
case Tier.TIER_2: return 'bg-indigo-50 text-indigo-700 border-indigo-200';
case Tier.TIER_3: return 'bg-slate-50 text-slate-600 border-slate-200';
}
};
const downloadMarkdown = () => {
const signalHeaders = strategy.signals.map(s => s.name);
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 content = `
# Market Intelligence Report: ${strategy.productContext}
**Context:** ${strategy.idealCustomerProfile}
| ${headers.join(" | ")} |
|${headers.map(() => "---").join("|")}|
${rows.join("\n")}
`;
const blob = new Blob([content], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${new Date().toISOString().slice(0,10)}.md`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="h-[calc(100vh-64px)] flex flex-col max-w-7xl mx-auto px-4 py-6">
<div className="flex-none flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
<div>
<h2 className="text-3xl font-bold text-slate-900">Analysis Report</h2>
<p className="text-slate-500">Context: <span className="font-semibold text-indigo-600">{strategy.productContext}</span></p>
</div>
<div className="flex items-center gap-2">
<button onClick={downloadMarkdown} 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">
<FileText size={16} /> Export MD
</button>
</div>
</div>
<div className="flex-1 bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex flex-col min-h-0">
<div className="overflow-auto flex-1 w-full relative">
<table className="w-full text-left border-collapse relative">
<thead className="sticky top-0 z-10 bg-slate-50 shadow-sm border-b border-slate-200">
<tr>
<th className="px-6 py-4 font-semibold text-slate-500 text-xs uppercase tracking-wider w-[25%] bg-slate-50">Company Profile</th>
<th className="px-6 py-4 font-semibold text-slate-500 text-xs uppercase tracking-wider w-[65%] bg-slate-50">Strategic Intelligence & Audit</th>
<th className="px-6 py-4 font-semibold text-slate-500 text-xs uppercase tracking-wider w-[10%] bg-slate-50 text-center">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{sortedResults.map((row, idx) => (
<tr key={idx} className="hover:bg-slate-50/80 transition-colors align-top">
{/* Column 1: Firmographics */}
<td className="px-6 py-6">
<div className="flex flex-col gap-3">
<div>
<div className="font-bold text-slate-900 text-lg leading-tight">{row.companyName}</div>
<div className="flex flex-wrap gap-2 mt-2">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold border uppercase tracking-wide ${getTierColor(row.tier)}`}>
{row.tier}
</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${getStatusColor(row.status)}`}>
{row.status}
</span>
</div>
</div>
<div className="text-sm text-slate-600 space-y-1 pt-2 border-t border-slate-100">
<div className="flex justify-between items-center">
<span className="text-slate-400 text-xs uppercase">Revenue</span>
<span className="font-medium text-slate-800">{row.revenue}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-400 text-xs uppercase">Employees</span>
<span className="font-medium text-slate-800">{row.employees}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-400 text-xs uppercase">Source</span>
<span className="text-xs text-slate-500 truncate max-w-[100px]" title={row.dataSource}>{row.dataSource}</span>
</div>
</div>
</div>
</td>
{/* Column 2: Intelligence (Stacked) */}
<td className="px-6 py-6">
<div className="space-y-5">
{/* Recommendation Block */}
<div className="bg-indigo-50/50 border border-indigo-100 rounded-lg p-3 relative">
<div className="absolute -top-2.5 left-3 bg-white px-1 text-[10px] font-bold text-indigo-600 uppercase tracking-wide border border-indigo-100 rounded">
AI Recommendation
</div>
<p className="text-sm text-slate-800 leading-relaxed italic mt-1">
"{row.recommendation}"
</p>
</div>
{/* Dynamic Signals Stacked */}
<div className="grid gap-4">
{strategy.signals.map((s, i) => {
const data = row.dynamicAnalysis[s.id];
if (!data) return null;
return (
<div key={s.id} className="group">
<div className="flex items-center gap-2 mb-1">
<div className="w-5 h-5 rounded-full bg-slate-100 text-slate-500 flex items-center justify-center text-[10px] font-bold">
{i + 1}
</div>
<span className="text-xs font-bold text-slate-500 uppercase tracking-wide">{s.name}</span>
</div>
<div className="pl-7">
<div className="text-sm text-slate-900 font-medium leading-snug">
{data.value}
</div>
{data.proof && (
<div className="mt-1.5 inline-flex items-center gap-1.5 text-xs text-slate-500 bg-slate-50 px-2 py-1 rounded border border-slate-100 max-w-full">
<Search size={10} />
<span className="truncate max-w-[400px]">Evidence: {data.proof}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
</td>
{/* Column 3: Action */}
<td className="px-6 py-6 text-center align-middle">
<button
onClick={() => onStartOutreach(row)}
className="bg-indigo-600 hover:bg-indigo-700 text-white p-3 rounded-xl transition-all shadow-sm hover:shadow-lg active:scale-95 flex flex-col items-center justify-center gap-1 w-full group"
title="Generate Outreach"
>
<Send size={20} className="group-hover:-translate-y-0.5 transition-transform" />
<span className="text-[10px] font-medium opacity-80">Outreach</span>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="flex-none flex justify-between items-center pt-6 bg-slate-50 border-t border-slate-200 mt-auto sticky bottom-0 z-20">
<button onClick={onRestart} className="text-slate-600 hover:text-indigo-600 font-medium flex items-center gap-2 px-4 py-2">
<ArrowLeft size={20} /> Back
</button>
<button onClick={onRestart} className="bg-slate-900 hover:bg-slate-800 text-white font-bold py-3 px-6 rounded-xl shadow-lg transition-all flex items-center gap-2">
<RefreshCcw size={18} /> New Search
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { Trash2, Plus, CheckCircle2, ExternalLink, FileText } from 'lucide-react';
import { Competitor } from '../types';
interface StepReviewProps {
competitors: Competitor[];
onRemove: (id: string) => void;
onAdd: (name: string) => void;
onConfirm: () => void;
hasResults?: boolean;
onShowReport?: () => void;
}
export const StepReview: React.FC<StepReviewProps> = ({ competitors, onRemove, onAdd, onConfirm, hasResults, onShowReport }) => {
const [newCompany, setNewCompany] = useState('');
const handleAdd = (e: React.FormEvent) => {
e.preventDefault();
if (newCompany.trim()) {
onAdd(newCompany.trim());
setNewCompany('');
}
};
return (
<div className="max-w-4xl mx-auto mt-8 px-4 pb-24">
<div className="flex items-end justify-between mb-6">
<div>
<h2 className="text-2xl font-bold text-slate-900">Confirm Target List</h2>
<p className="text-slate-600 mt-1">Review the identified companies before starting the deep tech audit.</p>
</div>
<div className="text-right">
<span className="bg-indigo-50 text-indigo-700 font-bold py-1 px-3 rounded-full text-sm">
{competitors.length} Companies
</span>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<ul className="divide-y divide-slate-100">
{competitors.map((comp) => (
<li key={comp.id} className="flex items-start justify-between p-4 hover:bg-slate-50 transition-colors group">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-slate-800 text-lg">{comp.name}</span>
{comp.url && (
<a
href={comp.url}
target="_blank"
rel="noreferrer"
className="text-indigo-500 hover:text-indigo-700 p-1 rounded hover:bg-indigo-50"
title={comp.url}
>
<ExternalLink size={14} />
</a>
)}
</div>
{comp.description && (
<p className="text-sm text-slate-500 mt-1 pr-8">{comp.description}</p>
)}
</div>
<button
onClick={() => onRemove(comp.id)}
className="text-slate-400 hover:text-red-500 p-2 rounded-full hover:bg-red-50 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100 self-center"
aria-label="Remove"
>
<Trash2 size={18} />
</button>
</li>
))}
{competitors.length === 0 && (
<li className="p-8 text-center text-slate-500 italic">
No companies in list. Add some manually below.
</li>
)}
</ul>
<div className="bg-slate-50 p-4 border-t border-slate-200">
<form onSubmit={handleAdd} className="flex gap-2">
<input
type="text"
value={newCompany}
onChange={(e) => setNewCompany(e.target.value)}
placeholder="Add another company manually..."
className="flex-1 px-4 py-2 rounded-lg border border-slate-300 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 outline-none"
/>
<button
type="submit"
disabled={!newCompany.trim()}
className="bg-white border border-slate-300 text-slate-700 hover:bg-slate-100 font-medium px-4 py-2 rounded-lg flex items-center gap-2 disabled:opacity-50"
>
<Plus size={18} /> Add
</button>
</form>
</div>
</div>
{/* Sticky Footer CTA */}
<div className="fixed bottom-0 left-0 right-0 bg-white/80 backdrop-blur-md border-t border-slate-200 p-4 z-40">
<div className="max-w-4xl mx-auto flex justify-end gap-3">
{hasResults && onShowReport && (
<button
onClick={onShowReport}
className="bg-white border border-slate-300 hover:bg-slate-50 text-slate-700 font-bold py-3 px-6 rounded-xl shadow-sm transition-all flex items-center gap-2"
>
<FileText size={20} /> View Report
</button>
)}
<button
onClick={onConfirm}
disabled={competitors.length === 0}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-xl shadow-lg shadow-emerald-600/20 transition-all transform active:scale-[0.98] flex items-center gap-2"
>
<CheckCircle2 size={20} /> {hasResults ? "Restart Analysis" : "Confirm & Run Audit"}
</button>
</div>
</div>
</div>
);
};