| | 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); |
| |
|
| | |
| | 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> |
| | ); |
| | } |
| |
|