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

View File

@ -11,23 +11,36 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
const aiRisk = careerDetails?.aiRisk || null; const aiRisk = careerDetails?.aiRisk || null;
console.log('CareerModal props:', { career, careerDetails, aiRisk }); if (!careerDetails) {
// Handle your normal careerDetails loading logic
if (careerDetails?.error) {
return ( return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50"> <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"> <div className="bg-white rounded-lg shadow-lg p-6">
<p className="text-lg text-gray-700 mb-4">{careerDetails.error}</p> <p className="text-lg text-gray-700">Loading career details</p>
<button onClick={closeModal} className="bg-red-500 text-white p-2 rounded">
Close
</button>
</div> </div>
</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 ( return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center z-50"> <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"> <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 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 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 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 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 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! 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> </h2>
{/* AI RISK SECTION */} {/* AI RISK SECTION */}
{loadingRisk ? ( {loadingRisk && (
<p className="text-sm text-gray-500 mt-1">Loading AI risk...</p> <p className="text-sm text-gray-500 mt-1">Loading AI risk</p>
) : aiRisk?.riskLevel && aiRisk?.reasoning ? ( )}
{!loadingRisk && aiRisk && aiRisk.riskLevel && aiRisk.reasoning && (
<div className="text-sm text-gray-500 mt-1"> <div className="text-sm text-gray-500 mt-1">
<strong>AI Risk Level:</strong> {aiRisk.riskLevel} <strong>AI Risk Level:</strong> {aiRisk.riskLevel}
<br /> <br />
<span>{aiRisk.reasoning}</span> <span>{aiRisk.reasoning}</span>
</div> </div>
) : ( )}
<p className="text-sm text-gray-500 mt-1">
No AI risk data available {!loadingRisk && !aiRisk && (
</p> <p className="text-sm text-gray-500 mt-1">No AI risk data available</p>
)} )}
</div> </div>
@ -126,12 +141,15 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</div> </div>
{/* Job Description */} {/* Job Description */}
{careerDetails.jobDescription && (
<div className="mb-4"> <div className="mb-4">
<h3 className="text-lg font-semibold mb-1">Job Description:</h3> <h3 className="text-lg font-semibold mb-1">Job Description:</h3>
<p className="text-gray-700">{careerDetails.jobDescription}</p> <p className="text-gray-700">{careerDetails.jobDescription}</p>
</div> </div>
)}
{/* Tasks */} {/* Tasks */}
{careerDetails.tasks?.length > 0 && (
<div className="mb-4 border-t pt-3"> <div className="mb-4 border-t pt-3">
<h3 className="text-lg font-semibold mb-2">Tasks:</h3> <h3 className="text-lg font-semibold mb-2">Tasks:</h3>
<ul className="list-disc pl-5 space-y-1"> <ul className="list-disc pl-5 space-y-1">
@ -140,12 +158,22 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
))} ))}
</ul> </ul>
</div> </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"> <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"> <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"> <table className="w-full text-left border border-gray-300 rounded">
<thead className="bg-gray-100"> <thead className="bg-gray-100">
<tr> <tr>
@ -155,22 +183,25 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{careerDetails.salaryData.map((s, i) => ( {careerDetails.salaryData.map((row, i) => (
<tr key={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"> <td className="px-3 py-2 border-b">
${s.regionalSalary.toLocaleString()} ${row.regionalSalary.toLocaleString()}
</td> </td>
<td className="px-3 py-2 border-b"> <td className="px-3 py-2 border-b">
${s.nationalSalary.toLocaleString()} ${row.nationalSalary.toLocaleString()}
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
)}
{/* Economic Projections */} {/* ── Economic projections ───────────────── */}
{(careerDetails.economicProjections?.state ||
careerDetails.economicProjections?.national) && (
<div className="md:w-1/2 overflow-x-auto"> <div className="md:w-1/2 overflow-x-auto">
<h3 className="text-lg font-semibold mb-2">Economic Projections</h3> <h3 className="text-lg font-semibold mb-2">Economic Projections</h3>
<table className="w-full text-left border border-gray-300 rounded"> <table className="w-full text-left border border-gray-300 rounded">
@ -189,7 +220,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</thead> </thead>
<tbody> <tbody>
<tr> <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 && ( {careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b"> <td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.base.toLocaleString()} {careerDetails.economicProjections.state.base.toLocaleString()}
@ -202,7 +235,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)} )}
</tr> </tr>
<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 && ( {careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b"> <td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.projection.toLocaleString()} {careerDetails.economicProjections.state.projection.toLocaleString()}
@ -215,7 +250,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)} )}
</tr> </tr>
<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 && ( {careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b"> <td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.percentChange}% {careerDetails.economicProjections.state.percentChange}%
@ -228,7 +263,9 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
)} )}
</tr> </tr>
<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 && ( {careerDetails.economicProjections.state && (
<td className="px-3 py-2 border-b"> <td className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.annualOpenings.toLocaleString()} {careerDetails.economicProjections.state.annualOpenings.toLocaleString()}
@ -244,18 +281,19 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</table> </table>
{/* Conditional disclaimer when AI risk is Moderate or High */} {/* 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"> <p className="text-sm text-red-600 mt-2">
Note: These 10-year projections may change if AI-driven Note: These 10year projections may change if AIdriven tools
tools significantly affect {careerDetails.title} tasks. significantly affect {careerDetails.title} tasks. With a&nbsp;
With a <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk, <strong>{aiRisk.riskLevel.toLowerCase()}</strong> AI risk, its possible
its possible that some tasks or responsibilities could be automated some responsibilities could be automated over time.
over time.
</p> </p>
)} )}
</div> </div>
)}
</div> </div>
)}
</div> </div>
</div> </div>
); );