Seth commited on
Commit
f485041
·
1 Parent(s): bde7250
backend/app/main.py CHANGED
@@ -283,9 +283,33 @@ async def download_sequences(file_id: str = Query(...), db: Session = Depends(ge
283
  raise HTTPException(status_code=500, detail=f"Error downloading sequences: {str(e)}")
284
 
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  @app.post("/api/push-to-smartlead")
287
  async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends(get_db)):
288
- """Push generated sequences to Smartlead campaign"""
289
  import uuid
290
 
291
  try:
@@ -314,15 +338,30 @@ async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends
314
  contacts[contact_key]['subjects'][seq.email_number] = seq.subject
315
  contacts[contact_key]['bodies'][seq.email_number] = seq.email_content
316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  # Create run record
318
  run_id = str(uuid.uuid4())
319
  run = SmartleadRun(
320
  run_id=run_id,
321
  file_id=request.file_id,
322
- mode=request.mode,
323
- campaign_id=request.campaign_id or '',
324
- campaign_name=request.campaign_name or '',
325
- steps_count=request.steps_count,
326
  dry_run=1 if request.dry_run else 0,
327
  total_leads=len(contacts),
328
  status='pending'
@@ -335,8 +374,7 @@ async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends
335
  return {
336
  "run_id": run_id,
337
  "campaign_id": request.campaign_id,
338
- "campaign_name": request.campaign_name,
339
- "steps_count": request.steps_count,
340
  "total": len(contacts),
341
  "added": 0,
342
  "skipped": 0,
@@ -346,58 +384,8 @@ async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends
346
  "message": "Dry run completed. No leads were sent to Smartlead."
347
  }
348
 
349
- # Initialize Smartlead client
350
- try:
351
- client = SmartleadClient()
352
- except ValueError as e:
353
- run.status = 'failed'
354
- run.error_details = str(e)
355
- db.commit()
356
- raise HTTPException(status_code=400, detail=str(e))
357
-
358
  campaign_id = request.campaign_id
359
 
360
- # Create campaign if mode is 'new'
361
- if request.mode == 'new':
362
- try:
363
- campaign_response = client.create_campaign(request.campaign_name)
364
- campaign_id = campaign_response.get('campaign_id') or campaign_response.get('id')
365
- if not campaign_id:
366
- raise Exception("Campaign creation failed: No campaign_id returned")
367
-
368
- run.campaign_id = campaign_id
369
- db.commit()
370
-
371
- # Build and save sequences
372
- sequences_data = client.build_sequences(request.steps_count)
373
- client.save_campaign_sequence(campaign_id, sequences_data)
374
-
375
- # Define custom fields at campaign level (subject_1, body_1, etc.)
376
- # Smartlead requires custom fields to be defined before they can be used in leads
377
- # Note: This may need to be done via campaign settings or a separate endpoint
378
- # For now, we'll try to define them via update_campaign_settings
379
- custom_fields = {}
380
- for i in range(1, request.steps_count + 1):
381
- custom_fields[f'subject_{i}'] = 'text' # Field type
382
- custom_fields[f'body_{i}'] = 'text' # Field type
383
-
384
- # Try to update campaign settings with custom fields
385
- # Note: The exact API structure may vary - this is an attempt
386
- try:
387
- client.update_campaign_settings(campaign_id, {
388
- "custom_fields": custom_fields
389
- })
390
- except Exception as e:
391
- # If custom fields can't be set via settings, continue anyway
392
- # They might be auto-created when used, or defined differently
393
- pass
394
-
395
- except Exception as e:
396
- run.status = 'failed'
397
- run.error_details = str(e)
398
- db.commit()
399
- raise HTTPException(status_code=500, detail=f"Failed to create campaign: {str(e)}")
400
-
401
  # Prepare leads for Smartlead
402
  leads = []
403
  errors = []
@@ -443,33 +431,14 @@ async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends
443
  continue
444
 
445
  # Build lead object - Smartlead API doesn't allow "company", "title", or custom variables when adding leads
446
- # We'll add leads first, then try to update them with custom variables separately
447
  lead = {
448
  "email": email,
449
  "first_name": first_name,
450
  "last_name": last_name
451
  }
452
 
453
- # Store custom variables separately to update leads after adding
454
- custom_vars = {}
455
- for i in range(1, request.steps_count + 1):
456
- if i in contact.get('subjects', {}):
457
- subject = contact['subjects'][i]
458
- subject_str = safe_str(subject) if subject else ""
459
- if subject_str:
460
- custom_vars[f'subject_{i}'] = subject_str
461
- if i in contact.get('bodies', {}):
462
- body = contact['bodies'][i]
463
- body_str = safe_str(body) if body else ""
464
- if body_str:
465
- custom_vars[f'body_{i}'] = body_str
466
-
467
- # Store lead with custom vars for later update
468
- leads.append({
469
- "lead_data": lead,
470
- "custom_vars": custom_vars,
471
- "email": email
472
- })
473
 
474
  except Exception as e:
475
  errors.append({
@@ -478,103 +447,22 @@ async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends
478
  })
479
  failed_count += 1
480
 
481
- # Step 1: Add leads without custom variables (chunk in batches of 50)
482
  batch_size = 50
483
- leads_to_update = [] # Store leads with their lead_ids for updating custom vars
484
-
485
  for i in range(0, len(leads), batch_size):
486
  batch = leads[i:i + batch_size]
487
- # Extract just the lead_data (without custom_vars) for adding
488
- lead_batch = [item["lead_data"] for item in batch]
489
  try:
490
- response = client.add_leads_to_campaign(campaign_id, lead_batch)
491
- added_count += len(lead_batch)
492
-
493
- # Parse response to extract lead_ids
494
- # Smartlead API response structure may vary - try common patterns
495
- if isinstance(response, dict):
496
- # Check for different response structures
497
- added_leads = response.get('added_leads') or response.get('leads') or response.get('data') or []
498
-
499
- # Map emails to lead_ids from response
500
- email_to_lead_id = {}
501
- if isinstance(added_leads, list):
502
- for lead_info in added_leads:
503
- if isinstance(lead_info, dict):
504
- lead_id = lead_info.get('lead_id') or lead_info.get('id') or lead_info.get('leadId')
505
- lead_email = lead_info.get('email') or lead_info.get('email_address')
506
- if lead_id and lead_email:
507
- email_to_lead_id[lead_email] = lead_id
508
-
509
- # Store leads with their lead_ids for updating
510
- for item in batch:
511
- lead_email = item.get('email')
512
- lead_id = email_to_lead_id.get(lead_email)
513
- if lead_id and item.get('custom_vars'):
514
- leads_to_update.append({
515
- 'lead_id': lead_id,
516
- 'email': lead_email,
517
- 'custom_vars': item['custom_vars']
518
- })
519
- # If response doesn't have lead_ids, store with email for later lookup
520
- if not email_to_lead_id:
521
- for item in batch:
522
- if item.get('custom_vars'):
523
- leads_to_update.append({
524
- 'lead_id': None, # Will try to get from email lookup
525
- 'email': item.get('email'),
526
- 'custom_vars': item['custom_vars']
527
- })
528
-
529
  except Exception as e:
530
  # Mark batch as failed
531
- for item in batch:
532
  errors.append({
533
- "email": item.get('email', 'unknown'),
534
  "error": f"Failed to add lead: {str(e)}"
535
  })
536
  failed_count += 1
537
 
538
- # Step 2: Try to update each successfully added lead with custom variables
539
- # First, fetch lead_id for any leads that don't have it
540
- for item in leads_to_update:
541
- if not item.get('lead_id'):
542
- # Try to fetch lead_id by email
543
- try:
544
- lead_info = client.get_lead_by_email(campaign_id, item['email'])
545
- if lead_info:
546
- lead_id = lead_info.get('lead_id') or lead_info.get('id') or lead_info.get('leadId')
547
- if lead_id:
548
- item['lead_id'] = lead_id
549
- except Exception as e:
550
- # If we can't fetch lead_id, we'll skip updating this lead
551
- pass
552
-
553
- # Now update leads with custom variables
554
- for item in leads_to_update:
555
- if item.get('lead_id'):
556
- try:
557
- # Convert lead_id to int if it's a string
558
- lead_id = int(item['lead_id'])
559
- client.update_lead(campaign_id, lead_id, item['custom_vars'])
560
- except (ValueError, TypeError) as e:
561
- errors.append({
562
- "email": item.get('email', 'unknown'),
563
- "error": f"Lead added but invalid lead_id: {str(e)}"
564
- })
565
- except Exception as e:
566
- # If update fails, log but don't fail the entire operation
567
- errors.append({
568
- "email": item.get('email', 'unknown'),
569
- "error": f"Lead added but custom variables could not be set: {str(e)}"
570
- })
571
- else:
572
- # If we still don't have lead_id after trying to fetch it, log an error
573
- errors.append({
574
- "email": item.get('email', 'unknown'),
575
- "error": "Lead added but lead_id not found - cannot update custom variables. You may need to set them manually in Smartlead UI."
576
- })
577
-
578
  # Update run record
579
  run.status = 'completed'
580
  run.added_leads = added_count
@@ -589,8 +477,7 @@ async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends
589
  return {
590
  "run_id": run_id,
591
  "campaign_id": campaign_id,
592
- "campaign_name": request.campaign_name or '',
593
- "steps_count": request.steps_count,
594
  "total": len(contacts),
595
  "added": added_count,
596
  "skipped": skipped_count,
 
283
  raise HTTPException(status_code=500, detail=f"Error downloading sequences: {str(e)}")
284
 
285
 
286
+ @app.get("/api/smartlead-campaigns")
287
+ async def get_smartlead_campaigns():
288
+ """Get list of campaigns from Smartlead"""
289
+ try:
290
+ client = SmartleadClient()
291
+ campaigns = client.get_campaigns()
292
+
293
+ # Format campaigns for frontend
294
+ formatted_campaigns = []
295
+ for campaign in campaigns:
296
+ campaign_id = campaign.get('campaign_id') or campaign.get('id') or campaign.get('campaignId')
297
+ campaign_name = campaign.get('name') or campaign.get('campaign_name') or campaign.get('title') or 'Unnamed Campaign'
298
+
299
+ if campaign_id:
300
+ formatted_campaigns.append({
301
+ "id": str(campaign_id),
302
+ "name": campaign_name
303
+ })
304
+
305
+ return {"campaigns": formatted_campaigns}
306
+ except Exception as e:
307
+ raise HTTPException(status_code=500, detail=f"Failed to fetch campaigns: {str(e)}")
308
+
309
+
310
  @app.post("/api/push-to-smartlead")
311
  async def push_to_smartlead(request: SmartleadPushRequest, db: Session = Depends(get_db)):
312
+ """Push generated sequences to Smartlead campaign (add leads to existing campaign)"""
313
  import uuid
314
 
315
  try:
 
338
  contacts[contact_key]['subjects'][seq.email_number] = seq.subject
339
  contacts[contact_key]['bodies'][seq.email_number] = seq.email_content
340
 
341
+ # Initialize Smartlead client
342
+ try:
343
+ client = SmartleadClient()
344
+ except ValueError as e:
345
+ raise HTTPException(status_code=400, detail=str(e))
346
+
347
+ # Get campaign name for logging
348
+ campaigns = client.get_campaigns()
349
+ campaign_name = 'Unknown Campaign'
350
+ for campaign in campaigns:
351
+ campaign_id = campaign.get('campaign_id') or campaign.get('id') or campaign.get('campaignId')
352
+ if str(campaign_id) == str(request.campaign_id):
353
+ campaign_name = campaign.get('name') or campaign.get('campaign_name') or campaign.get('title') or 'Unknown Campaign'
354
+ break
355
+
356
  # Create run record
357
  run_id = str(uuid.uuid4())
358
  run = SmartleadRun(
359
  run_id=run_id,
360
  file_id=request.file_id,
361
+ mode='existing', # Always 'existing' now
362
+ campaign_id=request.campaign_id,
363
+ campaign_name=campaign_name,
364
+ steps_count=0, # Not needed for existing campaigns
365
  dry_run=1 if request.dry_run else 0,
366
  total_leads=len(contacts),
367
  status='pending'
 
374
  return {
375
  "run_id": run_id,
376
  "campaign_id": request.campaign_id,
377
+ "campaign_name": campaign_name,
 
378
  "total": len(contacts),
379
  "added": 0,
380
  "skipped": 0,
 
384
  "message": "Dry run completed. No leads were sent to Smartlead."
385
  }
386
 
 
 
 
 
 
 
 
 
 
387
  campaign_id = request.campaign_id
388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  # Prepare leads for Smartlead
390
  leads = []
391
  errors = []
 
431
  continue
432
 
433
  # Build lead object - Smartlead API doesn't allow "company", "title", or custom variables when adding leads
434
+ # Custom variables should be set manually in Smartlead via CSV import
435
  lead = {
436
  "email": email,
437
  "first_name": first_name,
438
  "last_name": last_name
439
  }
440
 
441
+ leads.append(lead)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
  except Exception as e:
444
  errors.append({
 
447
  })
448
  failed_count += 1
449
 
450
+ # Add leads to campaign (chunk in batches of 50)
451
  batch_size = 50
 
 
452
  for i in range(0, len(leads), batch_size):
453
  batch = leads[i:i + batch_size]
 
 
454
  try:
455
+ response = client.add_leads_to_campaign(campaign_id, batch)
456
+ added_count += len(batch)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  except Exception as e:
458
  # Mark batch as failed
459
+ for lead in batch:
460
  errors.append({
461
+ "email": lead.get('email', 'unknown'),
462
  "error": f"Failed to add lead: {str(e)}"
463
  })
464
  failed_count += 1
465
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  # Update run record
467
  run.status = 'completed'
468
  run.added_leads = added_count
 
477
  return {
478
  "run_id": run_id,
479
  "campaign_id": campaign_id,
480
+ "campaign_name": campaign_name,
 
481
  "total": len(contacts),
482
  "added": added_count,
483
  "skipped": skipped_count,
backend/app/models.py CHANGED
@@ -29,10 +29,7 @@ class SequenceResponse(BaseModel):
29
 
30
  class SmartleadPushRequest(BaseModel):
31
  file_id: str
32
- mode: str # 'existing' or 'new'
33
- campaign_id: Optional[str] = None
34
- campaign_name: Optional[str] = None
35
- steps_count: int = 4
36
  dry_run: bool = False
37
 
38
 
 
29
 
30
  class SmartleadPushRequest(BaseModel):
31
  file_id: str
32
+ campaign_id: str # Required - campaign ID to add leads to
 
 
 
33
  dry_run: bool = False
34
 
35
 
backend/app/smartlead_client.py CHANGED
@@ -110,39 +110,30 @@ class SmartleadClient:
110
 
111
  raise Exception("Failed to make request after retries")
112
 
113
- def create_campaign(self, name: str) -> Dict:
114
- """Create a new campaign
115
 
116
- Smartlead API might expect different parameter names.
117
- Try common variations: "name", "campaign_name", "title"
118
  """
119
- # Try different parameter name variations
120
- variations = [
121
- {"name": name}, # Most common format
122
- {"campaign_name": name}, # Alternative
123
- {"title": name}, # Another alternative
124
- ]
125
-
126
- endpoints = ["/campaigns/create", "/campaigns"]
127
-
128
- last_error = None
129
- for endpoint in endpoints:
130
- for data in variations:
131
- try:
132
- return self._make_request("POST", endpoint, data)
133
- except Exception as e:
134
- last_error = e
135
- # If it's a 400 error about parameter name, try next variation
136
- if "400" in str(e) and ("not allowed" in str(e) or "invalid" in str(e).lower()):
137
- continue
138
- # If it's a 404, try next endpoint
139
- if "404" in str(e):
140
- break
141
- # For other errors, re-raise
142
- raise
143
-
144
- # If all variations failed, raise the last error
145
- raise last_error if last_error else Exception("Failed to create campaign: All parameter variations failed")
146
 
147
  def save_campaign_sequence(self, campaign_id: str, sequences: List[Dict]) -> Dict:
148
  """Save campaign sequence steps"""
 
110
 
111
  raise Exception("Failed to make request after retries")
112
 
113
+ def get_campaigns(self) -> List[Dict]:
114
+ """Get list of all campaigns from Smartlead
115
 
116
+ Returns list of campaigns with their IDs and names
 
117
  """
118
+ try:
119
+ response = self._make_request("GET", "/campaigns")
120
+
121
+ # Handle different response structures
122
+ if isinstance(response, list):
123
+ return response
124
+ elif isinstance(response, dict):
125
+ # Check for common response structures
126
+ campaigns = response.get('campaigns') or response.get('data') or response.get('results') or []
127
+ if isinstance(campaigns, list):
128
+ return campaigns
129
+ # If it's a dict with campaign info, wrap it
130
+ if response.get('campaign_id') or response.get('id'):
131
+ return [response]
132
+
133
+ return []
134
+ except Exception as e:
135
+ # If endpoint doesn't exist or fails, return empty list
136
+ return []
 
 
 
 
 
 
 
 
137
 
138
  def save_campaign_sequence(self, campaign_id: str, sequences: List[Dict]) -> Dict:
139
  """Save campaign sequence steps"""
frontend/src/components/smartlead/SmartleadPanel.jsx CHANGED
@@ -1,34 +1,49 @@
1
- import React, { useState } from 'react';
2
- import { Send, Download, Loader2, CheckCircle2, AlertCircle, Settings } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
4
- import { Input } from "@/components/ui/input";
5
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6
- import { Badge } from "@/components/ui/badge";
7
  import { motion } from 'framer-motion';
8
 
9
  export default function SmartleadPanel({ uploadedFile, sequencesCount, onPushComplete }) {
10
- const [mode, setMode] = useState('existing');
11
- const [campaignId, setCampaignId] = useState('');
12
- const [campaignName, setCampaignName] = useState('');
13
- const [stepsCount, setStepsCount] = useState(4);
14
  const [dryRun, setDryRun] = useState(false);
15
  const [isPushing, setIsPushing] = useState(false);
16
  const [pushResult, setPushResult] = useState(null);
17
  const [error, setError] = useState(null);
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  const handlePushToSmartlead = async () => {
20
  if (!uploadedFile?.fileId) {
21
  setError('No file uploaded');
22
  return;
23
  }
24
 
25
- if (mode === 'existing' && !campaignId.trim()) {
26
- setError('Campaign ID is required');
27
- return;
28
- }
29
-
30
- if (mode === 'new' && !campaignName.trim()) {
31
- setError('Campaign name is required');
32
  return;
33
  }
34
 
@@ -42,10 +57,7 @@ export default function SmartleadPanel({ uploadedFile, sequencesCount, onPushCom
42
  headers: { 'Content-Type': 'application/json' },
43
  body: JSON.stringify({
44
  file_id: uploadedFile.fileId,
45
- mode: mode,
46
- campaign_id: mode === 'existing' ? campaignId.trim() : null,
47
- campaign_name: mode === 'new' ? campaignName.trim() : null,
48
- steps_count: stepsCount,
49
  dry_run: dryRun
50
  })
51
  });
@@ -98,71 +110,51 @@ export default function SmartleadPanel({ uploadedFile, sequencesCount, onPushCom
98
  </div>
99
  </div>
100
 
101
- {/* Mode Selector */}
102
  <div className="mb-6">
103
- <label className="text-sm font-medium text-slate-700 mb-2 block">
104
- Campaign Mode
105
- </label>
106
- <Select value={mode} onValueChange={setMode}>
107
- <SelectTrigger className="w-full">
108
- <SelectValue />
109
- </SelectTrigger>
110
- <SelectContent>
111
- <SelectItem value="existing">Use Existing Campaign ID</SelectItem>
112
- <SelectItem value="new">Create New Campaign</SelectItem>
113
- </SelectContent>
114
- </Select>
115
- </div>
116
-
117
- {/* Campaign ID (Mode 1) */}
118
- {mode === 'existing' && (
119
- <div className="mb-4">
120
- <label className="text-sm font-medium text-slate-700 mb-2 block">
121
- Campaign ID
122
- </label>
123
- <Input
124
- value={campaignId}
125
- onChange={(e) => setCampaignId(e.target.value)}
126
- placeholder="Enter existing campaign ID"
127
- className="w-full"
128
- />
129
- </div>
130
- )}
131
-
132
- {/* Campaign Name (Mode 2) */}
133
- {mode === 'new' && (
134
- <div className="mb-4">
135
- <label className="text-sm font-medium text-slate-700 mb-2 block">
136
- Campaign Name
137
  </label>
138
- <Input
139
- value={campaignName}
140
- onChange={(e) => setCampaignName(e.target.value)}
141
- placeholder="Enter campaign name"
142
- className="w-full"
143
- />
 
 
 
 
 
144
  </div>
145
- )}
146
-
147
- {/* Steps Count */}
148
- <div className="mb-4">
149
- <label className="text-sm font-medium text-slate-700 mb-2 block">
150
- Steps to Use
151
- </label>
152
- <Select value={stepsCount.toString()} onValueChange={(v) => setStepsCount(parseInt(v))}>
153
- <SelectTrigger className="w-full">
154
- <SelectValue />
155
- </SelectTrigger>
156
- <SelectContent>
157
- {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(num => (
158
- <SelectItem key={num} value={num.toString()}>
159
- {num} {num === 1 ? 'step' : 'steps'}
160
- </SelectItem>
161
- ))}
162
- </SelectContent>
163
- </Select>
 
 
 
 
 
 
164
  <p className="text-xs text-slate-500 mt-1">
165
- Must align with generated sequences (max {sequencesCount || 10})
166
  </p>
167
  </div>
168
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Send, Download, Loader2, CheckCircle2, AlertCircle, Settings, RefreshCw } from 'lucide-react';
3
  import { Button } from "@/components/ui/button";
 
4
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 
5
  import { motion } from 'framer-motion';
6
 
7
  export default function SmartleadPanel({ uploadedFile, sequencesCount, onPushComplete }) {
8
+ const [selectedCampaignId, setSelectedCampaignId] = useState('');
9
+ const [campaigns, setCampaigns] = useState([]);
10
+ const [isLoadingCampaigns, setIsLoadingCampaigns] = useState(false);
 
11
  const [dryRun, setDryRun] = useState(false);
12
  const [isPushing, setIsPushing] = useState(false);
13
  const [pushResult, setPushResult] = useState(null);
14
  const [error, setError] = useState(null);
15
 
16
+ // Fetch campaigns on component mount
17
+ useEffect(() => {
18
+ fetchCampaigns();
19
+ }, []);
20
+
21
+ const fetchCampaigns = async () => {
22
+ setIsLoadingCampaigns(true);
23
+ setError(null);
24
+ try {
25
+ const response = await fetch('/api/smartlead-campaigns');
26
+ const data = await response.json();
27
+ if (response.ok) {
28
+ setCampaigns(data.campaigns || []);
29
+ } else {
30
+ setError('Failed to fetch campaigns: ' + (data.detail || 'Unknown error'));
31
+ }
32
+ } catch (err) {
33
+ setError('Network error: ' + err.message);
34
+ } finally {
35
+ setIsLoadingCampaigns(false);
36
+ }
37
+ };
38
+
39
  const handlePushToSmartlead = async () => {
40
  if (!uploadedFile?.fileId) {
41
  setError('No file uploaded');
42
  return;
43
  }
44
 
45
+ if (!selectedCampaignId) {
46
+ setError('Please select a campaign');
 
 
 
 
 
47
  return;
48
  }
49
 
 
57
  headers: { 'Content-Type': 'application/json' },
58
  body: JSON.stringify({
59
  file_id: uploadedFile.fileId,
60
+ campaign_id: selectedCampaignId,
 
 
 
61
  dry_run: dryRun
62
  })
63
  });
 
110
  </div>
111
  </div>
112
 
113
+ {/* Campaign Selector */}
114
  <div className="mb-6">
115
+ <div className="flex items-center justify-between mb-2">
116
+ <label className="text-sm font-medium text-slate-700">
117
+ Select Campaign
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </label>
119
+ <Button
120
+ type="button"
121
+ variant="ghost"
122
+ size="sm"
123
+ onClick={fetchCampaigns}
124
+ disabled={isLoadingCampaigns}
125
+ className="h-7 text-xs"
126
+ >
127
+ <RefreshCw className={`h-3 w-3 mr-1 ${isLoadingCampaigns ? 'animate-spin' : ''}`} />
128
+ Refresh
129
+ </Button>
130
  </div>
131
+ {isLoadingCampaigns ? (
132
+ <div className="flex items-center justify-center p-4 border border-slate-200 rounded-lg">
133
+ <Loader2 className="h-4 w-4 animate-spin text-slate-400 mr-2" />
134
+ <span className="text-sm text-slate-500">Loading campaigns...</span>
135
+ </div>
136
+ ) : campaigns.length === 0 ? (
137
+ <div className="p-4 border border-slate-200 rounded-lg bg-slate-50">
138
+ <p className="text-sm text-slate-600 text-center">
139
+ No campaigns found. Please create a campaign in Smartlead first.
140
+ </p>
141
+ </div>
142
+ ) : (
143
+ <Select value={selectedCampaignId} onValueChange={setSelectedCampaignId}>
144
+ <SelectTrigger className="w-full">
145
+ <SelectValue placeholder="Select a campaign" />
146
+ </SelectTrigger>
147
+ <SelectContent>
148
+ {campaigns.map((campaign) => (
149
+ <SelectItem key={campaign.id} value={campaign.id}>
150
+ {campaign.name} ({campaign.id})
151
+ </SelectItem>
152
+ ))}
153
+ </SelectContent>
154
+ </Select>
155
+ )}
156
  <p className="text-xs text-slate-500 mt-1">
157
+ Select an existing campaign to add leads to. Create campaigns manually in Smartlead using the CSV download.
158
  </p>
159
  </div>
160