AI Risk assessment in CareerExplorer implemented

This commit is contained in:
Josh 2025-05-29 13:24:35 +00:00
parent c72e680f2b
commit 7a31b722c5
4 changed files with 238 additions and 174 deletions

View File

@ -585,7 +585,7 @@ app.post('/api/premium/milestone/convert-ai', authenticatePremiumUser, async (re
});
/***************************************************
AI CAREER RISK ANALYSIS ENDPOINT
AI CAREER RISK ANALYSIS ENDPOINTS
****************************************************/
// server3.js
app.post('/api/premium/ai-risk-analysis', authenticatePremiumUser, async (req, res) => {
@ -671,6 +671,66 @@ app.post('/api/premium/ai-risk-analysis', authenticatePremiumUser, async (req, r
}
});
app.post('/api/public/ai-risk-analysis', async (req, res) => {
try {
const {
socCode,
careerName,
jobDescription,
tasks = []
} = req.body;
if (!socCode || !careerName) {
return res.status(400).json({ error: 'socCode and careerName are required.' });
}
const prompt = `
The user has a career named: ${careerName}
Description: ${jobDescription}
Tasks: ${tasks.join('; ')}
Provide AI automation risk analysis for the next 10 years.
Return JSON exactly in this format:
{
"riskLevel": "Low|Moderate|High",
"reasoning": "Short explanation (< 50 words)."
}
`;
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [{ role: 'user', content: prompt }],
temperature: 0.3,
max_tokens: 200,
});
const aiText = completion?.choices?.[0]?.message?.content?.trim() || '';
let parsed;
try {
parsed = JSON.parse(aiText);
} catch (err) {
console.error('Error parsing AI JSON:', err);
return res.status(500).json({ error: 'Invalid AI JSON response.' });
}
const { riskLevel, reasoning } = parsed;
res.json({
socCode,
careerName,
jobDescription,
tasks,
riskLevel,
reasoning
});
} catch (err) {
console.error('Error in public AI risk analysis:', err);
res.status(500).json({ error: 'AI risk analysis failed.' });
}
});
/* ------------------------------------------------------------------
MILESTONE ENDPOINTS
------------------------------------------------------------------ */

View File

@ -285,159 +285,186 @@ function CareerExplorer() {
// ------------------------------------------------------
// handleCareerClick (detail fetch for CIP, Salary, etc.)
// ------------------------------------------------------
const handleCareerClick = useCallback(
async (career) => {
console.log('[handleCareerClick] career =>', career);
const socCode = career.code;
setSelectedCareer(career);
setError(null);
setCareerDetails(null);
setSalaryData([]);
setEconomicProjections({});
const handleCareerClick = useCallback(
async (career) => {
console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2));
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
setLoading(false);
return;
}
const socCode = career.code;
setSelectedCareer(career);
setError(null);
setCareerDetails(null);
setSalaryData([]);
setEconomicProjections({});
setLoading(true);
try {
// CIP fetch
const cipResponse = await fetch(`${apiUrl}/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;
}
const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
if (!socCode) {
console.error('SOC Code is missing');
setError('SOC Code is missing');
setLoading(false);
return;
}
// Job details
const jobDetailsResponse = await fetch(
`${apiUrl}/onet/career-description/${socCode}`
try {
// 1) CIP fetch
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
if (!cipResponse.ok) {
setError(
`We're sorry, but specific details for "${career.title}" are not available at this time.`
);
if (!jobDetailsResponse.ok) {
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
setLoading(false);
return;
}
const { description, tasks } = await jobDetailsResponse.json();
// Salary
let salaryResponse;
try {
salaryResponse = await axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) {
salaryResponse = { 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,
},
]
: [];
// Economic
const fullStateName = getFullStateName(userState);
let economicResponse = { data: {} };
try {
economicResponse = await axios.get(
`${apiUrl}/projections/${socCode.split('.')[0]}`,
{
params: { state: fullStateName },
}
);
} catch (error) {
economicResponse = { data: {} };
}
// Build final details
const updatedCareerDetails = {
...career,
jobDescription: description,
tasks,
salaryData: salaryDataPoints,
economicProjections: economicResponse.data || {},
};
setCareerDetails(updatedCareerDetails);
} catch (error) {
console.error('Error processing career click:', error.message);
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
} finally {
setLoading(false);
return;
}
},
[userState, apiUrl, areaTitle]
);
const { cipCode } = await cipResponse.json();
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
// 2) Job details (description + tasks)
const jobDetailsResponse = await fetch(
`${apiUrl}/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;
}
const { description, tasks } = await jobDetailsResponse.json();
// 3) Salary data
let salaryResponse;
try {
salaryResponse = await axios.get(`${apiUrl}/salary`, {
params: { socCode: socCode.split('.')[0], area: areaTitle },
});
} catch (error) {
salaryResponse = { 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,
},
]
: [];
// 4) Economic Projections
const fullStateName = getFullStateName(userState); // your helper
let economicResponse = { data: {} };
try {
economicResponse = await axios.get(
`${apiUrl}/projections/${socCode.split('.')[0]}`,
{
params: { state: fullStateName },
}
);
} catch (error) {
economicResponse = { data: {} };
}
// ----------------------------------------------------
// 5) AI RISK ANALYSIS LOGIC
// Attempt to retrieve from server2 first;
// if not found => call server3 => store in server2.
// ----------------------------------------------------
let aiRisk = null;
const strippedSoc = socCode.split('.')[0];
try {
// Check local DB first (SQLite -> server2)
const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${strippedSoc}`);
aiRisk = localRiskRes.data;
} catch (err) {
// If 404, we call server3's ChatGPT route at the SAME base url
if (err.response && err.response.status === 404) {
try {
const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, {
socCode: strippedSoc,
careerName: career.title,
jobDescription: description,
tasks,
});
const { riskLevel, reasoning } = aiRes.data;
// store it back in server2 to avoid repeated GPT calls
await axios.post(`${apiUrl}/ai-risk`, {
socCode: strippedSoc,
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: strippedSoc,
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
const updatedCareerDetails = {
...career,
jobDescription: description,
tasks,
salaryData: salaryDataPoints,
economicProjections: economicResponse.data || {},
aiRisk, // <--- Now we have it attached
};
setCareerDetails(updatedCareerDetails);
} catch (error) {
console.error('Error processing career click:', error.message);
setCareerDetails({
error: `We're sorry, but detailed info for "${career.title}" isn't available right now.`,
});
} finally {
setLoading(false);
}
},
[userState, apiUrl, areaTitle]
);
// ------------------------------------------------------
// handleCareerFromSearch

View File

@ -3,37 +3,12 @@ import axios from 'axios';
const apiUrl = process.env.REACT_APP_API_URL;
function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
const [error, setError] = useState(null);
const [aiRisk, setAiRisk] = useState(null); // <-- store AI risk data
const [loadingRisk, setLoadingRisk] = useState(false);
const aiRisk = careerDetails?.aiRisk || null;
// Fetch AI risk whenever we have a valid SOC code from `career.code`
useEffect(() => {
if (!career?.code) return;
// e.g. "15-1231.00" => strip down to "15-1231" if needed
const strippedSoc = career.code.split('.')[0];
setLoadingRisk(true);
axios
.get(`${apiUrl}/ai-risk/${strippedSoc}`)
.then((res) => {
setAiRisk(res.data);
})
.catch((err) => {
if (err?.response?.status === 404) {
// no AI risk data
setAiRisk(null);
} else {
console.error('Error fetching AI risk:', err);
setError('Failed to load AI risk analysis.');
}
})
.finally(() => {
setLoadingRisk(false);
});
}, [career, apiUrl]);
console.log('CareerModal props:', { career, careerDetails, aiRisk });
@ -76,6 +51,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
return 1;
};
return (
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50">
<div className="bg-white rounded-lg shadow-lg w-full max-w-5xl p-6 m-4 max-h-[90vh] overflow-y-auto">
@ -86,10 +62,11 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
<h2 className="text-2xl font-bold text-blue-600">
{careerDetails.title}
</h2>
{/* AI RISK SECTION */}
{loadingRisk ? (
<p className="text-sm text-gray-500 mt-1">Loading AI risk...</p>
) : aiRisk ? (
) : aiRisk?.riskLevel && aiRisk?.reasoning ? (
<div className="text-sm text-gray-500 mt-1">
<strong>AI Risk Level:</strong> {aiRisk.riskLevel}
<br />

Binary file not shown.