AI Risk assessment in CareerExplorer implemented
This commit is contained in:
parent
c72e680f2b
commit
7a31b722c5
@ -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
|
// server3.js
|
||||||
app.post('/api/premium/ai-risk-analysis', authenticatePremiumUser, async (req, res) => {
|
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
|
MILESTONE ENDPOINTS
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
@ -285,159 +285,186 @@ function CareerExplorer() {
|
|||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// handleCareerClick (detail fetch for CIP, Salary, etc.)
|
// handleCareerClick (detail fetch for CIP, Salary, etc.)
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
const handleCareerClick = useCallback(
|
const handleCareerClick = useCallback(
|
||||||
async (career) => {
|
async (career) => {
|
||||||
console.log('[handleCareerClick] career =>', career);
|
console.log('[handleCareerClick] career object:', JSON.stringify(career, null, 2));
|
||||||
const socCode = career.code;
|
|
||||||
setSelectedCareer(career);
|
|
||||||
setError(null);
|
|
||||||
setCareerDetails(null);
|
|
||||||
setSalaryData([]);
|
|
||||||
setEconomicProjections({});
|
|
||||||
|
|
||||||
if (!socCode) {
|
const socCode = career.code;
|
||||||
console.error('SOC Code is missing');
|
setSelectedCareer(career);
|
||||||
setError('SOC Code is missing');
|
setError(null);
|
||||||
setLoading(false);
|
setCareerDetails(null);
|
||||||
return;
|
setSalaryData([]);
|
||||||
}
|
setEconomicProjections({});
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
if (!socCode) {
|
||||||
// CIP fetch
|
console.error('SOC Code is missing');
|
||||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
setError('SOC Code is missing');
|
||||||
if (!cipResponse.ok) {
|
setLoading(false);
|
||||||
setError(
|
return;
|
||||||
`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);
|
|
||||||
|
|
||||||
// Job details
|
try {
|
||||||
const jobDetailsResponse = await fetch(
|
// 1) CIP fetch
|
||||||
`${apiUrl}/onet/career-description/${socCode}`
|
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({
|
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.`,
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
const { cipCode } = await cipResponse.json();
|
||||||
[userState, apiUrl, areaTitle]
|
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
|
// handleCareerFromSearch
|
||||||
|
@ -3,37 +3,12 @@ import axios from 'axios';
|
|||||||
|
|
||||||
const apiUrl = process.env.REACT_APP_API_URL;
|
const apiUrl = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
|
|
||||||
function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [aiRisk, setAiRisk] = useState(null); // <-- store AI risk data
|
|
||||||
const [loadingRisk, setLoadingRisk] = useState(false);
|
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 });
|
console.log('CareerModal props:', { career, careerDetails, aiRisk });
|
||||||
|
|
||||||
@ -76,6 +51,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
|
|||||||
return 1;
|
return 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-900 bg-opacity-70 flex justify-center items-center overflow-auto z-50">
|
<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">
|
<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">
|
<h2 className="text-2xl font-bold text-blue-600">
|
||||||
{careerDetails.title}
|
{careerDetails.title}
|
||||||
</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 ? (
|
) : 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 />
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user