Skip to main content

Bulk CV Import — Frontend Integration

Upload a ZIP file containing multiple CVs (PDF, DOC, DOCX, TXT) to import talents in bulk. The backend processes each CV via AI extraction, creates User/Talent/CV records, and deduplicates by email. Progress is streamed in real-time via GraphQL subscriptions.


Architecture

User clicks "Import" → Import Choice Modal
├── Single Import (existing JSON flow)
└── Bulk CV Import (new ZIP flow)
├── Step 1: Upload ZIP to S3 (presigned URL + XHR)
├── Step 2: createBulkCvImport mutation (AI service)
└── Step 3: bulkCvImport subscription (progress + results)

Service Communication

StepTargetProtocolClient
S3 upload URLBackendApollo Client (GENERATE_UPLOAD_URL)Apollo
ZIP uploadS3/MinIOXHR PUT to presigned URLXMLHttpRequest
Start importNestJS AI gateway → BGaiGatewayRequest('createBulkCvImport', …, 'mutation')Apollo (via mutateAi)
Progress streamNestJS AI gateway → BGsubscribeAiOp('bulkCvImport', { importId }, …)Apollo (via subscribeAi)

Files Created

FilePurpose
lib/hooks/useBulkCvImport.tsCustom hook: upload → mutation → subscription orchestration
app/company/resume/talents/_components/BulkCvImportModal.tsxModal UI: upload, progress, results views
app/company/resume/talents/_components/ImportChoiceModal.tsxChooser modal: Single Import vs Bulk CV Import

The standalone GraphQL document constants (graphql/operations/bulkCvImport/mutations.ts and the equivalent subscription file) were removed in the Phase 3.7d cutover — both the createBulkCvImport mutation and the bulkCvImport subscription now live in the backend AI operation registries; the FE hook just sends the operation name + payload through the gateway.

Files Modified

FileChange
app/company/resume/talents/page.tsxImport button opens choice modal; hook at page level for background processing; background progress banner; completion toast

Hook API: useBulkCvImport()

State Machine

IDLE ──[startImport()]──→ UPLOADING ──→ INITIATING ──→ PROCESSING ──→ COMPLETED
└──→ FAILED
Any step can transition to FAILED on error.

Return Type

interface UseBulkCvImportReturn {
// Phase
phase: "idle" | "uploading" | "initiating" | "processing" | "completed" | "failed"
isProcessing: boolean

// Progress
uploadProgress: number // 0-100 during upload
processingProgress: number // 0-100 during processing
statusMessage: string // Human-readable status

// File counts
totalFiles: number
processedFiles: number
successCount: number
failedCount: number
skippedCount: number

// Results (populated on completion)
results: BulkCvFileResult[]
error: string | null
importId: string | null

// Actions
startImport: (file: File) => Promise<void>
reset: () => void
}

interface BulkCvFileResult {
filename: string
status: string // "SUCCESS" | "FAILED" | "SKIPPED"
userId?: string
talentId?: string
cvId?: string
email?: string
errorMessage?: string
}

GraphQL Operations

Mutation (AI Service)

Returns a plain BulkCvImportProcessing object (NOT a union type). Errors are returned as standard GraphQL errors.

mutation CreateBulkCvImport($input: BulkCvImportInput!) {
createBulkCvImport(input: $input) {
id status message progress totalFiles processedFiles
successCount failedCount skippedCount
}
}

Input: { fileUrl: String!, companyId: String!, userId: String! }

  • fileUrl is the S3 objectPath (e.g. uploads/2026/03/file.zip), not a full URL.

Subscription (AI Service WebSocket)

subscription BulkCvImport($importId: String!) {
bulkCvImport(importId: $importId) {
... on BulkCvImportProcessing {
id status message progress totalFiles processedFiles
successCount failedCount skippedCount
}
... on BulkCvImportSuccess {
id status totalFiles successCount failedCount skippedCount
processingTimeMs
results { filename status userId talentId cvId email errorMessage }
}
... on BulkCvImportFailure {
id status
error { code message recoveryHint }
}
}
}

The importId for the subscription comes from the id field in the mutation response.


UI Flow

  1. Import Button → Opens ImportChoiceModal with two options
  2. "Bulk CV Import" → Opens BulkCvImportModal
  3. Upload View (idle): Drag & drop Dropzone for ZIP files, shows selected file info
  4. Processing View (uploading/initiating/processing): Progress bars, live file counts
  5. Results View (completed): Summary cards (total/imported/failed/skipped) + per-file results table
  6. Error View (failed): Error message with "Try Again" button

Background Processing

The useBulkCvImport hook is lifted to the page level (not inside the modal), so the WebSocket subscription survives modal close:

  • During processing, the modal shows a "Close & Continue in Background" button
  • When the modal is closed while processing, a progress banner appears at the top of the talents page showing:
    • File counts (3/5 files processed)
    • Imported/failed counters
    • TWProgress bar with live percentage
    • "View Details" link to re-open the modal
  • When processing completes in the background, a toast notification fires with summary counts and a "View Results" button to re-open the modal with the results view
  • The talent list is auto-refetched on successful background completion

Error Handling

Client-Side Validation

  • Non-ZIP files rejected immediately
  • Files > 500 MB rejected
  • Missing company context shows error

Server Errors

  • Mutation errors (invalid ZIP, no CV files, too many files) displayed in error view
  • Subscription errors (network, timeout) show toast + error view

Billing Errors

  • showBillingToastIfNeeded() checks for INSUFFICIENT_CREDITS, PLAN_LIMIT_EXCEEDED, etc.
  • Billing-blocked CVs are marked as SKIPPED (not FAILED) by the backend

Environment Variables

The hook needs no AI-specific env vars after the Phase 3.7 cutover. It uses the same NEXT_PUBLIC_API_URL (HTTP) and NEXT_PUBLIC_GRAPHQL_WS_URL (WS) as the rest of the frontend.


Supported File Types (inside ZIP)

ExtensionFormat
.pdfPDF documents
.docLegacy Word
.docxModern Word
.txtPlain text

Skipped automatically: __MACOSX/ folders, ._ prefixed files, hidden files, unsupported extensions.


Limitations

  • No password-protected ZIPs — encrypted ZIP files will fail
  • Email required — CVs without an email in the parsed data are marked as FAILED
  • No invitation emails — Users created via bulk import are inactive and unverified
  • Text files (.txt) may fail if the LLM cannot parse structured data from plain text
  • Maximum 500 files per ZIP, maximum 500 MB compressed size

Patterns Reused

PatternSource
S3 presigned upload + XHR progresslib/hooks/useCvUploadAndParse.ts
AI streaming subscriptionsubscribeAiOp in lib/ai/aiGateway.ts (used by every AI hook)
AI HTTP mutationsaiGatewayRequest in lib/ai/aiGateway.ts
Billing error detectionlib/hooks/useBillingError.ts
Modal with results tableImportTalentModal.tsx
File drag & dropcomponents/reusable/Dropzone.tsx

Last updated: 2026-03-09