Seth commited on
Commit ·
f485041
1
Parent(s): bde7250
update
Browse files- backend/app/main.py +53 -166
- backend/app/models.py +1 -4
- backend/app/smartlead_client.py +22 -31
- frontend/src/components/smartlead/SmartleadPanel.jsx +72 -80
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=
|
| 323 |
-
campaign_id=request.campaign_id
|
| 324 |
-
campaign_name=
|
| 325 |
-
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":
|
| 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 |
-
#
|
| 447 |
lead = {
|
| 448 |
"email": email,
|
| 449 |
"first_name": first_name,
|
| 450 |
"last_name": last_name
|
| 451 |
}
|
| 452 |
|
| 453 |
-
|
| 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 |
-
#
|
| 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,
|
| 491 |
-
added_count += len(
|
| 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
|
| 532 |
errors.append({
|
| 533 |
-
"email":
|
| 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":
|
| 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 |
-
|
| 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
|
| 114 |
-
"""
|
| 115 |
|
| 116 |
-
|
| 117 |
-
Try common variations: "name", "campaign_name", "title"
|
| 118 |
"""
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
return
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 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 [
|
| 11 |
-
const [
|
| 12 |
-
const [
|
| 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 (
|
| 26 |
-
setError('
|
| 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 |
-
|
| 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 |
-
{/*
|
| 102 |
<div className="mb-6">
|
| 103 |
-
<
|
| 104 |
-
|
| 105 |
-
|
| 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 |
-
<
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
</div>
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
</
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
<p className="text-xs text-slate-500 mt-1">
|
| 165 |
-
|
| 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 |
|