Seth
update
952e292
import React, { useState, useEffect } from 'react';
import { Send, Download, Loader2, CheckCircle2, AlertCircle, Settings, RefreshCw } from 'lucide-react';
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { motion } from 'framer-motion';
export default function SmartleadPanel({ uploadedFile, sequencesCount, onPushComplete }) {
const [selectedCampaignId, setSelectedCampaignId] = useState('');
const [campaigns, setCampaigns] = useState([]);
const [isLoadingCampaigns, setIsLoadingCampaigns] = useState(false);
const [dryRun, setDryRun] = useState(false);
const [isPushing, setIsPushing] = useState(false);
const [pushResult, setPushResult] = useState(null);
const [error, setError] = useState(null);
// Fetch campaigns on component mount
useEffect(() => {
fetchCampaigns();
}, []);
const fetchCampaigns = async () => {
setIsLoadingCampaigns(true);
setError(null);
try {
const response = await fetch('/api/smartlead-campaigns');
const data = await response.json();
if (response.ok) {
setCampaigns(data.campaigns || []);
} else {
setError('Failed to fetch campaigns: ' + (data.detail || 'Unknown error'));
}
} catch (err) {
setError('Network error: ' + err.message);
} finally {
setIsLoadingCampaigns(false);
}
};
const handlePushToSmartlead = async () => {
if (!uploadedFile?.fileId) {
setError('No file uploaded');
return;
}
if (!selectedCampaignId) {
setError('Please select a campaign');
return;
}
setIsPushing(true);
setError(null);
setPushResult(null);
try {
const response = await fetch('/api/push-to-smartlead', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_id: uploadedFile.fileId,
campaign_id: selectedCampaignId,
dry_run: dryRun
})
});
const data = await response.json();
if (response.ok) {
setPushResult(data);
onPushComplete?.(data);
} else {
setError(data.detail || 'Failed to push to Smartlead');
}
} catch (err) {
setError('Network error: ' + err.message);
} finally {
setIsPushing(false);
}
};
const handleDownloadCSV = async () => {
try {
const response = await fetch(`/api/download-sequences?file_id=${uploadedFile.fileId}`);
if (response.ok) {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'email_sequences_fixed.csv';
a.click();
URL.revokeObjectURL(url);
} else {
alert('Failed to download CSV. Please try again.');
}
} catch (error) {
console.error('Download error:', error);
alert('Error downloading CSV. Please try again.');
}
};
return (
<div className="w-full">
<div className="rounded-2xl border border-slate-200 bg-white p-6">
<div className="flex items-center gap-3 mb-6">
<div className="rounded-xl bg-blue-100 p-3">
<Settings className="h-6 w-6 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-slate-800">Send via Smartlead</h3>
<p className="text-sm text-slate-500">Push sequences to Smartlead campaign</p>
</div>
</div>
{/* Campaign Selector */}
<div className="mb-6">
<div className="flex items-center justify-between mb-2">
<label className="text-sm font-medium text-slate-700">
Select Campaign
</label>
<Button
type="button"
variant="ghost"
size="sm"
onClick={fetchCampaigns}
disabled={isLoadingCampaigns}
className="h-7 text-xs"
>
<RefreshCw className={`h-3 w-3 mr-1 ${isLoadingCampaigns ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
{isLoadingCampaigns ? (
<div className="flex items-center justify-center p-4 border border-slate-200 rounded-lg">
<Loader2 className="h-4 w-4 animate-spin text-slate-400 mr-2" />
<span className="text-sm text-slate-500">Loading campaigns...</span>
</div>
) : campaigns.length === 0 ? (
<div className="p-4 border border-slate-200 rounded-lg bg-slate-50">
<p className="text-sm text-slate-600 text-center">
No campaigns found. Please create a campaign in Smartlead first.
</p>
</div>
) : (
<Select value={selectedCampaignId} onValueChange={setSelectedCampaignId}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a campaign" />
</SelectTrigger>
<SelectContent>
{campaigns.map((campaign) => (
<SelectItem key={campaign.id} value={campaign.id}>
{campaign.name} ({campaign.id})
</SelectItem>
))}
</SelectContent>
</Select>
)}
<p className="text-xs text-slate-500 mt-1">
Select an existing campaign to add leads to. Create campaigns manually in Smartlead using the CSV download.
</p>
</div>
{/* Dry Run Toggle */}
<div className="mb-6 flex items-center gap-2">
<input
type="checkbox"
id="dry-run"
checked={dryRun}
onChange={(e) => setDryRun(e.target.checked)}
className="rounded border-slate-300"
/>
<label htmlFor="dry-run" className="text-sm text-slate-700">
Dry run (generate but do not call Smartlead)
</label>
</div>
{/* Error Display */}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-4 p-3 rounded-lg bg-red-50 border border-red-200 flex items-start gap-2"
>
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-red-800">Error</p>
<p className="text-sm text-red-600">{error}</p>
</div>
</motion.div>
)}
{/* Success Result */}
{pushResult && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="mb-4 p-4 rounded-lg bg-green-50 border border-green-200"
>
<div className="flex items-start gap-2 mb-3">
<CheckCircle2 className="h-5 w-5 text-green-600 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-green-800 mb-2">
{pushResult.status === 'dry_run_completed' ? 'Dry Run Completed' : 'Push Successful'}
</p>
{pushResult.campaign_id && (
<p className="text-xs text-green-700 mb-1">
Campaign ID: <span className="font-mono">{pushResult.campaign_id}</span>
</p>
)}
{pushResult.campaign_name && (
<p className="text-xs text-green-700 mb-1">
Campaign: {pushResult.campaign_name}
</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-xs">
<div>
<span className="text-green-600">Total:</span> {pushResult.total}
</div>
<div>
<span className="text-green-600">Added:</span> {pushResult.added}
</div>
{pushResult.skipped > 0 && (
<div>
<span className="text-green-600">Skipped:</span> {pushResult.skipped}
</div>
)}
{pushResult.failed > 0 && (
<div>
<span className="text-red-600">Failed:</span> {pushResult.failed}
</div>
)}
</div>
{pushResult.errors && pushResult.errors.length > 0 && (
<div className="mt-2 pt-2 border-t border-green-200">
<p className="text-xs font-medium text-green-800 mb-1">Errors:</p>
<div className="max-h-32 overflow-y-auto">
{pushResult.errors.slice(0, 5).map((err, idx) => (
<p key={idx} className="text-xs text-red-600">
{err.email}: {err.error}
</p>
))}
</div>
</div>
)}
</motion.div>
)}
{/* Actions */}
<div className="flex gap-3">
<Button
onClick={handlePushToSmartlead}
disabled={isPushing || !uploadedFile?.fileId}
className="flex-1 bg-blue-600 hover:bg-blue-700"
>
{isPushing ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Pushing...
</>
) : (
<>
<Send className="h-4 w-4 mr-2" />
Generate + Push to Smartlead
</>
)}
</Button>
<Button
onClick={handleDownloadCSV}
variant="outline"
className="flex-1"
>
<Download className="h-4 w-4 mr-2" />
Download CSV
</Button>
</div>
</div>
</div>
);
}