Fixed limited data behavior, All Others in CareerExplorer

This commit is contained in:
Josh 2025-08-05 15:18:54 +00:00
parent b5184f2a02
commit 13e898242c
3 changed files with 258 additions and 296 deletions

2
.env
View File

@ -2,4 +2,4 @@ CORS_ALLOWED_ORIGINS=https://dev1.aptivaai.com,http://34.16.120.118:3000,http://
SERVER1_PORT=5000
SERVER2_PORT=5001
SERVER3_PORT=5002
IMG_TAG=8449dc2-202508050250
IMG_TAG=6a57e00-202508051419

View File

@ -9,6 +9,7 @@ import InterestMeaningModal from './InterestMeaningModal.js';
import CareerSearch from './CareerSearch.js';
import { Button } from './ui/button.js';
import axios from 'axios';
import isAllOther from '../utils/isAllOther.js';
const STATES = [
{ name: 'Alabama', code: 'AL' }, { name: 'Alaska', code: 'AK' }, { name: 'Arizona', code: 'AZ' },
@ -338,181 +339,103 @@ function CareerExplorer() {
}
}, [location.state, userProfile, fetchSuggestions, navigate]);
// ------------------------------------------------------
// handleCareerClick (detail fetch for CIP, Salary, etc.)
// ------------------------------------------------------
const handleCareerClick = useCallback(
/* ------------------------------------------------------
handleCareerClick  fetches all details for one career
---------------------------------------------------- */
const handleCareerClick = useCallback(
async (career) => {
console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2));
const socCode = career.code;
if (!socCode) return;
setSelectedCareer(career);
setError(null);
setCareerDetails(null);
setSalaryData([]);
setEconomicProjections({});
setCareerDetails(null); // reset any previous modal
setLoading(true);
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
setLoading(false);
return;
}
try {
// 1) CIP fetch
const cipResponse = await fetch(`/api/cip/${socCode}`);
if (!cipResponse.ok) {
setError(
`We're sorry, but specific details for "${career.title}" are not available at this time.`
);
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
setLoading(false);
return;
/* ---------- 1. CIP lookup ---------- */
let cipCode = null;
const cipRes = await fetch(`/api/cip/${socCode}`);
if (cipRes.ok) {
cipCode = (await cipRes.json()).cipCode ?? null;
}
const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
// 2) Job details (description + tasks)
const jobDetailsResponse = await fetch(
`/api/onet/career-description/${socCode}`
);
if (!jobDetailsResponse.ok) {
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
setLoading(false);
return;
/* ---------- 2. Job description & tasks ---------- */
let description = '';
let tasks = [];
const jobRes = await fetch(`/api/onet/career-description/${socCode}`);
if (jobRes.ok) {
const jd = await jobRes.json();
description = jd.description ?? '';
tasks = jd.tasks ?? [];
}
const { description, tasks } = await jobDetailsResponse.json();
// 3) Salary data
let salaryResponse;
try {
salaryResponse = await axios.get('/api/salary', {
/* ---------- 3. Salary data ---------- */
const salaryRes = await axios.get('/api/salary', {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) {
salaryResponse = { data: {} };
}
}).catch(() => ({ data: {} }));
const sData = salaryResponse.data || {};
const salaryDataPoints =
sData && Object.keys(sData).length > 0
? [
{
percentile: '10th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT10, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT10, 10) || 0,
},
{
percentile: '25th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT25, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT25, 10) || 0,
},
{
percentile: 'Median',
regionalSalary: parseInt(sData.regional?.regional_MEDIAN, 10) || 0,
nationalSalary: parseInt(sData.national?.national_MEDIAN, 10) || 0,
},
{
percentile: '75th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT75, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT75, 10) || 0,
},
{
percentile: '90th Percentile',
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0,
},
]
: [];
const s = salaryRes.data;
const salaryDataPoints = s && Object.keys(s).length > 0 ? [
{ percentile: '10th Percentile', regionalSalary: +s.regional?.regional_PCT10 || 0, nationalSalary: +s.national?.national_PCT10 || 0 },
{ percentile: '25th Percentile', regionalSalary: +s.regional?.regional_PCT25 || 0, nationalSalary: +s.national?.national_PCT25 || 0 },
{ percentile: 'Median', regionalSalary: +s.regional?.regional_MEDIAN || 0, nationalSalary: +s.national?.national_MEDIAN || 0 },
{ percentile: '75th Percentile', regionalSalary: +s.regional?.regional_PCT75 || 0, nationalSalary: +s.national?.national_PCT75 || 0 },
{ percentile: '90th Percentile', regionalSalary: +s.regional?.regional_PCT90 || 0, nationalSalary: +s.national?.national_PCT90 || 0 },
] : [];
// 4) Economic Projections
const fullStateName = getFullStateName(userState); // your helper
let economicResponse = { data: {} };
try {
economicResponse = await axios.get(
/* ---------- 4. Economic projections ---------- */
const fullStateName = getFullStateName(userState);
const projRes = await axios.get(
`/api/projections/${socCode.split('.')[0]}`,
{
params: { state: fullStateName },
}
);
} catch (error) {
economicResponse = { data: {} };
{ params: { state: fullStateName } }
).catch(() => ({ data: {} }));
/* ---------- 5. Decide if we actually have data ---------- */
const haveSalary = salaryDataPoints.length > 0;
const haveProj = !!(projRes.data.state || projRes.data.national);
const haveJobInfo = description || tasks.length > 0;
if (!haveSalary && !haveProj && !haveJobInfo) {
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
return; // stops here nothing useful to show
}
// ----------------------------------------------------
// 5) AI RISK ANALYSIS LOGIC
// Attempt to retrieve from server2 first;
// if not found => call server3 => store in server2.
// ----------------------------------------------------
/* ---------- 6. AIrisk (only if job details exist) ---------- */
let aiRisk = null;
const strippedSocCode = socCode.split('.')[0];
if (haveJobInfo) {
try {
// Check local DB first (SQLite -> server2)
const localRiskRes = await axios.get(`/api/ai-risk/${socCode}`);
aiRisk = localRiskRes.data;
aiRisk = (await axios.get(`/api/ai-risk/${socCode}`)).data;
} catch (err) {
// If 404, we call server3's ChatGPT route at the SAME base url
if (err.response && err.response.status === 404) {
try {
if (err.response?.status === 404) {
const aiRes = await axios.post('/api/public/ai-risk-analysis', {
socCode,
careerName: career.title,
jobDescription: description,
careerName : career.title,
jobDescription : description,
tasks,
});
const { riskLevel, reasoning } = aiRes.data;
// store it back in server2 to avoid repeated GPT calls
await axios.post('/api/ai-risk', {
socCode,
careerName: aiRes.data.careerName,
jobDescription: aiRes.data.jobDescription,
tasks: aiRes.data.tasks,
riskLevel: aiRes.data.riskLevel,
reasoning: aiRes.data.reasoning,
});
// build final object
aiRisk = {
socCode,
careerName: career.title,
jobDescription: description,
tasks,
riskLevel,
reasoning,
};
} catch (err2) {
console.error('Error calling server3 or storing AI risk:', err2);
// fallback
aiRisk = aiRes.data;
// store for next time (besteffort)
axios.post('/api/ai-risk', aiRisk).catch(() => {});
}
} else {
console.error('Error fetching AI risk from server2:', err);
}
}
// 6) Build final details object
/* ---------- 7. Build the final object & show modal ---------- */
const updatedCareerDetails = {
...career,
cipCode, // may be null  EducationalPrograms handles it
jobDescription: description,
tasks,
salaryData: salaryDataPoints,
economicProjections: economicResponse.data || {},
aiRisk, // <--- Now we have it attached
economicProjections: projRes.data ?? {},
aiRisk,
};
setCareerDetails(updatedCareerDetails);
} catch (error) {
console.error('Error processing career click:', error.message);
} catch (e) {
console.error('[handleCareerClick] fatal:', e);
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
@ -520,9 +443,10 @@ function CareerExplorer() {
setLoading(false);
}
},
[userState, areaTitle]
[areaTitle, userState]
);
// ------------------------------------------------------
// handleCareerFromSearch
// ------------------------------------------------------
@ -1071,7 +995,7 @@ const handleSelectForEducation = (career) => {
{/* Legend container with less internal gap, plus a left margin */}
<div className="flex items-center gap-1 ml-4">
<span className="warning-icon"></span>
<span>= Limited Data for this career path</span>
<span>= May have limited data for this career path</span>
</div>
</div>

View File

@ -11,23 +11,36 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
const aiRisk = careerDetails?.aiRisk || null;
console.log('CareerModal props:', { career, careerDetails, aiRisk });
// Handle your normal careerDetails loading logic
if (careerDetails?.error) {
if (!careerDetails) {
return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6">
<p className="text-lg text-gray-700 mb-4">{careerDetails.error}</p>
<button onClick={closeModal} className="bg-red-500 text-white p-2 rounded">
Close
</button>
<p className="text-lg text-gray-700">Loading career details</p>
</div>
</div>
);
}
if (!careerDetails?.salaryData) {
// Handle your normal careerDetails loading logic
if (careerDetails?.error) {
return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6 max-w-xl">
<p className="text-lg text-gray-700 mb-4">
{careerDetails.error}
</p>
<button
onClick={closeModal}
className="bg-red-500 text-white p-2 rounded"
>
Close
</button>
</div>
</div>
);
}
if (!careerDetails?.salaryData === undefined) {
return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50">
<div className="bg-white rounded-lg shadow-lg p-6">
@ -65,7 +78,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
You've selected an "umbrella" field that covers a wide range of careersmany
people begin a career journey with a broad interest area and we don't want to discourage
anyone from taking this approach. It's just difficult to display detailed career data
and daytoday tasks for this allother occupatio.. Use it as a starting point,
and daytoday tasks for this allother occupation. Use it as a starting point,
keep exploring specializations, and we can show you richer insights as soon as you are able
to narrow it down to a more specific role. If you know this is the field for you, go ahead to
add it to your comparison list or move straight into Preparing & Upskilling for Your Career!
@ -81,18 +94,20 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</h2>
{/* AI RISK SECTION */}
{loadingRisk ? (
<p className="text-sm text-gray-500 mt-1">Loading AI risk...</p>
) : aiRisk?.riskLevel && aiRisk?.reasoning ? (
{loadingRisk && (
<p className="text-sm text-gray-500 mt-1">Loading AI risk</p>
)}
{!loadingRisk && aiRisk && aiRisk.riskLevel && aiRisk.reasoning && (
<div className="text-sm text-gray-500 mt-1">
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
<br />
<span>{aiRisk.reasoning}</span>
</div>
) : (
<p className="text-sm text-gray-500 mt-1">
No AI risk data available
</p>
)}
{!loadingRisk && !aiRisk && (
<p className="text-sm text-gray-500 mt-1">No AI risk data available</p>
)}
</div>
@ -126,12 +141,15 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</div>
{/* Job Description */}
{careerDetails.jobDescription && (
<div className="mb-4">
<h3 className="text-lg font-semibold mb-1">Job Description:</h3>
<p className="text-gray-700">{careerDetails.jobDescription}</p>
</div>
)}
{/* Tasks */}
{careerDetails.tasks?.length > 0 && (
<div className="mb-4 border-t pt-3">
<h3 className="text-lg font-semibold mb-2">Tasks:</h3>
<ul className="list-disc pl-5 space-y-1">
@ -140,12 +158,22 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
))}
</ul>
</div>
)}
{(careerDetails.salaryData?.length > 0 ||
(careerDetails.economicProjections &&
(careerDetails.economicProjections.state ||
careerDetails.economicProjections.national))) && (
{/* Salary & Projections side-by-side */}
<div className="flex flex-col md:flex-row gap-4 border-t pt-3">
{/* Salary Data */}
{/* ── Salary table ───────────────────────── */}
{careerDetails.salaryData?.length > 0 && (
<div className="md:w-1/2 overflow-x-auto">
<h3 className="text-lg font-semibold mb-2">Salary Data:</h3>
<h3 className="text-lg font-semibold mb-2">Salary Data</h3>
<table className="w-full text-left border border-gray-300 rounded">
<thead className="bg-gray-100">
<tr>
@ -155,22 +183,25 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</tr>
</thead>
<tbody>
{careerDetails.salaryData.map((s, i) => (
{careerDetails.salaryData.map((row, i) => (
<tr key={i}>
<td className="px-3 py-2 border-b">{s.percentile}</td>
<td className="px-3 py-2 border-b">{row.percentile}</td>
<td className="px-3 py-2 border-b">
${s.regionalSalary.toLocaleString()}
${row.regionalSalary.toLocaleString()}
</td>
<td className="px-3 py-2 border-b">
${s.nationalSalary.toLocaleString()}
${row.nationalSalary.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Economic Projections */}
{/* ── Economic projections ───────────────── */}
{(careerDetails.economicProjections?.state ||
careerDetails.economicProjections?.national) && (
<div className="md:w-1/2 overflow-x-auto">
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
<table className="w-full text-left border border-gray-300 rounded">
@ -189,7 +220,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</thead>
<tbody>
<tr>
<td className="px-3 py-2 border-b font-semibold">Current Jobs</td>
<td className="px-3 py-2 border-b font-semibold">
Current Jobs
</td>
{careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.base.toLocaleString()}
@ -202,7 +235,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)}
</tr>
<tr>
<td className="px-3 py-2 border-b font-semibold">Jobs in 10 yrs</td>
<td className="px-3 py-2 border-b font-semibold">
Jobs in&nbsp;10&nbsp;yrs
</td>
{careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.projection.toLocaleString()}
@ -215,7 +250,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)}
</tr>
<tr>
<td className="px-3 py-2 border-b font-semibold">Growth %</td>
<td className="px-3 py-2 border-b font-semibold">Growth%</td>
{careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.percentChange}%
@ -228,7 +263,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)}
</tr>
<tr>
<td className="px-3 py-2 border-b font-semibold">Annual Openings</td>
<td className="px-3 py-2 border-b font-semibold">
Annual Openings
</td>
{careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
@ -244,18 +281,19 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</table>
{/* Conditional disclaimer when AI risk is Moderate or High */}
{(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
{aiRisk?.riskLevel &&
(aiRisk.riskLevel === 'Moderate' || aiRisk.riskLevel === 'High') && (
<p className="text-sm text-red-600 mt-2">
Note: These 10-year projections may change if AI-driven
tools significantly affect {careerDetails.title} tasks.
With a <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk,
its possible that some tasks or responsibilities could be automated
over time.
Note: These 10year projections may change if AIdriven tools
significantly affect {careerDetails.title} tasks. With a&nbsp;
<strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk, its possible
some responsibilities could be automated over time.
</p>
)}
</div>
)}
</div>
)}
</div>
</div>
);