Dateien nach "general-market-intelligence/components" hochladen
This commit is contained in:
244
general-market-intelligence/components/StepInput.tsx
Normal file
244
general-market-intelligence/components/StepInput.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
313
general-market-intelligence/components/StepOutreach.tsx
Normal file
313
general-market-intelligence/components/StepOutreach.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
65
general-market-intelligence/components/StepProcessing.tsx
Normal file
65
general-market-intelligence/components/StepProcessing.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
204
general-market-intelligence/components/StepReport.tsx
Normal file
204
general-market-intelligence/components/StepReport.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
122
general-market-intelligence/components/StepReview.tsx
Normal file
122
general-market-intelligence/components/StepReview.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user