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
- Base URL
- CV Extraction API
- Job Parsing API
- Common Concepts
- Frontend Integration Examples
- Best Practices
Overview
The system provides two main document parsing pipelines:
- CV Extraction: Parse CV/resume documents (PDF, DOCX, images) to extract structured candidate data
- 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
| Environment | REST/GraphQL URL | WebSocket URL |
|---|---|---|
| Production | https://ai.aiqlick.com | wss://ai.aiqlick.com/graphql |
| Development | http://localhost:8080 | ws://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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | String | Yes | - | Unique identifier for this extraction request. Use UUID or similar. |
fileUrl | String | Yes | - | URL to the CV file. Can be MinIO URL or external URL. Supported formats: PDF, DOCX, PNG, JPG, JPEG |
sequential | Boolean | No | true | Process document pages sequentially. Recommended for better accuracy. |
schemaOverride | String | No | - | Custom extraction schema (JSON string). Advanced use only. |
CV Extraction Output Data Models
CVParsedData (Main Output)
| Field | Type | Nullable | Description |
|---|---|---|---|
contact | ContactInfo | No | Contact information (required) |
skills | [SkillItem] | No | List of all extracted skills |
experience | [ExperienceItem] | No | Work experience entries |
education | [EducationItem] | No | Educational qualifications |
languages | [LanguageItem] | No | Languages with proficiency levels |
certifications | [CertificationItem] | No | Professional certifications |
workMode | String | Yes | Preferred work arrangement: REMOTE, ONSITE, HYBRID, UNSPECIFIED |
introduction | String | Yes | Professional summary/objective |
cvUrl | String | Yes | URL to the original CV document |
ContactInfo
| Field | Type | Nullable | Description |
|---|---|---|---|
name | String | No | Full name of the candidate |
firstName | String | Yes | First name |
middleName | String | Yes | Middle name |
lastName | String | Yes | Last name |
email | String | No | Email address |
phone | String | Yes | Phone number |
linkedinUrl | String | Yes | LinkedIn profile URL |
profileLink | String | Yes | Other profile link (GitHub, portfolio) |
gender | String | Yes | Gender |
dateOfBirth | String | Yes | Date of birth (YYYY-MM-DD or MM/DD/YYYY) |
location | String | Yes | Location/address |
SkillItem
| Field | Type | Default | Description |
|---|---|---|---|
skill | String | - | Skill name (e.g., "Python", "Project Management") |
years | Int | 0 | Years of experience with this skill |
proficiency | SkillProficiency | BASIC | Proficiency level |
ExperienceItem
| Field | Type | Nullable | Description |
|---|---|---|---|
title | String | No | Job title |
company | String | No | Company name |
startDate | String | No | Start date (YYYY-MM or YYYY-MM-DD) |
endDate | String | Yes | End date (null = current position) |
description | String | Yes | Role description and achievements |
location | String | Yes | Work location |
workType | WorkType | No | REMOTE, ONSITE, HYBRID, UNSPECIFIED |
industry | String | Yes | Industry sector |
skills | [String] | No | Skills used in this role |
EducationItem
| Field | Type | Nullable | Description |
|---|---|---|---|
degree | String | No | Degree name (e.g., "Bachelor of Science") |
major | String | Yes | Major/field of study |
institution | String | Yes | School/university name |
startDate | String | Yes | Start date |
endDate | String | Yes | End date |
gradYear | Int | Yes | Graduation year |
gpa | Float | Yes | Grade point average |
LanguageItem
| Field | Type | Description |
|---|---|---|
name | String | Language name (e.g., "English", "Swedish") |
proficiency | LanguageProficiency | Proficiency level |
CertificationItem
| Field | Type | Nullable | Description |
|---|---|---|---|
name | String | No | Certification name |
institution | String | Yes | Issuing organization |
year | Int | Yes | Year obtained |
location | String | Yes | Location where obtained |
expiryDate | String | Yes | Expiration date |
credentialId | String | Yes | Credential ID/number |
CV Extraction Progress Updates
The subscription yields progress updates during processing:
| Progress | Message | Description |
|---|---|---|
| 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
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique identifier for this parsing request |
text | String | No* | Job description text content |
fileUrl | String | No* | URL to job description file (PDF, DOCX, images) |
extractionSchema | String | No | Custom extraction schema (JSON string). Advanced use only. |
Note: Either
textorfileUrlmust be provided. If both are provided,fileUrltakes precedence.
Job Parsing Output Data Models
JobExtractedData (Main Output)
| Field | Type | Nullable | Default | Description |
|---|---|---|---|---|
title | String | No | - | Job title |
description | String | Yes | - | Full job description |
jobType | JobType | No | - | Employment type |
workSite | WorkSite | No | - | Work location arrangement |
salary | Float | Yes | - | Salary amount |
currency | String | Yes | - | 3-letter currency code (USD, EUR, SEK) |
rateIn | Float | Yes | - | Input hourly/daily rate (for contractors) |
rateOut | Float | Yes | - | Output/billing rate (for contractors) |
startDate | String | Yes | - | Job start date (YYYY-MM-DD) |
dueDate | String | Yes | - | Application deadline (YYYY-MM-DD) |
endDate | String | Yes | - | Job end date for temporary positions (YYYY-MM-DD) |
priority | Boolean | No | false | High priority position flag |
publishedOnline | Boolean | No | true | Publish on job boards |
skills | [JobSkillRequirement] | No | [] | Required skills with details |
languages | [String] | No | [] | Required languages |
requirements | String | Yes | - | Job requirements text |
responsibilities | String | Yes | - | Job responsibilities text |
benefits | String | Yes | - | Benefits offered |
companyName | String | Yes | - | Company name |
industry | String | Yes | - | Industry sector |
location | String | Yes | - | Job location |
JobSkillRequirement
| Field | Type | Nullable | Default | Description |
|---|---|---|---|---|
name | String | No | - | Skill name |
proficiency | JobSkillProficiency | No | INTERMEDIATE | Required proficiency level |
yearsExperience | Int | Yes | - | Years of experience required |
mustHave | Boolean | No | true | Whether skill is mandatory |
weight | Int | No | 10 | Skill importance weight (1-100) |
Job Parsing Progress Updates
| Progress | Message | Description |
|---|---|---|
| 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:
- MinIO URLs (recommended):
https://storage.aiqlick.com/aiqlick-uploads/path/to/file.pdf - 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
| Status | Description |
|---|---|
processing | Operation is currently in progress |
completed | Operation completed successfully |
failed | Operation failed with an error |
Error Types and Handling
PipelineError Structure
| Field | Type | Nullable | Description |
|---|---|---|---|
code | PipelineErrorType | No | Error type code |
message | String | No | Human-readable error message |
field | String | Yes | Field that caused error (dot notation for nested fields) |
context | JSON | Yes | Additional error context for debugging |
timestamp | Float | No | Unix timestamp when error occurred |
recoveryHint | String | Yes | Suggestion for recovery |
Error Codes Reference
Validation Errors:
| Code | Description | Common Cause |
|---|---|---|
VALIDATION_ERROR | Input validation failed | Invalid field values |
INVALID_INPUT | Input data is malformed | Missing required fields |
MISSING_FIELD | Required field is missing | Null required parameter |
INVALID_FORMAT | Field format is invalid | Wrong date format |
Processing Errors:
| Code | Description | Common Cause |
|---|---|---|
DOCUMENT_ERROR | Failed to process document | Corrupted file, unsupported format |
EXTRACTION_ERROR | AI extraction failed | LLM service issues |
AI_ERROR | LLM service error | Bedrock timeout or quota |
Infrastructure Errors:
| Code | Description | Common Cause |
|---|---|---|
DATABASE_ERROR | Database operation failed | Connection issues |
TIMEOUT_ERROR | Operation timed out | Processing took >5 minutes |
NETWORK_ERROR | Network communication error | File download failed |
STORAGE_ERROR | File storage operation failed | MinIO unavailable |
Resource Errors:
| Code | Description | Common Cause |
|---|---|---|
NOT_FOUND | Resource not found | Invalid file URL |
JOB_NOT_FOUND | Job ID doesn't exist | Invalid job reference |
USER_NOT_FOUND | User ID doesn't exist | Invalid user reference |
Enum Values Reference
SkillProficiency (CV Extraction)
| Value | Description |
|---|---|
NONE | No proficiency |
BASIC | Basic/beginner level |
INTERMEDIATE | Intermediate level |
ADVANCED | Advanced level |
EXPERT | Expert/mastery level |
JobSkillProficiency (Job Parsing)
| Value | Description |
|---|---|
NONE | Not required |
BASIC | Basic knowledge sufficient |
INTERMEDIATE | Working knowledge required |
ADVANCED | Deep expertise required |
EXPERT | Expert-level mastery required |
WorkType / WorkSite
| Value | Description |
|---|---|
REMOTE | Fully remote position |
ONSITE | On-site/in-office only |
HYBRID | Mix of remote and on-site |
UNSPECIFIED / NOT_AVAILABLE | Not specified |
OFFSHORE | Offshore location (Job Parsing only) |
JobType
| Value | Description |
|---|---|
FREELANCE_GIG | Freelance/contract work |
INTERNSHIP | Internship position |
TRIAL_AND_HIRE | Trial period before full hire |
DIRECT_HIRE | Direct permanent hire |
LanguageProficiency
| Value | Description |
|---|---|
BEGINNER | A1 level |
ELEMENTARY | A2 level |
INTERMEDIATE | B1-B2 level |
ADVANCED | C1 level |
FLUENT | C2 level |
NATIVE | Native 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 progressSuccess- Display resultsFailure- 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
| Feature | GraphQL Subscription | WebSocket URL |
|---|---|---|
| CV Extraction | extractCV | wss://ai.aiqlick.com/graphql |
| Job Parsing | parseJob | wss://ai.aiqlick.com/graphql |
Processing Times
| Operation | Typical Time | Timeout |
|---|---|---|
| CV Extraction | 30-60 seconds | 5 minutes |
| Job Parsing (text) | 25-40 seconds | 5 minutes |
| Job Parsing (file) | 30-55 seconds | 5 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"