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
|
||||
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
|
||||
------------------------------------------------------------------ */
|
||||
|
@ -287,13 +287,15 @@ function CareerExplorer() {
|
||||
// ------------------------------------------------------
|
||||
const handleCareerClick = useCallback(
|
||||
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({});
|
||||
setLoading(true);
|
||||
|
||||
if (!socCode) {
|
||||
console.error('SOC Code is missing');
|
||||
@ -303,7 +305,7 @@ function CareerExplorer() {
|
||||
}
|
||||
|
||||
try {
|
||||
// CIP fetch
|
||||
// 1) CIP fetch
|
||||
const cipResponse = await fetch(`${apiUrl}/cip/${socCode}`);
|
||||
if (!cipResponse.ok) {
|
||||
setError(
|
||||
@ -318,7 +320,7 @@ function CareerExplorer() {
|
||||
const { cipCode } = await cipResponse.json();
|
||||
const cleanedCipCode = cipCode.replace('.', '').slice(0, 4);
|
||||
|
||||
// Job details
|
||||
// 2) Job details (description + tasks)
|
||||
const jobDetailsResponse = await fetch(
|
||||
`${apiUrl}/onet/career-description/${socCode}`
|
||||
);
|
||||
@ -331,7 +333,7 @@ function CareerExplorer() {
|
||||
}
|
||||
const { description, tasks } = await jobDetailsResponse.json();
|
||||
|
||||
// Salary
|
||||
// 3) Salary data
|
||||
let salaryResponse;
|
||||
try {
|
||||
salaryResponse = await axios.get(`${apiUrl}/salary`, {
|
||||
@ -347,64 +349,34 @@ function CareerExplorer() {
|
||||
? [
|
||||
{
|
||||
percentile: '10th Percentile',
|
||||
regionalSalary: parseInt(
|
||||
sData.regional?.regional_PCT10,
|
||||
10
|
||||
) || 0,
|
||||
nationalSalary: parseInt(
|
||||
sData.national?.national_PCT10,
|
||||
10
|
||||
) || 0,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
regionalSalary: parseInt(sData.regional?.regional_PCT90, 10) || 0,
|
||||
nationalSalary: parseInt(sData.national?.national_PCT90, 10) || 0,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
// Economic
|
||||
const fullStateName = getFullStateName(userState);
|
||||
// 4) Economic Projections
|
||||
const fullStateName = getFullStateName(userState); // your helper
|
||||
let economicResponse = { data: {} };
|
||||
try {
|
||||
economicResponse = await axios.get(
|
||||
@ -417,16 +389,71 @@ function CareerExplorer() {
|
||||
economicResponse = { data: {} };
|
||||
}
|
||||
|
||||
// Build final details
|
||||
// ----------------------------------------------------
|
||||
// 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({
|
||||
@ -437,7 +464,7 @@ function CareerExplorer() {
|
||||
}
|
||||
},
|
||||
[userState, apiUrl, areaTitle]
|
||||
);
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// handleCareerFromSearch
|
||||
|
@ -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 />
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user