diff --git a/api/constants.py b/api/constants.py index b92596a..c45109e 100644 --- a/api/constants.py +++ b/api/constants.py @@ -92,3 +92,13 @@ COUNTRY_CODES = { "LU": "352", # Luxembourg "IE": "353", # Ireland } + +DEFAULT_ORG_CONCURRENCY_LIMIT = os.getenv("DEFAULT_ORG_CONCURRENCY_LIMIT", 2) +DEFAULT_CAMPAIGN_RETRY_CONFIG = { + "enabled": True, + "max_retries": 1, + "retry_delay_seconds": 120, + "retry_on_busy": True, + "retry_on_no_answer": True, + "retry_on_voicemail": False, +} diff --git a/api/db/campaign_client.py b/api/db/campaign_client.py index 0b8cff7..398c1ef 100644 --- a/api/db/campaign_client.py +++ b/api/db/campaign_client.py @@ -17,9 +17,16 @@ class CampaignClient(BaseDBClient): source_id: str, user_id: int, organization_id: int, + retry_config: Optional[dict] = None, + max_concurrency: Optional[int] = None, ) -> CampaignModel: """Create a new campaign""" async with self.async_session() as session: + # Build orchestrator_metadata with max_concurrency if provided + orchestrator_metadata = {} + if max_concurrency is not None: + orchestrator_metadata["max_concurrency"] = max_concurrency + campaign = CampaignModel( name=name, workflow_id=workflow_id, @@ -27,6 +34,10 @@ class CampaignClient(BaseDBClient): source_id=source_id, created_by=user_id, organization_id=organization_id, + retry_config=retry_config + if retry_config + else CampaignModel.retry_config.default.arg, + orchestrator_metadata=orchestrator_metadata, ) session.add(campaign) try: diff --git a/api/db/models.py b/api/db/models.py index 4f44636..3ba0ae7 100644 --- a/api/db/models.py +++ b/api/db/models.py @@ -22,6 +22,8 @@ from sqlalchemy import ( ) from sqlalchemy.orm import declarative_base, relationship +from api.constants import DEFAULT_CAMPAIGN_RETRY_CONFIG + from ..enums import ( CallType, IntegrationAction, @@ -537,14 +539,7 @@ class CampaignModel(Base): retry_config = Column( JSON, nullable=False, - default={ - "enabled": True, - "max_retries": 2, - "retry_delay_seconds": 120, - "retry_on_busy": True, - "retry_on_no_answer": True, - "retry_on_voicemail": True, - }, + default=DEFAULT_CAMPAIGN_RETRY_CONFIG, server_default=text( '\'{"enabled": true, "max_retries": 2, "retry_on_busy": true, "retry_on_no_answer": true, "retry_on_voicemail": true, "retry_delay_seconds": 120}\'::jsonb' ), diff --git a/api/routes/campaign.py b/api/routes/campaign.py index 34c0b4e..cd61169 100644 --- a/api/routes/campaign.py +++ b/api/routes/campaign.py @@ -4,22 +4,61 @@ from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field +from api.constants import DEFAULT_CAMPAIGN_RETRY_CONFIG, DEFAULT_ORG_CONCURRENCY_LIMIT from api.db import db_client from api.db.models import UserModel from api.enums import OrganizationConfigurationKey from api.services.auth.depends import get_user from api.services.campaign.runner import campaign_runner_service +from api.services.campaign.source_validator import ( + validate_csv_source, + validate_google_sheet_source, +) from api.services.quota_service import check_dograh_quota from api.services.storage import storage_fs router = APIRouter(prefix="/campaign") +async def _get_org_concurrent_limit(organization_id: int) -> int: + """Get the concurrent call limit for an organization.""" + try: + config = await db_client.get_configuration( + organization_id, + OrganizationConfigurationKey.CONCURRENT_CALL_LIMIT.value, + ) + if config and config.value: + return int(config.value.get("value", DEFAULT_ORG_CONCURRENCY_LIMIT)) + except Exception: + pass + return DEFAULT_ORG_CONCURRENCY_LIMIT + + +class RetryConfigRequest(BaseModel): + enabled: bool = True + max_retries: int = Field(default=2, ge=0, le=10) + retry_delay_seconds: int = Field(default=120, ge=30, le=3600) + retry_on_busy: bool = True + retry_on_no_answer: bool = True + retry_on_voicemail: bool = True + + +class RetryConfigResponse(BaseModel): + enabled: bool + max_retries: int + retry_delay_seconds: int + retry_on_busy: bool + retry_on_no_answer: bool + retry_on_voicemail: bool + + class CreateCampaignRequest(BaseModel): name: str = Field(..., min_length=1, max_length=255) workflow_id: int source_type: str = Field(..., pattern="^(google-sheet|csv)$") source_id: str # Google Sheet URL or CSV file key + retry_config: Optional[RetryConfigRequest] = None + max_concurrency: Optional[int] = Field(default=None, ge=1, le=100) class CampaignResponse(BaseModel): @@ -36,6 +75,8 @@ class CampaignResponse(BaseModel): created_at: datetime started_at: Optional[datetime] completed_at: Optional[datetime] + retry_config: RetryConfigResponse + max_concurrency: Optional[int] = None class CampaignsResponse(BaseModel): @@ -63,26 +104,23 @@ class CampaignProgressResponse(BaseModel): completed_at: Optional[datetime] -@router.post("/create") -async def create_campaign( - request: CreateCampaignRequest, - user: UserModel = Depends(get_user), -) -> CampaignResponse: - """Create a new campaign""" - # Verify workflow exists and belongs to organization - workflow_name = await db_client.get_workflow_name(request.workflow_id, user.id) - if not workflow_name: - raise HTTPException(status_code=404, detail="Workflow not found") +# Default retry config for campaigns - campaign = await db_client.create_campaign( - name=request.name, - workflow_id=request.workflow_id, - source_type=request.source_type, - source_id=request.source_id, - user_id=user.id, - organization_id=user.selected_organization_id, + +def _build_campaign_response(campaign, workflow_name: str) -> CampaignResponse: + """Build a CampaignResponse from a campaign model.""" + # Get retry_config from campaign or use defaults + retry_config = ( + campaign.retry_config + if campaign.retry_config + else DEFAULT_CAMPAIGN_RETRY_CONFIG ) + # Get max_concurrency from orchestrator_metadata + max_concurrency = None + if campaign.orchestrator_metadata: + max_concurrency = campaign.orchestrator_metadata.get("max_concurrency") + return CampaignResponse( id=campaign.id, name=campaign.name, @@ -97,9 +135,62 @@ async def create_campaign( created_at=campaign.created_at, started_at=campaign.started_at, completed_at=campaign.completed_at, + retry_config=RetryConfigResponse(**retry_config), + max_concurrency=max_concurrency, ) +@router.post("/create") +async def create_campaign( + request: CreateCampaignRequest, + user: UserModel = Depends(get_user), +) -> CampaignResponse: + """Create a new campaign""" + # Verify workflow exists and belongs to organization + workflow_name = await db_client.get_workflow_name(request.workflow_id, user.id) + if not workflow_name: + raise HTTPException(status_code=404, detail="Workflow not found") + + # Validate source data (phone_number column and format) + if request.source_type == "csv": + validation_result = await validate_csv_source(request.source_id) + if not validation_result.is_valid: + raise HTTPException(status_code=400, detail=validation_result.error.message) + elif request.source_type == "google-sheet": + validation_result = await validate_google_sheet_source( + request.source_id, user.selected_organization_id + ) + if not validation_result.is_valid: + raise HTTPException(status_code=400, detail=validation_result.error.message) + + # Validate max_concurrency against org limit if provided + if request.max_concurrency is not None: + org_limit = await _get_org_concurrent_limit(user.selected_organization_id) + if request.max_concurrency > org_limit: + raise HTTPException( + status_code=400, + detail=f"max_concurrency ({request.max_concurrency}) cannot exceed organization limit ({org_limit})", + ) + + # Build retry_config dict if provided + retry_config = None + if request.retry_config: + retry_config = request.retry_config.model_dump() + + campaign = await db_client.create_campaign( + name=request.name, + workflow_id=request.workflow_id, + source_type=request.source_type, + source_id=request.source_id, + user_id=user.id, + organization_id=user.selected_organization_id, + retry_config=retry_config, + max_concurrency=request.max_concurrency, + ) + + return _build_campaign_response(campaign, workflow_name) + + @router.get("/") async def get_campaigns( user: UserModel = Depends(get_user), @@ -115,21 +206,7 @@ async def get_campaigns( workflow_map = {w.id: w.name for w in workflows} campaign_responses = [ - CampaignResponse( - id=c.id, - name=c.name, - workflow_id=c.workflow_id, - workflow_name=workflow_map.get(c.workflow_id, "Unknown"), - state=c.state, - source_type=c.source_type, - source_id=c.source_id, - total_rows=c.total_rows, - processed_rows=c.processed_rows, - failed_rows=c.failed_rows, - created_at=c.created_at, - started_at=c.started_at, - completed_at=c.completed_at, - ) + _build_campaign_response(c, workflow_map.get(c.workflow_id, "Unknown")) for c in campaigns ] @@ -148,21 +225,7 @@ async def get_campaign( workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) - return CampaignResponse( - id=campaign.id, - name=campaign.name, - workflow_id=campaign.workflow_id, - workflow_name=workflow_name or "Unknown", - state=campaign.state, - source_type=campaign.source_type, - source_id=campaign.source_id, - total_rows=campaign.total_rows, - processed_rows=campaign.processed_rows, - failed_rows=campaign.failed_rows, - created_at=campaign.created_at, - started_at=campaign.started_at, - completed_at=campaign.completed_at, - ) + return _build_campaign_response(campaign, workflow_name or "Unknown") @router.post("/{campaign_id}/start") @@ -203,21 +266,7 @@ async def start_campaign( campaign = await db_client.get_campaign(campaign_id, user.selected_organization_id) workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) - return CampaignResponse( - id=campaign.id, - name=campaign.name, - workflow_id=campaign.workflow_id, - workflow_name=workflow_name or "Unknown", - state=campaign.state, - source_type=campaign.source_type, - source_id=campaign.source_id, - total_rows=campaign.total_rows, - processed_rows=campaign.processed_rows, - failed_rows=campaign.failed_rows, - created_at=campaign.created_at, - started_at=campaign.started_at, - completed_at=campaign.completed_at, - ) + return _build_campaign_response(campaign, workflow_name or "Unknown") @router.post("/{campaign_id}/pause") @@ -241,21 +290,7 @@ async def pause_campaign( campaign = await db_client.get_campaign(campaign_id, user.selected_organization_id) workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) - return CampaignResponse( - id=campaign.id, - name=campaign.name, - workflow_id=campaign.workflow_id, - workflow_name=workflow_name or "Unknown", - state=campaign.state, - source_type=campaign.source_type, - source_id=campaign.source_id, - total_rows=campaign.total_rows, - processed_rows=campaign.processed_rows, - failed_rows=campaign.failed_rows, - created_at=campaign.created_at, - started_at=campaign.started_at, - completed_at=campaign.completed_at, - ) + return _build_campaign_response(campaign, workflow_name or "Unknown") @router.get("/{campaign_id}/runs") @@ -316,21 +351,7 @@ async def resume_campaign( campaign = await db_client.get_campaign(campaign_id, user.selected_organization_id) workflow_name = await db_client.get_workflow_name(campaign.workflow_id, user.id) - return CampaignResponse( - id=campaign.id, - name=campaign.name, - workflow_id=campaign.workflow_id, - workflow_name=workflow_name or "Unknown", - state=campaign.state, - source_type=campaign.source_type, - source_id=campaign.source_id, - total_rows=campaign.total_rows, - processed_rows=campaign.processed_rows, - failed_rows=campaign.failed_rows, - created_at=campaign.created_at, - started_at=campaign.started_at, - completed_at=campaign.completed_at, - ) + return _build_campaign_response(campaign, workflow_name or "Unknown") @router.get("/{campaign_id}/progress") diff --git a/api/routes/organization.py b/api/routes/organization.py index 1a14b8a..f14e5e2 100644 --- a/api/routes/organization.py +++ b/api/routes/organization.py @@ -1,7 +1,9 @@ from typing import Union from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from api.constants import DEFAULT_CAMPAIGN_RETRY_CONFIG, DEFAULT_ORG_CONCURRENCY_LIMIT from api.db import db_client from api.db.models import UserModel from api.enums import OrganizationConfigurationKey @@ -210,3 +212,46 @@ def preserve_masked_fields(request, existing_config, config_value): field_value, existing_config.value.get(field_name, "") ): config_value[field_name] = existing_config.value[field_name] + + +class RetryConfigResponse(BaseModel): + enabled: bool + max_retries: int + retry_delay_seconds: int + retry_on_busy: bool + retry_on_no_answer: bool + retry_on_voicemail: bool + + +class CampaignLimitsResponse(BaseModel): + concurrent_call_limit: int + default_retry_config: RetryConfigResponse + + +@router.get("/campaign-limits", response_model=CampaignLimitsResponse) +async def get_campaign_limits(user: UserModel = Depends(get_user)): + """Get campaign limits for the user's organization. + + Returns the organization's concurrent call limit and default retry configuration. + """ + if not user.selected_organization_id: + raise HTTPException(status_code=400, detail="No organization selected") + + # Get concurrent call limit + concurrent_limit = DEFAULT_ORG_CONCURRENCY_LIMIT + try: + config = await db_client.get_configuration( + user.selected_organization_id, + OrganizationConfigurationKey.CONCURRENT_CALL_LIMIT.value, + ) + if config and config.value: + concurrent_limit = int( + config.value.get("value", DEFAULT_ORG_CONCURRENCY_LIMIT) + ) + except Exception: + pass + + return CampaignLimitsResponse( + concurrent_call_limit=concurrent_limit, + default_retry_config=RetryConfigResponse(**DEFAULT_CAMPAIGN_RETRY_CONFIG), + ) diff --git a/api/services/campaign/call_dispatcher.py b/api/services/campaign/call_dispatcher.py index ec85c8d..5d3c38f 100644 --- a/api/services/campaign/call_dispatcher.py +++ b/api/services/campaign/call_dispatcher.py @@ -5,6 +5,7 @@ from typing import Optional from loguru import logger +from api.constants import DEFAULT_ORG_CONCURRENCY_LIMIT from api.db import db_client from api.db.models import QueuedRunModel, WorkflowRunModel from api.enums import OrganizationConfigurationKey, WorkflowRunState @@ -18,7 +19,7 @@ class CampaignCallDispatcher: """Manages rate-limited and concurrent-limited call dispatching""" def __init__(self): - self.default_concurrent_limit = 20 + self.default_concurrent_limit = int(DEFAULT_ORG_CONCURRENCY_LIMIT) async def get_telephony_provider(self, organization_id: int) -> TelephonyProvider: """Get telephony provider instance for specific organization""" @@ -132,7 +133,22 @@ class CampaignCallDispatcher: ) -> Optional[WorkflowRunModel]: """Creates workflow run and initiates call with concurrent limiting""" # Get concurrent limit for organization - max_concurrent = await self.get_org_concurrent_limit(campaign.organization_id) + org_concurrent_limit = await self.get_org_concurrent_limit( + campaign.organization_id + ) + + # Check for campaign-level max_concurrency in orchestrator_metadata + campaign_max_concurrency = None + if campaign.orchestrator_metadata: + campaign_max_concurrency = campaign.orchestrator_metadata.get( + "max_concurrency" + ) + + # Use the lower of campaign limit and org limit + if campaign_max_concurrency is not None: + max_concurrent = min(campaign_max_concurrency, org_concurrent_limit) + else: + max_concurrent = org_concurrent_limit # Track wait time for alerting wait_start = time.time() diff --git a/api/services/campaign/source_validator.py b/api/services/campaign/source_validator.py new file mode 100644 index 0000000..b622808 --- /dev/null +++ b/api/services/campaign/source_validator.py @@ -0,0 +1,265 @@ +""" +Source validation for campaign data sources (CSV, Google Sheets). + +Validates that: +- phone_number column exists +- All phone numbers include country code (start with '+') +""" + +import csv +from dataclasses import dataclass +from io import StringIO +from typing import List, Optional + +import httpx +from loguru import logger + +from api.services.storage import storage_fs + + +@dataclass +class ValidationError: + """Represents a validation error with details.""" + + message: str + invalid_rows: Optional[List[int]] = None + + +@dataclass +class ValidationResult: + """Result of source validation.""" + + is_valid: bool + error: Optional[ValidationError] = None + + +def _validate_source_data( + headers: List[str], rows: List[List[str]] +) -> ValidationResult: + """ + Validate source data for campaign creation. + + Args: + headers: List of column headers + rows: List of data rows (excluding header) + + Returns: + ValidationResult with is_valid=True if valid, or error details if invalid + """ + # Normalize headers to lowercase for comparison + normalized_headers = [h.strip().lower() for h in headers] + + # Check for phone_number column + if "phone_number" not in normalized_headers: + return ValidationResult( + is_valid=False, + error=ValidationError( + message="Source must contain a 'phone_number' column" + ), + ) + + phone_number_idx = normalized_headers.index("phone_number") + + # Validate phone numbers in all data rows + invalid_rows = [] + for row_idx, row in enumerate(rows, start=2): # Start at 2 (1-indexed, skip header) + if len(row) <= phone_number_idx: + continue # Skip rows that don't have enough columns + + phone_number = row[phone_number_idx].strip() + if phone_number and not phone_number.startswith("+"): + invalid_rows.append(row_idx) + + if invalid_rows: + # Limit the number of rows shown in error message + if len(invalid_rows) > 5: + rows_str = f"{', '.join(map(str, invalid_rows[:5]))} and {len(invalid_rows) - 5} more" + else: + rows_str = ", ".join(map(str, invalid_rows)) + + return ValidationResult( + is_valid=False, + error=ValidationError( + message=f"Invalid phone numbers in rows: {rows_str}. All phone numbers must include country code (start with '+')", + invalid_rows=invalid_rows, + ), + ) + + return ValidationResult(is_valid=True) + + +async def validate_csv_source(file_key: str) -> ValidationResult: + """ + Validate a CSV source file for campaign creation. + + Args: + file_key: S3/MinIO file key for the CSV file + + Returns: + ValidationResult with is_valid=True if valid, or error details if invalid + """ + # Get download URL using internal endpoint + signed_url = await storage_fs.aget_signed_url( + file_key, expiration=3600, use_internal_endpoint=True + ) + + if not signed_url: + return ValidationResult( + is_valid=False, + error=ValidationError(message=f"Failed to access CSV file: {file_key}"), + ) + + # Download CSV file + async with httpx.AsyncClient() as client: + try: + response = await client.get(signed_url) + response.raise_for_status() + csv_content = response.text + except httpx.HTTPError as e: + logger.error(f"Failed to download CSV file for validation: {e}") + return ValidationResult( + is_valid=False, + error=ValidationError( + message="Failed to download CSV file for validation" + ), + ) + + # Parse CSV + try: + csv_file = StringIO(csv_content) + reader = csv.reader(csv_file) + rows = list(reader) + except Exception as e: + logger.error(f"Failed to parse CSV: {e}") + return ValidationResult( + is_valid=False, + error=ValidationError(message=f"Invalid CSV format: {str(e)}"), + ) + + if not rows or len(rows) < 2: + return ValidationResult( + is_valid=False, + error=ValidationError( + message="CSV file must have a header row and at least one data row" + ), + ) + + headers = rows[0] + data_rows = rows[1:] + + return _validate_source_data(headers, data_rows) + + +async def validate_google_sheet_source( + sheet_url: str, organization_id: int +) -> ValidationResult: + """ + Validate a Google Sheet source for campaign creation. + + Args: + sheet_url: Google Sheets URL + organization_id: Organization ID to get integration credentials + + Returns: + ValidationResult with is_valid=True if valid, or error details if invalid + """ + import re + + from api.db import db_client + from api.services.integrations.nango import NangoService + + # Extract sheet ID from URL + pattern = r"/spreadsheets/d/([a-zA-Z0-9-_]+)" + match = re.search(pattern, sheet_url) + if not match: + return ValidationResult( + is_valid=False, + error=ValidationError(message=f"Invalid Google Sheets URL: {sheet_url}"), + ) + + sheet_id = match.group(1) + + # Get Google Sheets integration for the organization + integrations = await db_client.get_integrations_by_organization_id(organization_id) + integration = None + for intg in integrations: + if intg.provider == "google-sheet" and intg.is_active: + integration = intg + break + + if not integration: + return ValidationResult( + is_valid=False, + error=ValidationError( + message="Google Sheets integration not found or inactive" + ), + ) + + # Get OAuth token via Nango + try: + nango_service = NangoService() + token_data = await nango_service.get_access_token( + connection_id=integration.integration_id, provider_config_key="google-sheet" + ) + access_token = token_data["credentials"]["access_token"] + except Exception as e: + logger.error(f"Failed to get Google Sheets access token: {e}") + return ValidationResult( + is_valid=False, + error=ValidationError(message="Failed to authenticate with Google Sheets"), + ) + + # Fetch sheet data + sheets_api_base = "https://sheets.googleapis.com/v4/spreadsheets" + + async with httpx.AsyncClient() as client: + try: + # Get sheet metadata to find the first sheet name + metadata_url = f"{sheets_api_base}/{sheet_id}" + headers = {"Authorization": f"Bearer {access_token}"} + response = await client.get(metadata_url, headers=headers) + response.raise_for_status() + metadata = response.json() + + if not metadata.get("sheets"): + return ValidationResult( + is_valid=False, + error=ValidationError(message="No sheets found in the spreadsheet"), + ) + + sheet_name = metadata["sheets"][0]["properties"]["title"] + + # Fetch all data from sheet + data_url = f"{sheets_api_base}/{sheet_id}/values/{sheet_name}!A:Z" + response = await client.get(data_url, headers=headers) + response.raise_for_status() + data = response.json() + rows = data.get("values", []) + + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error fetching Google Sheet: {e.response.status_code}") + return ValidationResult( + is_valid=False, + error=ValidationError( + message=f"Failed to fetch Google Sheet data: {e.response.status_code}" + ), + ) + except Exception as e: + logger.error(f"Error fetching Google Sheet: {e}") + return ValidationResult( + is_valid=False, + error=ValidationError(message="Failed to fetch Google Sheet data"), + ) + + if not rows or len(rows) < 2: + return ValidationResult( + is_valid=False, + error=ValidationError( + message="Google Sheet must have a header row and at least one data row" + ), + ) + + headers = rows[0] + data_rows = rows[1:] + + return _validate_source_data(headers, data_rows) diff --git a/ui/src/app/campaigns/CsvUploadSelector.tsx b/ui/src/app/campaigns/CsvUploadSelector.tsx index 34a1a75..d829eb1 100644 --- a/ui/src/app/campaigns/CsvUploadSelector.tsx +++ b/ui/src/app/campaigns/CsvUploadSelector.tsx @@ -132,7 +132,9 @@ export default function CsvUploadSelector({ accessToken, onFileUploaded, selecte )}

- Upload a CSV file with contact data. Must include phone_number, first_name, and last_name columns. Max 10MB. + Upload a CSV file with contact data. Must include phone_number column. + The columns can be accessed as initial_context in the workflow nodes.
+ Max 10MB.

); diff --git a/ui/src/app/campaigns/[campaignId]/page.tsx b/ui/src/app/campaigns/[campaignId]/page.tsx index 550d564..fc32258 100644 --- a/ui/src/app/campaigns/[campaignId]/page.tsx +++ b/ui/src/app/campaigns/[campaignId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { ArrowLeft, Pause, Play, RefreshCw } from 'lucide-react'; +import { ArrowLeft, Check, Pause, Play, RefreshCw, X } from 'lucide-react'; import { useParams, useRouter } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; @@ -16,6 +16,7 @@ import type { CampaignResponse, WorkflowRunResponse } from '@/client/types.gen'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; import { Table, TableBody, @@ -425,6 +426,76 @@ export default function CampaignDetailPage() { + {/* Campaign Settings */} + + + Campaign Settings + + Concurrency and retry configuration + + + + {/* Concurrency Setting */} +
+
Max Concurrent Calls
+
+ {campaign.max_concurrency ? ( + {campaign.max_concurrency} + ) : ( + Using organization default + )} +
+
+ + + + {/* Retry Configuration */} +
+
+ Retries Enabled + {campaign.retry_config.enabled ? ( + + + Enabled + + ) : ( + + + Disabled + + )} +
+ + {campaign.retry_config.enabled && ( +
+
+
Max Retries
+
{campaign.retry_config.max_retries}
+
+
+
Retry Delay
+
{campaign.retry_config.retry_delay_seconds}s
+
+
+
Retry On
+
+ {campaign.retry_config.retry_on_busy && ( + Busy + )} + {campaign.retry_config.retry_on_no_answer && ( + No Answer + )} + {campaign.retry_config.retry_on_voicemail && ( + Voicemail + )} +
+
+
+ )} +
+
+
+ {/* Workflow Runs */} diff --git a/ui/src/app/campaigns/new/page.tsx b/ui/src/app/campaigns/new/page.tsx index d364d94..315f8c8 100644 --- a/ui/src/app/campaigns/new/page.tsx +++ b/ui/src/app/campaigns/new/page.tsx @@ -1,14 +1,19 @@ "use client"; -import { ArrowLeft } from 'lucide-react'; +import { ArrowLeft, ChevronDown, ChevronRight } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; import { toast } from 'sonner'; -import { createCampaignApiV1CampaignCreatePost, getWorkflowsSummaryApiV1WorkflowSummaryGet } from '@/client/sdk.gen'; +import { + createCampaignApiV1CampaignCreatePost, + getCampaignLimitsApiV1OrganizationsCampaignLimitsGet, + getWorkflowsSummaryApiV1WorkflowSummaryGet +} from '@/client/sdk.gen'; import type { WorkflowSummaryResponse } from '@/client/types.gen'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { @@ -18,6 +23,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select'; +import { Switch } from '@/components/ui/switch'; import { useAuth } from '@/lib/auth'; import CsvUploadSelector from '../CsvUploadSelector'; @@ -34,12 +40,25 @@ export default function NewCampaignPage() { const [sourceId, setSourceId] = useState(''); const [selectedFileName, setSelectedFileName] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); + const [createError, setCreateError] = useState(null); const [userAccessToken, setUserAccessToken] = useState(''); // Workflows state const [workflows, setWorkflows] = useState([]); const [isLoadingWorkflows, setIsLoadingWorkflows] = useState(true); + // Advanced settings state + const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + const [orgConcurrentLimit, setOrgConcurrentLimit] = useState(2); + const [maxConcurrency, setMaxConcurrency] = useState(''); + // Retry config state + const [retryEnabled, setRetryEnabled] = useState(true); + const [maxRetries, setMaxRetries] = useState('2'); + const [retryDelaySeconds, setRetryDelaySeconds] = useState('120'); + const [retryOnBusy, setRetryOnBusy] = useState(true); + const [retryOnNoAnswer, setRetryOnNoAnswer] = useState(true); + const [retryOnVoicemail, setRetryOnVoicemail] = useState(true); + // Redirect if not authenticated useEffect(() => { if (!loading && !user) { @@ -70,45 +89,111 @@ export default function NewCampaignPage() { } }, [user, getAccessToken]); - // Initial load - useEffect(() => { - if (user) { - fetchWorkflows(); - } - }, [fetchWorkflows, user]); - - // Handle form submission - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!campaignName || !selectedWorkflowId || !sourceId) { - toast.error('Please fill in all fields'); - return; - } - - setIsSubmitting(true); - + // Fetch campaign limits + const fetchCampaignLimits = useCallback(async () => { + if (!user) return; try { const accessToken = await getAccessToken(); - const response = await createCampaignApiV1CampaignCreatePost({ - body: { - name: campaignName, - workflow_id: parseInt(selectedWorkflowId), - source_type: sourceType, - source_id: sourceId, - }, + const response = await getCampaignLimitsApiV1OrganizationsCampaignLimitsGet({ headers: { 'Authorization': `Bearer ${accessToken}`, } }); + if (response.data) { + setOrgConcurrentLimit(response.data.concurrent_call_limit); + // Initialize retry config from defaults + const retryConfig = response.data.default_retry_config; + setRetryEnabled(retryConfig.enabled); + setMaxRetries(String(retryConfig.max_retries)); + setRetryDelaySeconds(String(retryConfig.retry_delay_seconds)); + setRetryOnBusy(retryConfig.retry_on_busy); + setRetryOnNoAnswer(retryConfig.retry_on_no_answer); + setRetryOnVoicemail(retryConfig.retry_on_voicemail); + } + } catch (error) { + console.error('Failed to fetch campaign limits:', error); + } + }, [user, getAccessToken]); + + // Initial load + useEffect(() => { + if (user) { + fetchWorkflows(); + fetchCampaignLimits(); + } + }, [fetchWorkflows, fetchCampaignLimits, user]); + + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setCreateError(null); + + if (!campaignName || !selectedWorkflowId || !sourceId) { + toast.error('Please fill in all fields'); + return; + } + + // Validate max_concurrency if provided + const maxConcurrencyValue = maxConcurrency ? parseInt(maxConcurrency) : null; + if (maxConcurrencyValue !== null) { + if (isNaN(maxConcurrencyValue) || maxConcurrencyValue < 1 || maxConcurrencyValue > 100) { + toast.error('Max concurrent calls must be between 1 and 100'); + return; + } + if (maxConcurrencyValue > orgConcurrentLimit) { + toast.error(`Max concurrent calls cannot exceed organization limit (${orgConcurrentLimit})`); + return; + } + } + + setIsSubmitting(true); + + try { + const accessToken = await getAccessToken(); + + // Build retry_config only if user has modified settings from defaults + const retryConfig = { + enabled: retryEnabled, + max_retries: parseInt(maxRetries) || 2, + retry_delay_seconds: parseInt(retryDelaySeconds) || 120, + retry_on_busy: retryOnBusy, + retry_on_no_answer: retryOnNoAnswer, + retry_on_voicemail: retryOnVoicemail, + }; + + const response = await createCampaignApiV1CampaignCreatePost({ + body: { + name: campaignName, + workflow_id: parseInt(selectedWorkflowId), + source_type: sourceType, + source_id: sourceId, + retry_config: retryConfig, + max_concurrency: maxConcurrencyValue, + }, + headers: { + 'Authorization': `Bearer ${accessToken}`, + } + }); + + if (response.error) { + // Extract error message from API response + const errorDetail = (response.error as { detail?: string })?.detail; + const errorMessage = errorDetail || 'Failed to create campaign'; + setCreateError(errorMessage); + toast.error(errorMessage); + return; + } + if (response.data) { toast.success('Campaign created successfully'); router.push(`/campaigns/${response.data.id}`); } - } catch (error) { + } catch (error: unknown) { console.error('Failed to create campaign:', error); - toast.error('Failed to create campaign'); + const errorMessage = 'Failed to create campaign'; + setCreateError(errorMessage); + toast.error(errorMessage); } finally { setIsSubmitting(false); } @@ -131,7 +216,7 @@ export default function NewCampaignPage() { }; return ( -
+
- - - {errors.from_number && ( + + {fromNumbers.map((number, index) => ( +
+ updatePhoneNumber(index, e.target.value)} + /> + {fromNumbers.length > 1 && ( + + )} +
+ ))} + + {fromNumbers.some(n => n.trim() !== "" && !/^\+[1-9]\d{1,14}$/.test(n)) && (

- {errors.from_number.message} + Enter valid phone numbers with country code (e.g., +1234567890)

)}
@@ -497,23 +558,39 @@ export default function ConfigureTelephonyPage() {
- - - {errors.from_number && ( + + {fromNumbers.map((number, index) => ( +
+ updatePhoneNumber(index, e.target.value)} + /> + {fromNumbers.length > 1 && ( + + )} +
+ ))} + + {fromNumbers.some(n => n.trim() !== "" && !/^[1-9]\d{1,14}$/.test(n)) && (

- {errors.from_number.message} + Enter valid phone numbers without + prefix (e.g., 14155551234)

)}
@@ -564,23 +641,39 @@ export default function ConfigureTelephonyPage() {
- - - {errors.from_number && ( + + {fromNumbers.map((number, index) => ( +
+ updatePhoneNumber(index, e.target.value)} + /> + {fromNumbers.length > 1 && ( + + )} +
+ ))} + + {fromNumbers.some(n => n.trim() !== "" && !/^[1-9]\d{1,14}$/.test(n)) && (

- {errors.from_number.message} + Enter valid phone numbers without + prefix (e.g., 14155551234)

)}
@@ -635,24 +728,39 @@ export default function ConfigureTelephonyPage() {
- - - {errors.from_number && ( + + {fromNumbers.map((number, index) => ( +
+ updatePhoneNumber(index, e.target.value)} + /> + {fromNumbers.length > 1 && ( + + )} +
+ ))} + + {fromNumbers.some(n => n.trim() !== "" && !/^\+?[1-9]\d{1,14}$/.test(n)) && (

- {errors.from_number.message} + Enter valid phone numbers (e.g., +1234567890)

)}

diff --git a/ui/src/client/sdk.gen.ts b/ui/src/client/sdk.gen.ts index 57a9786..7d3c1b1 100644 --- a/ui/src/client/sdk.gen.ts +++ b/ui/src/client/sdk.gen.ts @@ -3,7 +3,7 @@ import type { Client,Options as ClientOptions, TDataShape } from '@hey-api/client-fetch'; import { client as _heyApiClient } from './client.gen'; -import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; +import type { ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteData, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteError, ArchiveApiKeyApiV1UserApiKeysApiKeyIdDeleteResponse, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteData, ArchiveServiceKeyApiV1UserServiceKeysServiceKeyIdDeleteError, CreateApiKeyApiV1UserApiKeysPostData, CreateApiKeyApiV1UserApiKeysPostError, CreateApiKeyApiV1UserApiKeysPostResponse, CreateCampaignApiV1CampaignCreatePostData, CreateCampaignApiV1CampaignCreatePostError, CreateCampaignApiV1CampaignCreatePostResponse, CreateCredentialApiV1CredentialsPostData, CreateCredentialApiV1CredentialsPostError, CreateCredentialApiV1CredentialsPostResponse, CreateLoadTestApiV1LooptalkLoadTestsPostData, CreateLoadTestApiV1LooptalkLoadTestsPostError, CreateLoadTestApiV1LooptalkLoadTestsPostResponse, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostData, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostError, CreateOrUpdateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenPostResponse, CreateServiceKeyApiV1UserServiceKeysPostData, CreateServiceKeyApiV1UserServiceKeysPostError, CreateServiceKeyApiV1UserServiceKeysPostResponse, CreateSessionApiV1IntegrationSessionPostData, CreateSessionApiV1IntegrationSessionPostError, CreateSessionApiV1IntegrationSessionPostResponse, CreateTestSessionApiV1LooptalkTestSessionsPostData, CreateTestSessionApiV1LooptalkTestSessionsPostError, CreateTestSessionApiV1LooptalkTestSessionsPostResponse, CreateToolApiV1ToolsPostData, CreateToolApiV1ToolsPostError, CreateToolApiV1ToolsPostResponse, CreateWorkflowApiV1WorkflowCreateDefinitionPostData, CreateWorkflowApiV1WorkflowCreateDefinitionPostError, CreateWorkflowApiV1WorkflowCreateDefinitionPostResponse, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostData, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostError, CreateWorkflowFromTemplateApiV1WorkflowCreateTemplatePostResponse, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostData, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostError, CreateWorkflowRunApiV1WorkflowWorkflowIdRunsPostResponse, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteData, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteError, DeactivateEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenDeleteResponse, DeleteCredentialApiV1CredentialsCredentialUuidDeleteData, DeleteCredentialApiV1CredentialsCredentialUuidDeleteError, DeleteCredentialApiV1CredentialsCredentialUuidDeleteResponse, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteData, DeleteDocumentApiV1KnowledgeBaseDocumentsDocumentUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteData, DeleteToolApiV1ToolsToolUuidDeleteError, DeleteToolApiV1ToolsToolUuidDeleteResponse, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetData, DownloadWorkflowArtifactApiV1PublicDownloadWorkflowTokenArtifactTypeGetError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostData, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostError, DuplicateWorkflowTemplateApiV1WorkflowTemplatesDuplicatePostResponse, GetActiveTestsApiV1LooptalkActiveTestsGetData, GetActiveTestsApiV1LooptalkActiveTestsGetError, GetApiKeysApiV1UserApiKeysGetData, GetApiKeysApiV1UserApiKeysGetError, GetApiKeysApiV1UserApiKeysGetResponse, GetAuthUserApiV1UserAuthUserGetData, GetAuthUserApiV1UserAuthUserGetError, GetAuthUserApiV1UserAuthUserGetResponse, GetCampaignApiV1CampaignCampaignIdGetData, GetCampaignApiV1CampaignCampaignIdGetError, GetCampaignApiV1CampaignCampaignIdGetResponse, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError, GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse, GetCampaignProgressApiV1CampaignCampaignIdProgressGetData, GetCampaignProgressApiV1CampaignCampaignIdProgressGetError, GetCampaignProgressApiV1CampaignCampaignIdProgressGetResponse, GetCampaignRunsApiV1CampaignCampaignIdRunsGetData, GetCampaignRunsApiV1CampaignCampaignIdRunsGetError, GetCampaignRunsApiV1CampaignCampaignIdRunsGetResponse, GetCampaignsApiV1CampaignGetData, GetCampaignsApiV1CampaignGetError, GetCampaignsApiV1CampaignGetResponse, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetData, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetError, GetCampaignSourceDownloadUrlApiV1CampaignCampaignIdSourceDownloadUrlGetResponse, GetCredentialApiV1CredentialsCredentialUuidGetData, GetCredentialApiV1CredentialsCredentialUuidGetError, GetCredentialApiV1CredentialsCredentialUuidGetResponse, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetData, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetError, GetCurrentPeriodUsageApiV1OrganizationsUsageCurrentPeriodGetResponse, GetDailyReportApiV1OrganizationsReportsDailyGetData, GetDailyReportApiV1OrganizationsReportsDailyGetError, GetDailyReportApiV1OrganizationsReportsDailyGetResponse, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetData, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetError, GetDailyRunsDetailApiV1OrganizationsReportsDailyRunsGetResponse, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetData, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetError, GetDailyUsageBreakdownApiV1OrganizationsUsageDailyBreakdownGetResponse, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetData, GetDefaultConfigurationsApiV1UserConfigurationsDefaultsGetResponse, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetData, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetError, GetDocumentApiV1KnowledgeBaseDocumentsDocumentUuidGetResponse, GetEmbedConfigApiV1PublicEmbedConfigTokenGetData, GetEmbedConfigApiV1PublicEmbedConfigTokenGetError, GetEmbedConfigApiV1PublicEmbedConfigTokenGetResponse, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetData, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetError, GetEmbedTokenApiV1WorkflowWorkflowIdEmbedTokenGetResponse, GetFileMetadataApiV1S3FileMetadataGetData, GetFileMetadataApiV1S3FileMetadataGetError, GetFileMetadataApiV1S3FileMetadataGetResponse, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetData, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetError, GetIntegrationAccessTokenApiV1IntegrationIntegrationIdAccessTokenGetResponse, GetIntegrationsApiV1IntegrationGetData, GetIntegrationsApiV1IntegrationGetError, GetIntegrationsApiV1IntegrationGetResponse, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetData, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetError, GetLoadTestStatsApiV1LooptalkLoadTestsLoadTestGroupIdStatsGetResponse, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostData, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostError, GetPresignedUploadUrlApiV1S3PresignedUploadUrlPostResponse, GetServiceKeysApiV1UserServiceKeysGetData, GetServiceKeysApiV1UserServiceKeysGetError, GetServiceKeysApiV1UserServiceKeysGetResponse, GetSignedUrlApiV1S3SignedUrlGetData, GetSignedUrlApiV1S3SignedUrlGetError, GetSignedUrlApiV1S3SignedUrlGetResponse, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetData, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetError, GetTelephonyConfigurationApiV1OrganizationsTelephonyConfigGetResponse, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetData, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetError, GetTestSessionApiV1LooptalkTestSessionsTestSessionIdGetResponse, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetData, GetTestSessionConversationApiV1LooptalkTestSessionsTestSessionIdConversationGetError, GetToolApiV1ToolsToolUuidGetData, GetToolApiV1ToolsToolUuidGetError, GetToolApiV1ToolsToolUuidGetResponse, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostData, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostError, GetUploadUrlApiV1KnowledgeBaseUploadUrlPostResponse, GetUsageHistoryApiV1OrganizationsUsageRunsGetData, GetUsageHistoryApiV1OrganizationsUsageRunsGetError, GetUsageHistoryApiV1OrganizationsUsageRunsGetResponse, GetUserConfigurationsApiV1UserConfigurationsUserGetData, GetUserConfigurationsApiV1UserConfigurationsUserGetError, GetUserConfigurationsApiV1UserConfigurationsUserGetResponse, GetVoicesApiV1UserConfigurationsVoicesProviderGetData, GetVoicesApiV1UserConfigurationsVoicesProviderGetError, GetVoicesApiV1UserConfigurationsVoicesProviderGetResponse, GetWorkflowApiV1WorkflowFetchWorkflowIdGetData, GetWorkflowApiV1WorkflowFetchWorkflowIdGetError, GetWorkflowApiV1WorkflowFetchWorkflowIdGetResponse, GetWorkflowCountApiV1WorkflowCountGetData, GetWorkflowCountApiV1WorkflowCountGetError, GetWorkflowCountApiV1WorkflowCountGetResponse, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetData, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetError, GetWorkflowOptionsApiV1OrganizationsReportsWorkflowsGetResponse, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetData, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetError, GetWorkflowRunApiV1WorkflowWorkflowIdRunsRunIdGetResponse, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetData, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetError, GetWorkflowRunsApiV1SuperuserWorkflowRunsGetResponse, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetData, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetError, GetWorkflowRunsApiV1WorkflowWorkflowIdRunsGetResponse, GetWorkflowsApiV1WorkflowFetchGetData, GetWorkflowsApiV1WorkflowFetchGetError, GetWorkflowsApiV1WorkflowFetchGetResponse, GetWorkflowsSummaryApiV1WorkflowSummaryGetData, GetWorkflowsSummaryApiV1WorkflowSummaryGetError, GetWorkflowsSummaryApiV1WorkflowSummaryGetResponse, GetWorkflowTemplatesApiV1WorkflowTemplatesGetData, GetWorkflowTemplatesApiV1WorkflowTemplatesGetResponse, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostData, HandleCloudonixStatusCallbackApiV1TelephonyCloudonixStatusCallbackWorkflowRunIdPostError, HandleInboundFallbackApiV1TelephonyInboundFallbackPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostData, HandleInboundTelephonyApiV1TelephonyInboundWorkflowIdPostError, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostData, HandleTwilioStatusCallbackApiV1TelephonyTwilioStatusCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostData, HandleVobizHangupCallbackApiV1TelephonyVobizHangupCallbackWorkflowRunIdPostError, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostData, HandleVobizHangupCallbackByWorkflowApiV1TelephonyVobizHangupCallbackWorkflowWorkflowIdPostError, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostData, HandleVobizRingCallbackApiV1TelephonyVobizRingCallbackWorkflowRunIdPostError, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostData, HandleVonageEventsApiV1TelephonyVonageEventsWorkflowRunIdPostError, HealthApiV1HealthGetData, HealthApiV1HealthGetResponse,ImpersonateApiV1SuperuserImpersonatePostData, ImpersonateApiV1SuperuserImpersonatePostError, ImpersonateApiV1SuperuserImpersonatePostResponse, InitializeEmbedSessionApiV1PublicEmbedInitPostData, InitializeEmbedSessionApiV1PublicEmbedInitPostError, InitializeEmbedSessionApiV1PublicEmbedInitPostResponse, InitiateCallApiV1PublicAgentUuidPostData, InitiateCallApiV1PublicAgentUuidPostError, InitiateCallApiV1PublicAgentUuidPostResponse, InitiateCallApiV1TelephonyInitiateCallPostData, InitiateCallApiV1TelephonyInitiateCallPostError, ListCredentialsApiV1CredentialsGetData, ListCredentialsApiV1CredentialsGetError, ListCredentialsApiV1CredentialsGetResponse, ListDocumentsApiV1KnowledgeBaseDocumentsGetData, ListDocumentsApiV1KnowledgeBaseDocumentsGetError, ListDocumentsApiV1KnowledgeBaseDocumentsGetResponse, ListTestSessionsApiV1LooptalkTestSessionsGetData, ListTestSessionsApiV1LooptalkTestSessionsGetError, ListTestSessionsApiV1LooptalkTestSessionsGetResponse, ListToolsApiV1ToolsGetData, ListToolsApiV1ToolsGetError, ListToolsApiV1ToolsGetResponse, OptionsConfigApiV1PublicEmbedConfigTokenOptionsData, OptionsConfigApiV1PublicEmbedConfigTokenOptionsError, OptionsInitApiV1PublicEmbedInitOptionsData, PauseCampaignApiV1CampaignCampaignIdPausePostData, PauseCampaignApiV1CampaignCampaignIdPausePostError, PauseCampaignApiV1CampaignCampaignIdPausePostResponse, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostData, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostError, ProcessDocumentApiV1KnowledgeBaseProcessDocumentPostResponse, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutData, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutError, ReactivateApiKeyApiV1UserApiKeysApiKeyIdReactivatePutResponse, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutData, ReactivateServiceKeyApiV1UserServiceKeysServiceKeyIdReactivatePutError, ResumeCampaignApiV1CampaignCampaignIdResumePostData, ResumeCampaignApiV1CampaignCampaignIdResumePostError, ResumeCampaignApiV1CampaignCampaignIdResumePostResponse, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostData, SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostError, SearchChunksApiV1KnowledgeBaseSearchPostData, SearchChunksApiV1KnowledgeBaseSearchPostError, SearchChunksApiV1KnowledgeBaseSearchPostResponse, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostData, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostError, SetAdminCommentApiV1SuperuserWorkflowRunsRunIdCommentPostResponse, StartCampaignApiV1CampaignCampaignIdStartPostData, StartCampaignApiV1CampaignCampaignIdStartPostError, StartCampaignApiV1CampaignCampaignIdStartPostResponse, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostData, StartTestSessionApiV1LooptalkTestSessionsTestSessionIdStartPostError, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostData, StopTestSessionApiV1LooptalkTestSessionsTestSessionIdStopPostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostData, UnarchiveToolApiV1ToolsToolUuidUnarchivePostError, UnarchiveToolApiV1ToolsToolUuidUnarchivePostResponse, UpdateCredentialApiV1CredentialsCredentialUuidPutData, UpdateCredentialApiV1CredentialsCredentialUuidPutError, UpdateCredentialApiV1CredentialsCredentialUuidPutResponse, UpdateIntegrationApiV1IntegrationIntegrationIdPutData, UpdateIntegrationApiV1IntegrationIntegrationIdPutError, UpdateIntegrationApiV1IntegrationIntegrationIdPutResponse, UpdateToolApiV1ToolsToolUuidPutData, UpdateToolApiV1ToolsToolUuidPutError, UpdateToolApiV1ToolsToolUuidPutResponse, UpdateUserConfigurationsApiV1UserConfigurationsUserPutData, UpdateUserConfigurationsApiV1UserConfigurationsUserPutError, UpdateUserConfigurationsApiV1UserConfigurationsUserPutResponse, UpdateWorkflowApiV1WorkflowWorkflowIdPutData, UpdateWorkflowApiV1WorkflowWorkflowIdPutError, UpdateWorkflowApiV1WorkflowWorkflowIdPutResponse, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutData, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutError, UpdateWorkflowStatusApiV1WorkflowWorkflowIdStatusPutResponse, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetData, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetError, ValidateUserConfigurationsApiV1UserConfigurationsUserValidateGetResponse, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostData, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostError, ValidateWorkflowApiV1WorkflowWorkflowIdValidatePostResponse } from './types.gen'; export type Options = ClientOptions & { /** @@ -945,6 +945,19 @@ export const saveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPost = < }); }; +/** + * Get Campaign Limits + * Get campaign limits for the user's organization. + * + * Returns the organization's concurrent call limit and default retry configuration. + */ +export const getCampaignLimitsApiV1OrganizationsCampaignLimitsGet = (options?: Options) => { + return (options?.client ?? _heyApiClient).get({ + url: '/api/v1/organizations/campaign-limits', + ...options + }); +}; + /** * Generate a signed S3 URL * Return a short-lived signed URL for a transcript or recording file stored on S3. diff --git a/ui/src/client/types.gen.ts b/ui/src/client/types.gen.ts index cd1eeb9..3b61b67 100644 --- a/ui/src/client/types.gen.ts +++ b/ui/src/client/types.gen.ts @@ -43,6 +43,11 @@ export type AuthUserResponse = { export type CallType = 'inbound' | 'outbound'; +export type CampaignLimitsResponse = { + concurrent_call_limit: number; + default_retry_config: RetryConfigResponse; +}; + export type CampaignProgressResponse = { campaign_id: number; state: string; @@ -72,6 +77,8 @@ export type CampaignResponse = { created_at: string; started_at: string | null; completed_at: string | null; + retry_config: RetryConfigResponse; + max_concurrency?: number | null; }; export type CampaignSourceDownloadResponse = { @@ -177,6 +184,8 @@ export type CreateCampaignRequest = { workflow_id: number; source_type: string; source_id: string; + retry_config?: RetryConfigRequest | null; + max_concurrency?: number | null; }; /** @@ -689,6 +698,24 @@ export type ProcessDocumentRequestSchema = { embedding_service?: 'sentence_transformer' | 'openai'; }; +export type RetryConfigRequest = { + enabled?: boolean; + max_retries?: number; + retry_delay_seconds?: number; + retry_on_busy?: boolean; + retry_on_no_answer?: boolean; + retry_on_voicemail?: boolean; +}; + +export type RetryConfigResponse = { + enabled: boolean; + max_retries: number; + retry_delay_seconds: number; + retry_on_busy: boolean; + retry_on_no_answer: boolean; + retry_on_voicemail: boolean; +}; + export type S3SignedUrlResponse = { url: string; expires_in: number; @@ -3289,6 +3316,39 @@ export type SaveTelephonyConfigurationApiV1OrganizationsTelephonyConfigPostRespo 200: unknown; }; +export type GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetData = { + body?: never; + headers?: { + authorization?: string | null; + 'X-API-Key'?: string | null; + }; + path?: never; + query?: never; + url: '/api/v1/organizations/campaign-limits'; +}; + +export type GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetErrors = { + /** + * Not found + */ + 404: unknown; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetError = GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetErrors[keyof GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetErrors]; + +export type GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponses = { + /** + * Successful Response + */ + 200: CampaignLimitsResponse; +}; + +export type GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponse = GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponses[keyof GetCampaignLimitsApiV1OrganizationsCampaignLimitsGetResponses]; + export type GetSignedUrlApiV1S3SignedUrlGetData = { body?: never; headers?: { diff --git a/ui/src/components/layout/AppLayout.tsx b/ui/src/components/layout/AppLayout.tsx index 42d9170..19ae59a 100644 --- a/ui/src/components/layout/AppLayout.tsx +++ b/ui/src/components/layout/AppLayout.tsx @@ -34,7 +34,7 @@ const AppLayout: React.FC = ({ return ( -

+
{/* Optional header area for specific pages */} @@ -60,7 +60,7 @@ const AppLayout: React.FC = ({ )} {/* Main content area */} -
+
{children}