diff --git a/lib/services/jobRecommendation.ts b/lib/services/jobRecommendation.ts index e9f6e41..9b373ba 100644 --- a/lib/services/jobRecommendation.ts +++ b/lib/services/jobRecommendation.ts @@ -8,13 +8,14 @@ import type { ResumeProfile } from "@/lib/types"; export interface MCFJob { uuid: string; title: string; - company: { name: string }; + postedCompany: { name: string } | null; + company?: { name: string } | null; salary: { minimum: number | null; maximum: number | null }; - employmentTypes: { jobType: string }[]; + employmentTypes: { employmentType: string }[]; categories: { category: string }[]; skills: { skill: string }[]; positionLevels: { position: string }[]; - metadata: { jobPostId: string; originalPostUrl?: string }; + metadata: { jobPostId: string; jobDetailsUrl?: string; originalPostUrl?: string }; description?: string; } @@ -108,16 +109,18 @@ For each job return: - improvements: 2–3 bullet points on gaps to address - reasoning: 1–2 sentence overall reasoning -Return ONLY a JSON array in this exact shape, no extra text: -[ - { - "id": "", - "matchScore": 85, - "strengths": ["...", "..."], - "improvements": ["...", "..."], - "reasoning": "..." - } -] +Return ONLY a JSON object in this exact shape, no extra text: +{ + "matches": [ + { + "id": "", + "matchScore": 85, + "strengths": ["...", "..."], + "improvements": ["...", "..."], + "reasoning": "..." + } + ] +} Consider: skills overlap, seniority level, industry match, years of experience, target roles. Singapore job market context applies.`; @@ -151,7 +154,7 @@ export async function scoreJobsWithAI( const jobSummaries = jobs.map((j) => ({ id: j.uuid, title: j.title, - company: j.company?.name, + company: j.postedCompany?.name ?? j.company?.name ?? "Unknown", skills: j.skills?.map((s) => s.skill).slice(0, 10), categories: j.categories?.map((c) => c.category), positionLevel: j.positionLevels?.[0]?.position ?? null, @@ -172,10 +175,7 @@ export async function scoreJobsWithAI( const raw = response.choices[0]?.message?.content ?? "{}"; try { const parsed = JSON.parse(raw); - // Handle both array and wrapped object responses - const results: MatchResult[] = Array.isArray(parsed) - ? parsed - : (parsed.results ?? parsed.jobs ?? []); + const results: MatchResult[] = parsed.matches ?? parsed.results ?? parsed.jobs ?? []; return results; } catch { return []; @@ -197,20 +197,17 @@ export async function getJobRecommendations( const recommendations: JobRecommendation[] = jobs.map((job) => { const score = scoreMap.get(job.uuid); - const applyUrl = - job.metadata?.originalPostUrl ?? - `https://www.mycareersfuture.gov.sg/job/${job.metadata?.jobPostId ?? job.uuid}`; return { id: job.uuid, title: job.title, - company: job.company?.name ?? "Unknown", + company: job.postedCompany?.name ?? job.company?.name ?? "Unknown", matchScore: score?.matchScore ?? 50, salaryMin: job.salary?.minimum ?? null, salaryMax: job.salary?.maximum ?? null, - employmentType: job.employmentTypes?.[0]?.jobType ?? null, + employmentType: job.employmentTypes?.[0]?.employmentType ?? null, skills: job.skills?.map((s) => s.skill).slice(0, 8) ?? [], - applyUrl, + applyUrl: job.metadata?.jobDetailsUrl ?? `https://www.mycareersfuture.gov.sg/job/${job.metadata?.jobPostId ?? job.uuid}`, strengths: score?.strengths ?? [], improvements: score?.improvements ?? [], reasoning: score?.reasoning ?? "This role matches your profile.", @@ -252,11 +249,15 @@ export async function extractJobFromUrl(url: string): Promise { // For MCF URLs, use the API directly instead of scraping if (source === "MyCareersFuture") { - const match = url.match(/JOB-[\w-]+/i); - if (match) { - const jobPostId = match[0]; + const jobMatch = url.match(/JOB-[\w-]+/i); + const uuidMatch = url.match(/([a-f0-9]{32})(?:[^a-f0-9]|$)/i); + const jobPostId = jobMatch?.[0] ?? null; + const uuid = uuidMatch?.[1] ?? null; + + if (jobPostId || uuid) { + const query = jobPostId ? `jobPostId=${jobPostId}` : `uuid=${uuid}`; const res = await fetch( - `${MCF_API_BASE}/jobs?jobPostId=${jobPostId}`, + `${MCF_API_BASE}/jobs?${query}`, { headers: { Accept: "application/json", @@ -270,13 +271,13 @@ export async function extractJobFromUrl(url: string): Promise { if (job) { return { title: job.title ?? null, - company: job.company?.name ?? null, + company: job.postedCompany?.name ?? job.company?.name ?? null, description: job.description ?? null, salary: job.salary?.minimum ? `$${job.salary.minimum.toLocaleString()} – $${job.salary.maximum?.toLocaleString() ?? "?"} / month` : null, location: "Singapore", - employmentType: job.employmentTypes?.[0]?.jobType ?? null, + employmentType: job.employmentTypes?.[0]?.employmentType ?? null, skills: job.skills?.map((s) => s.skill) ?? [], applyUrl: url, source: "MyCareersFuture", diff --git a/package-lock.json b/package-lock.json index 10fcbec..5f99903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1503,6 +1503,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz", "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.98.0", "@supabase/functions-js": "2.98.0", @@ -1584,6 +1585,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1666,6 +1668,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2254,6 +2257,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3620,6 +3624,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3789,6 +3794,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5447,6 +5453,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -6585,6 +6592,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6864,6 +6872,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6876,6 +6885,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8008,6 +8018,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8079,6 +8090,7 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -8216,6 +8228,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"