Skip to main content

Frontend API Documentation: CV Extraction & Job Parsing

This document provides comprehensive documentation for frontend developers integrating with the CV Extraction and Job Parsing pipelines.

Table of Contents


Overview

The system provides two main document parsing pipelines:

  1. CV Extraction: Parse CV/resume documents (PDF, DOCX, images) to extract structured candidate data
  2. Job Parsing: Parse job descriptions (text or documents) to extract structured job requirements

Both pipelines use GraphQL subscriptions for real-time progress updates and AI-powered extraction via AWS Bedrock Claude.

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│ Upload File │────▶│ GraphQL │────▶│ AI Extraction │
│ to MinIO │ │ Subscription │ │ (Bedrock) │
└─────────────────┘ └─────────────────┘ └─────────────────┘


┌─────────────────┐
│ Structured │
│ Data Output │
└─────────────────┘

Base URL

EnvironmentREST/GraphQL URLWebSocket URL
Productionhttps://ai.aiqlick.comwss://ai.aiqlick.com/graphql
Developmenthttp://localhost:8080ws://localhost:8080/graphql

CV Extraction API

Extract structured candidate data from CV/resume documents using AI.

CV Extraction GraphQL Subscription

subscription ExtractCv($input: CVExtractionInput!) {
extractCv(input: $input) {
... on CVExtractionProcessing {
id
status
message
progress
}
... on CVExtractionSuccess {
id
status
parsed {
contact {
name
firstName
middleName
lastName
email
phone
linkedinUrl
profileLink
gender
dateOfBirth
location
}
skills {
skill
years
proficiency
}
experience {
title
company
startDate
endDate
description
location
workType
industry
skills
}
education {
degree
major
institution
startDate
endDate
gradYear
gpa
}
languages {
name
proficiency
}
certifications {
name
institution
year
location
expiryDate
credentialId
}
workMode
introduction
cvUrl
}
rawText
fileUrl
}
... on CVExtractionFailure {
id
status
error {
code
message
field
context
timestamp
recoveryHint
}
}
}
}

Variables:

{
"input": {
"id": "cv-unique-123",
"fileUrl": "https://storage.aiqlick.com/aiqlick-uploads/cvs/resume.pdf",
"sequential": true
}
}

CV Extraction Input Parameters

FieldTypeRequiredDefaultDescription
idStringYes-Unique identifier for this extraction request. Use UUID or similar.
fileUrlStringYes-URL to the CV file. Can be MinIO URL or external URL. Supported formats: PDF, DOCX, PNG, JPG, JPEG
sequentialBooleanNotrueProcess document pages sequentially. Recommended for better accuracy.
schemaOverrideStringNo-Custom extraction schema (JSON string). Advanced use only.

CV Extraction Output Data Models

CVParsedData (Main Output)

FieldTypeNullableDescription
contactContactInfoNoContact information (required)
skills[SkillItem]NoList of all extracted skills
experience[ExperienceItem]NoWork experience entries
education[EducationItem]NoEducational qualifications
languages[LanguageItem]NoLanguages with proficiency levels
certifications[CertificationItem]NoProfessional certifications
workModeStringYesPreferred work arrangement: REMOTE, ONSITE, HYBRID, UNSPECIFIED
introductionStringYesProfessional summary/objective
cvUrlStringYesURL to the original CV document

ContactInfo

FieldTypeNullableDescription
nameStringNoFull name of the candidate
firstNameStringYesFirst name
middleNameStringYesMiddle name
lastNameStringYesLast name
emailStringNoEmail address
phoneStringYesPhone number
linkedinUrlStringYesLinkedIn profile URL
profileLinkStringYesOther profile link (GitHub, portfolio)
genderStringYesGender
dateOfBirthStringYesDate of birth (YYYY-MM-DD or MM/DD/YYYY)
locationStringYesLocation/address

SkillItem

FieldTypeDefaultDescription
skillString-Skill name (e.g., "Python", "Project Management")
yearsInt0Years of experience with this skill
proficiencySkillProficiencyBASICProficiency level

ExperienceItem

FieldTypeNullableDescription
titleStringNoJob title
companyStringNoCompany name
startDateStringNoStart date (YYYY-MM or YYYY-MM-DD)
endDateStringYesEnd date (null = current position)
descriptionStringYesRole description and achievements
locationStringYesWork location
workTypeWorkTypeNoREMOTE, ONSITE, HYBRID, UNSPECIFIED
industryStringYesIndustry sector
skills[String]NoSkills used in this role

EducationItem

FieldTypeNullableDescription
degreeStringNoDegree name (e.g., "Bachelor of Science")
majorStringYesMajor/field of study
institutionStringYesSchool/university name
startDateStringYesStart date
endDateStringYesEnd date
gradYearIntYesGraduation year
gpaFloatYesGrade point average

LanguageItem

FieldTypeDescription
nameStringLanguage name (e.g., "English", "Swedish")
proficiencyLanguageProficiencyProficiency level

CertificationItem

FieldTypeNullableDescription
nameStringNoCertification name
institutionStringYesIssuing organization
yearIntYesYear obtained
locationStringYesLocation where obtained
expiryDateStringYesExpiration date
credentialIdStringYesCredential ID/number

CV Extraction Progress Updates

The subscription yields progress updates during processing:

ProgressMessageDescription
0%"Initializing CV extraction..."Starting the pipeline
25%"Processing document and extracting text..."Converting document to text
50%"Extracting structured data with AI..."AI analysis in progress
75%"Transforming extracted data..."Formatting results
100%(Success response)Extraction complete

Typical processing time: 30-60 seconds depending on document complexity.


Job Parsing API

Extract structured job requirements from job descriptions using AI.

Job Parsing GraphQL Subscription

subscription ParseJob($input: JobParsingInput!) {
parseJob(input: $input) {
... on JobParsingProcessing {
id
status
message
progress
}
... on JobParsingSuccess {
id
status
extractedData {
title
description
jobType
workSite
salary
currency
rateIn
rateOut
startDate
dueDate
endDate
priority
publishedOnline
skills {
name
proficiency
yearsExperience
mustHave
weight
}
languages
requirements
responsibilities
benefits
companyName
industry
location
}
rawText
fileUrl
processingTime
}
... on JobParsingFailure {
id
status
error {
code
message
field
context
timestamp
recoveryHint
}
}
}
}

Variables (with text):

{
"input": {
"id": "job-unique-456",
"text": "We are looking for a Senior Software Engineer with 5+ years of experience..."
}
}

Variables (with file URL):

{
"input": {
"id": "job-unique-456",
"fileUrl": "https://storage.aiqlick.com/aiqlick-uploads/jobs/description.pdf"
}
}

Job Parsing Input Parameters

FieldTypeRequiredDescription
idStringYesUnique identifier for this parsing request
textStringNo*Job description text content
fileUrlStringNo*URL to job description file (PDF, DOCX, images)
extractionSchemaStringNoCustom extraction schema (JSON string). Advanced use only.

Note: Either text or fileUrl must be provided. If both are provided, fileUrl takes precedence.

Job Parsing Output Data Models

JobExtractedData (Main Output)

FieldTypeNullableDefaultDescription
titleStringNo-Job title
descriptionStringYes-Full job description
jobTypeJobTypeNo-Employment type
workSiteWorkSiteNo-Work location arrangement
salaryFloatYes-Salary amount
currencyStringYes-3-letter currency code (USD, EUR, SEK)
rateInFloatYes-Input hourly/daily rate (for contractors)
rateOutFloatYes-Output/billing rate (for contractors)
startDateStringYes-Job start date (YYYY-MM-DD)
dueDateStringYes-Application deadline (YYYY-MM-DD)
endDateStringYes-Job end date for temporary positions (YYYY-MM-DD)
priorityBooleanNofalseHigh priority position flag
publishedOnlineBooleanNotruePublish on job boards
skills[JobSkillRequirement]No[]Required skills with details
languages[String]No[]Required languages
requirementsStringYes-Job requirements text
responsibilitiesStringYes-Job responsibilities text
benefitsStringYes-Benefits offered
companyNameStringYes-Company name
industryStringYes-Industry sector
locationStringYes-Job location

JobSkillRequirement

FieldTypeNullableDefaultDescription
nameStringNo-Skill name
proficiencyJobSkillProficiencyNoINTERMEDIATERequired proficiency level
yearsExperienceIntYes-Years of experience required
mustHaveBooleanNotrueWhether skill is mandatory
weightIntNo10Skill importance weight (1-100)

Job Parsing Progress Updates

ProgressMessageDescription
0%"Initializing job parsing..."Starting the pipeline
25%"Processing document..."Converting document to text (if file URL provided)
50%"Extracting job data with AI..."AI analysis in progress
75%"Transforming extracted data..."Validating and formatting results
100%(Success response)Parsing complete

Typical processing time: 25-55 seconds.


Common Concepts

File Upload and MinIO URLs

Files should be uploaded to MinIO storage before extraction. The system accepts:

  1. MinIO URLs (recommended): https://storage.aiqlick.com/aiqlick-uploads/path/to/file.pdf
  2. External URLs: Any publicly accessible URL to a supported file format

Supported file formats:

  • PDF (.pdf)
  • Microsoft Word (.docx, .doc)
  • Images (.png, .jpg, .jpeg)

MinIO URL format:

https://storage.aiqlick.com/{bucket}/{path}

Default bucket: aiqlick-uploads

Pipeline Status Codes

StatusDescription
processingOperation is currently in progress
completedOperation completed successfully
failedOperation failed with an error

Error Types and Handling

PipelineError Structure

FieldTypeNullableDescription
codePipelineErrorTypeNoError type code
messageStringNoHuman-readable error message
fieldStringYesField that caused error (dot notation for nested fields)
contextJSONYesAdditional error context for debugging
timestampFloatNoUnix timestamp when error occurred
recoveryHintStringYesSuggestion for recovery

Error Codes Reference

Validation Errors:

CodeDescriptionCommon Cause
VALIDATION_ERRORInput validation failedInvalid field values
INVALID_INPUTInput data is malformedMissing required fields
MISSING_FIELDRequired field is missingNull required parameter
INVALID_FORMATField format is invalidWrong date format

Processing Errors:

CodeDescriptionCommon Cause
DOCUMENT_ERRORFailed to process documentCorrupted file, unsupported format
EXTRACTION_ERRORAI extraction failedLLM service issues
AI_ERRORLLM service errorBedrock timeout or quota

Infrastructure Errors:

CodeDescriptionCommon Cause
DATABASE_ERRORDatabase operation failedConnection issues
TIMEOUT_ERROROperation timed outProcessing took >5 minutes
NETWORK_ERRORNetwork communication errorFile download failed
STORAGE_ERRORFile storage operation failedMinIO unavailable

Resource Errors:

CodeDescriptionCommon Cause
NOT_FOUNDResource not foundInvalid file URL
JOB_NOT_FOUNDJob ID doesn't existInvalid job reference
USER_NOT_FOUNDUser ID doesn't existInvalid user reference

Enum Values Reference

SkillProficiency (CV Extraction)

ValueDescription
NONENo proficiency
BASICBasic/beginner level
INTERMEDIATEIntermediate level
ADVANCEDAdvanced level
EXPERTExpert/mastery level

JobSkillProficiency (Job Parsing)

ValueDescription
NONENot required
BASICBasic knowledge sufficient
INTERMEDIATEWorking knowledge required
ADVANCEDDeep expertise required
EXPERTExpert-level mastery required

WorkType / WorkSite

ValueDescription
REMOTEFully remote position
ONSITEOn-site/in-office only
HYBRIDMix of remote and on-site
UNSPECIFIED / NOT_AVAILABLENot specified
OFFSHOREOffshore location (Job Parsing only)

JobType

ValueDescription
FREELANCE_GIGFreelance/contract work
INTERNSHIPInternship position
TRIAL_AND_HIRETrial period before full hire
DIRECT_HIREDirect permanent hire

LanguageProficiency

ValueDescription
BEGINNERA1 level
ELEMENTARYA2 level
INTERMEDIATEB1-B2 level
ADVANCEDC1 level
FLUENTC2 level
NATIVENative speaker

Frontend Integration Examples

React/TypeScript Examples

Type Definitions

// types/cv-extraction.ts

export type SkillProficiency =
| "NONE"
| "BASIC"
| "INTERMEDIATE"
| "ADVANCED"
| "EXPERT"
export type LanguageProficiency =
| "BEGINNER"
| "ELEMENTARY"
| "INTERMEDIATE"
| "ADVANCED"
| "FLUENT"
| "NATIVE"
export type WorkType = "REMOTE" | "ONSITE" | "HYBRID" | "UNSPECIFIED"
export type PipelineStatus = "processing" | "completed" | "failed"

export interface ContactInfo {
name: string
firstName?: string
middleName?: string
lastName?: string
email: string
phone?: string
linkedinUrl?: string
profileLink?: string
gender?: string
dateOfBirth?: string
location?: string
}

export interface SkillItem {
skill: string
years: number
proficiency: SkillProficiency
}

export interface ExperienceItem {
title: string
company: string
startDate: string
endDate?: string
description?: string
location?: string
workType: WorkType
industry?: string
skills: string[]
}

export interface EducationItem {
degree: string
major?: string
institution?: string
startDate?: string
endDate?: string
gradYear?: number
gpa?: number
}

export interface LanguageItem {
name: string
proficiency: LanguageProficiency
}

export interface CertificationItem {
name: string
institution?: string
year?: number
location?: string
expiryDate?: string
credentialId?: string
}

export interface CVParsedData {
contact: ContactInfo
skills: SkillItem[]
experience: ExperienceItem[]
education: EducationItem[]
languages: LanguageItem[]
certifications: CertificationItem[]
workMode?: string
introduction?: string
cvUrl?: string
}

export interface PipelineError {
code: string
message: string
field?: string
context?: Record<string, unknown>
timestamp: number
recoveryHint?: string
}

export interface CVExtractionProcessing {
__typename: "CVExtractionProcessing"
id: string
status: "processing"
message: string
progress: number
}

export interface CVExtractionSuccess {
__typename: "CVExtractionSuccess"
id: string
status: "completed"
parsed: CVParsedData
rawText: string
fileUrl: string
}

export interface CVExtractionFailure {
__typename: "CVExtractionFailure"
id: string
status: "failed"
error: PipelineError
}

export type CVExtractionResult =
| CVExtractionProcessing
| CVExtractionSuccess
| CVExtractionFailure
// types/job-parsing.ts

export type JobType =
| "FREELANCE_GIG"
| "INTERNSHIP"
| "TRIAL_AND_HIRE"
| "DIRECT_HIRE"
export type WorkSite =
| "NOT_AVAILABLE"
| "HYBRID"
| "OFFSHORE"
| "ONSITE"
| "REMOTE"
export type JobSkillProficiency =
| "NONE"
| "BASIC"
| "INTERMEDIATE"
| "ADVANCED"
| "EXPERT"

export interface JobSkillRequirement {
name: string
proficiency: JobSkillProficiency
yearsExperience?: number
mustHave: boolean
weight: number
}

export interface JobExtractedData {
title: string
description?: string
jobType: JobType
workSite: WorkSite
salary?: number
currency?: string
rateIn?: number
rateOut?: number
startDate?: string
dueDate?: string
endDate?: string
priority: boolean
publishedOnline: boolean
skills: JobSkillRequirement[]
languages: string[]
requirements?: string
responsibilities?: string
benefits?: string
companyName?: string
industry?: string
location?: string
}

export interface JobParsingProcessing {
__typename: "JobParsingProcessing"
id: string
status: "processing"
message: string
progress: number
}

export interface JobParsingSuccess {
__typename: "JobParsingSuccess"
id: string
status: "completed"
extractedData: JobExtractedData
rawText: string
fileUrl?: string
processingTime: number
}

export interface JobParsingFailure {
__typename: "JobParsingFailure"
id: string
status: "failed"
error: PipelineError
}

export type JobParsingResult =
| JobParsingProcessing
| JobParsingSuccess
| JobParsingFailure

GraphQL Subscription Hooks

// hooks/useCVExtraction.ts
import { useSubscription, gql } from "@apollo/client"
import { useState, useCallback } from "react"
import { v4 as uuidv4 } from "uuid"
import type {
CVExtractionResult,
CVParsedData,
PipelineError,
} from "../types/cv-extraction"

const CV_EXTRACTION_SUBSCRIPTION = gql`
subscription ExtractCV($input: CVExtractionInput!) {
extractCV(input: $input) {
... on CVExtractionProcessing {
id
status
message
progress
}
... on CVExtractionSuccess {
id
status
parsed {
contact {
name
firstName
lastName
email
phone
linkedinUrl
location
}
skills {
skill
years
proficiency
}
experience {
title
company
startDate
endDate
description
location
workType
skills
}
education {
degree
major
institution
gradYear
}
languages {
name
proficiency
}
certifications {
name
institution
year
}
workMode
introduction
}
rawText
fileUrl
}
... on CVExtractionFailure {
id
status
error {
code
message
field
recoveryHint
}
}
}
}
`

interface UseCVExtractionOptions {
onProgress?: (progress: number, message: string) => void
onSuccess?: (data: CVParsedData) => void
onError?: (error: PipelineError) => void
}

export function useCVExtraction(options: UseCVExtractionOptions = {}) {
const [extractionId, setExtractionId] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
const [message, setMessage] = useState("")
const [result, setResult] = useState<CVParsedData | null>(null)
const [error, setError] = useState<PipelineError | null>(null)
const [isExtracting, setIsExtracting] = useState(false)

const { data } = useSubscription(CV_EXTRACTION_SUBSCRIPTION, {
skip: !extractionId,
variables: {
input: {
id: extractionId,
fileUrl: "", // Will be set when startExtraction is called
sequential: true,
},
},
onData: ({ data: subscriptionData }) => {
const update = subscriptionData?.data?.extractCV as CVExtractionResult
if (!update) return

switch (update.__typename) {
case "CVExtractionProcessing":
setProgress(update.progress)
setMessage(update.message)
options.onProgress?.(update.progress, update.message)
break

case "CVExtractionSuccess":
setResult(update.parsed)
setProgress(100)
setIsExtracting(false)
options.onSuccess?.(update.parsed)
break

case "CVExtractionFailure":
setError(update.error)
setIsExtracting(false)
options.onError?.(update.error)
break
}
},
})

const startExtraction = useCallback((fileUrl: string, customId?: string) => {
const id = customId || uuidv4()
setExtractionId(id)
setProgress(0)
setMessage("Initializing...")
setResult(null)
setError(null)
setIsExtracting(true)
return id
}, [])

const reset = useCallback(() => {
setExtractionId(null)
setProgress(0)
setMessage("")
setResult(null)
setError(null)
setIsExtracting(false)
}, [])

return {
startExtraction,
reset,
progress,
message,
result,
error,
isExtracting,
}
}
// hooks/useJobParsing.ts
import { useSubscription, gql } from "@apollo/client"
import { useState, useCallback } from "react"
import { v4 as uuidv4 } from "uuid"
import type {
JobParsingResult,
JobExtractedData,
PipelineError,
} from "../types/job-parsing"

const JOB_PARSING_SUBSCRIPTION = gql`
subscription ParseJob($input: JobParsingInput!) {
parseJob(input: $input) {
... on JobParsingProcessing {
id
status
message
progress
}
... on JobParsingSuccess {
id
status
extractedData {
title
description
jobType
workSite
salary
currency
skills {
name
proficiency
yearsExperience
mustHave
weight
}
languages
requirements
responsibilities
benefits
companyName
industry
location
startDate
dueDate
}
rawText
fileUrl
processingTime
}
... on JobParsingFailure {
id
status
error {
code
message
field
recoveryHint
}
}
}
}
`

interface UseJobParsingOptions {
onProgress?: (progress: number, message: string) => void
onSuccess?: (data: JobExtractedData, processingTime: number) => void
onError?: (error: PipelineError) => void
}

export function useJobParsing(options: UseJobParsingOptions = {}) {
const [parsingId, setParsingId] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
const [message, setMessage] = useState("")
const [result, setResult] = useState<JobExtractedData | null>(null)
const [error, setError] = useState<PipelineError | null>(null)
const [isParsing, setIsParsing] = useState(false)
const [processingTime, setProcessingTime] = useState<number | null>(null)

useSubscription(JOB_PARSING_SUBSCRIPTION, {
skip: !parsingId,
variables: {
input: {
id: parsingId,
text: "", // Will be set when startParsing is called
},
},
onData: ({ data: subscriptionData }) => {
const update = subscriptionData?.data?.parseJob as JobParsingResult
if (!update) return

switch (update.__typename) {
case "JobParsingProcessing":
setProgress(update.progress)
setMessage(update.message)
options.onProgress?.(update.progress, update.message)
break

case "JobParsingSuccess":
setResult(update.extractedData)
setProcessingTime(update.processingTime)
setProgress(100)
setIsParsing(false)
options.onSuccess?.(update.extractedData, update.processingTime)
break

case "JobParsingFailure":
setError(update.error)
setIsParsing(false)
options.onError?.(update.error)
break
}
},
})

const startParsingFromText = useCallback(
(text: string, customId?: string) => {
const id = customId || uuidv4()
setParsingId(id)
setProgress(0)
setMessage("Initializing...")
setResult(null)
setError(null)
setIsParsing(true)
return id
},
[],
)

const startParsingFromFile = useCallback(
(fileUrl: string, customId?: string) => {
const id = customId || uuidv4()
setParsingId(id)
setProgress(0)
setMessage("Initializing...")
setResult(null)
setError(null)
setIsParsing(true)
return id
},
[],
)

const reset = useCallback(() => {
setParsingId(null)
setProgress(0)
setMessage("")
setResult(null)
setError(null)
setIsParsing(false)
setProcessingTime(null)
}, [])

return {
startParsingFromText,
startParsingFromFile,
reset,
progress,
message,
result,
error,
isParsing,
processingTime,
}
}

Progress Bar Implementation

// components/ExtractionProgressBar.tsx
import React from 'react';

interface ProgressBarProps {
progress: number;
message: string;
status: 'idle' | 'processing' | 'completed' | 'failed';
error?: { message: string; recoveryHint?: string } | null;
}

export function ExtractionProgressBar({
progress,
message,
status,
error
}: ProgressBarProps) {
const getStatusColor = () => {
switch (status) {
case 'processing': return 'bg-blue-500';
case 'completed': return 'bg-green-500';
case 'failed': return 'bg-red-500';
default: return 'bg-gray-300';
}
};

return (
<div className="w-full">
{/* Progress Bar */}
<div className="w-full bg-gray-200 rounded-full h-2.5 mb-2">
<div
className={`h-2.5 rounded-full transition-all duration-300 ${getStatusColor()}`}
style={{ width: `${progress}%` }}
/>
</div>

{/* Status Message */}
<div className="flex justify-between items-center text-sm">
<span className="text-gray-600">{message}</span>
<span className="text-gray-400">{progress}%</span>
</div>

{/* Error Display */}
{status === 'failed' && error && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-700 font-medium">{error.message}</p>
{error.recoveryHint && (
<p className="text-red-600 text-sm mt-1">
Tip: {error.recoveryHint}
</p>
)}
</div>
)}

{/* Success Display */}
{status === 'completed' && (
<div className="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<p className="text-green-700 font-medium">
Extraction completed successfully!
</p>
</div>
)}
</div>
);
}
// components/CVExtractionForm.tsx
import React, { useState } from 'react';
import { useCVExtraction } from '../hooks/useCVExtraction';
import { ExtractionProgressBar } from './ExtractionProgressBar';

export function CVExtractionForm() {
const [fileUrl, setFileUrl] = useState('');
const {
startExtraction,
reset,
progress,
message,
result,
error,
isExtracting,
} = useCVExtraction({
onSuccess: (data) => {
console.log('Extraction successful:', data);
},
onError: (err) => {
console.error('Extraction failed:', err);
},
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (fileUrl.trim()) {
startExtraction(fileUrl);
}
};

const getStatus = () => {
if (error) return 'failed';
if (result) return 'completed';
if (isExtracting) return 'processing';
return 'idle';
};

return (
<div className="max-w-2xl mx-auto p-6">
<h2 className="text-2xl font-bold mb-6">CV Extraction</h2>

<form onSubmit={handleSubmit} className="mb-6">
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
CV File URL
</label>
<input
type="url"
value={fileUrl}
onChange={(e) => setFileUrl(e.target.value)}
placeholder="https://storage.aiqlick.com/aiqlick-uploads/cvs/resume.pdf"
className="w-full p-3 border rounded-lg"
disabled={isExtracting}
/>
</div>

<div className="flex gap-4">
<button
type="submit"
disabled={isExtracting || !fileUrl.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isExtracting ? 'Extracting...' : 'Extract CV'}
</button>

{(result || error) && (
<button
type="button"
onClick={reset}
className="px-6 py-2 bg-gray-200 text-gray-700 rounded-lg"
>
Reset
</button>
)}
</div>
</form>

{/* Progress Bar */}
{(isExtracting || result || error) && (
<ExtractionProgressBar
progress={progress}
message={message}
status={getStatus()}
error={error}
/>
)}

{/* Results Display */}
{result && (
<div className="mt-6 p-6 bg-white border rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Extracted Data</h3>

{/* Contact Info */}
<div className="mb-4">
<h4 className="font-medium text-gray-700">Contact</h4>
<p>{result.contact.name}</p>
<p className="text-gray-600">{result.contact.email}</p>
{result.contact.phone && (
<p className="text-gray-600">{result.contact.phone}</p>
)}
</div>

{/* Skills */}
<div className="mb-4">
<h4 className="font-medium text-gray-700">
Skills ({result.skills.length})
</h4>
<div className="flex flex-wrap gap-2 mt-2">
{result.skills.map((skill, idx) => (
<span
key={idx}
className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
>
{skill.skill}
{skill.years > 0 && ` (${skill.years}y)`}
</span>
))}
</div>
</div>

{/* Experience */}
<div className="mb-4">
<h4 className="font-medium text-gray-700">
Experience ({result.experience.length})
</h4>
{result.experience.map((exp, idx) => (
<div key={idx} className="mt-2 p-3 bg-gray-50 rounded">
<p className="font-medium">{exp.title}</p>
<p className="text-gray-600">{exp.company}</p>
<p className="text-sm text-gray-500">
{exp.startDate} - {exp.endDate || 'Present'}
</p>
</div>
))}
</div>
</div>
)}
</div>
);
}

Error Handling Patterns

// utils/error-handling.ts
import type { PipelineError } from "../types/cv-extraction"

export function getErrorMessage(error: PipelineError): string {
// Map error codes to user-friendly messages
const errorMessages: Record<string, string> = {
VALIDATION_ERROR:
"The input data is invalid. Please check your submission.",
DOCUMENT_ERROR:
"We could not process this document. Please ensure it's a valid PDF, DOCX, or image file.",
EXTRACTION_ERROR:
"The AI extraction service encountered an issue. Please try again.",
AI_ERROR:
"Our AI service is temporarily unavailable. Please try again in a few moments.",
TIMEOUT_ERROR:
"The operation took too long. Please try with a smaller file.",
NETWORK_ERROR:
"Unable to download the file. Please check the URL is accessible.",
STORAGE_ERROR:
"File storage service is unavailable. Please try again later.",
NOT_FOUND: "The specified file could not be found.",
MISSING_FIELD: `Required field is missing: ${error.field}`,
}

return errorMessages[error.code] || error.message
}

export function isRetryableError(error: PipelineError): boolean {
const retryableCodes = [
"AI_ERROR",
"TIMEOUT_ERROR",
"NETWORK_ERROR",
"STORAGE_ERROR",
]
return retryableCodes.includes(error.code)
}

export function getRetryDelay(attemptNumber: number): number {
// Exponential backoff: 1s, 2s, 4s, 8s...
return Math.min(1000 * Math.pow(2, attemptNumber - 1), 30000)
}

// Retry wrapper for extraction operations
export async function withRetry<T>(
operation: () => Promise<T>,
maxAttempts: number = 3,
shouldRetry: (error: unknown) => boolean = () => true,
): Promise<T> {
let lastError: unknown

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation()
} catch (error) {
lastError = error

if (attempt < maxAttempts && shouldRetry(error)) {
const delay = getRetryDelay(attempt)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}

throw error
}
}

throw lastError
}
// components/ErrorDisplay.tsx
import React from 'react';
import type { PipelineError } from '../types/cv-extraction';
import { getErrorMessage, isRetryableError } from '../utils/error-handling';

interface ErrorDisplayProps {
error: PipelineError;
onRetry?: () => void;
}

export function ErrorDisplay({ error, onRetry }: ErrorDisplayProps) {
const userMessage = getErrorMessage(error);
const canRetry = isRetryableError(error);

return (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start">
<div className="flex-shrink-0">
{/* Error icon */}
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800">
{userMessage}
</h3>

{error.recoveryHint && (
<p className="mt-2 text-sm text-red-700">
<strong>Tip:</strong> {error.recoveryHint}
</p>
)}

{error.field && (
<p className="mt-1 text-sm text-red-600">
Field: {error.field}
</p>
)}

{canRetry && onRetry && (
<button
onClick={onRetry}
className="mt-3 px-4 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200 text-sm font-medium"
>
Try Again
</button>
)}
</div>
</div>

{/* Debug info (only in development) */}
{process.env.NODE_ENV === 'development' && (
<details className="mt-4">
<summary className="text-sm text-red-500 cursor-pointer">
Debug Information
</summary>
<pre className="mt-2 text-xs text-red-600 overflow-auto">
{JSON.stringify(error, null, 2)}
</pre>
</details>
)}
</div>
);
}

Best Practices

1. Use GraphQL Subscriptions for Real-Time UX

For the best user experience, always use GraphQL subscriptions to show real-time progress. The operations can take 30-60 seconds, and showing progress keeps users informed.

2. Generate Unique Request IDs

Always generate unique IDs for each extraction/parsing request using UUIDs. This prevents conflicts and allows for request tracking.

import { v4 as uuidv4 } from "uuid"
const requestId = uuidv4()

3. Handle All Three Response Types

Always implement handlers for all three response types:

  • Processing - Show progress
  • Success - Display results
  • Failure - Show error with recovery options

4. Validate File URLs Before Submission

Check that the file URL is accessible and in a supported format before starting extraction:

const SUPPORTED_EXTENSIONS = [".pdf", ".docx", ".doc", ".png", ".jpg", ".jpeg"]

function isValidFileUrl(url: string): boolean {
try {
const urlObj = new URL(url)
const extension = urlObj.pathname.toLowerCase().split(".").pop()
return SUPPORTED_EXTENSIONS.some((ext) => `.${extension}` === ext)
} catch {
return false
}
}

5. Implement Retry Logic for Transient Errors

Some errors (AI_ERROR, TIMEOUT_ERROR, NETWORK_ERROR) are transient. Implement retry with exponential backoff for these cases.

6. Cache Extracted Data

Once extraction is successful, cache the results locally. The same file will produce the same results, so there's no need to re-extract.

7. Default Timeout Handling

The default timeout is 5 minutes (300 seconds). Inform users if they're working with large files that extraction may take longer, and implement client-side timeout handling.

8. Use Sequential Mode for Better Accuracy

For CV extraction, always use sequential: true for better accuracy, especially with multi-page documents. This processes pages in order, maintaining context.

9. Prefer Text Input for Job Parsing

When possible, prefer providing job description text directly rather than file URLs. This is faster and more reliable.


Quick Reference

Endpoints Summary

FeatureGraphQL SubscriptionWebSocket URL
CV ExtractionextractCVwss://ai.aiqlick.com/graphql
Job ParsingparseJobwss://ai.aiqlick.com/graphql

Processing Times

OperationTypical TimeTimeout
CV Extraction30-60 seconds5 minutes
Job Parsing (text)25-40 seconds5 minutes
Job Parsing (file)30-55 seconds5 minutes

Key TypeScript Types Summary

// Common
type PipelineStatus = "processing" | "completed" | "failed"

// CV Extraction
type SkillProficiency =
| "NONE"
| "BASIC"
| "INTERMEDIATE"
| "ADVANCED"
| "EXPERT"
type LanguageProficiency =
| "BEGINNER"
| "ELEMENTARY"
| "INTERMEDIATE"
| "ADVANCED"
| "FLUENT"
| "NATIVE"
type WorkType = "REMOTE" | "ONSITE" | "HYBRID" | "UNSPECIFIED"

// Job Parsing
type JobType = "FREELANCE_GIG" | "INTERNSHIP" | "TRIAL_AND_HIRE" | "DIRECT_HIRE"
type WorkSite = "NOT_AVAILABLE" | "HYBRID" | "OFFSHORE" | "ONSITE" | "REMOTE"
type JobSkillProficiency =
| "NONE"
| "BASIC"
| "INTERMEDIATE"
| "ADVANCED"
| "EXPERT"