Halfway through adding AI Risk Analysis. GPT has completely forgotten what we're doing.

This commit is contained in:
Josh 2025-05-29 11:52:07 +00:00
parent 6298eedaba
commit c72e680f2b
4 changed files with 292 additions and 51 deletions

View File

@ -157,6 +157,47 @@ try {
console.error('Error reading economicproj.json:', err); console.error('Error reading economicproj.json:', err);
} }
//AI At Risk helpers
async function getRiskAnalysisFromDB(socCode) {
const row = await userProfileDb.get(
`SELECT * FROM ai_risk_analysis WHERE soc_code = ?`,
[socCode]
);
return row || null;
}
// Helper to upsert a row
async function storeRiskAnalysisInDB({
socCode,
careerName,
jobDescription,
tasks,
riskLevel,
reasoning,
}) {
// We'll use INSERT OR REPLACE so that if a row with the same soc_code
// already exists, it gets replaced (acts like an upsert).
const sql = `
INSERT OR REPLACE INTO ai_risk_analysis (
soc_code,
career_name,
job_description,
tasks,
risk_level,
reasoning,
created_at
) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`;
await userProfileDb.run(sql, [
socCode,
careerName || '',
jobDescription || '',
tasks || '',
riskLevel || '',
reasoning || '',
]);
}
/************************************************** /**************************************************
* O*Net routes, CIP routes, distance routes, etc. * O*Net routes, CIP routes, distance routes, etc.
**************************************************/ **************************************************/
@ -735,6 +776,68 @@ app.get('/api/user-profile/:id', (req, res) => {
}); });
}); });
/***************************************************
* AI RISK ASSESSMENT ENDPOINT READ
****************************************************/
app.get('/api/ai-risk/:socCode', async (req, res) => {
const { socCode } = req.params;
try {
const row = await getRiskAnalysisFromDB(socCode);
if (!row) {
return res.status(404).json({ error: 'Not found' });
}
// Return full data or partial, up to you:
res.json({
socCode: row.soc_code,
careerName: row.career_name,
jobDescription: row.job_description,
tasks: row.tasks,
riskLevel: row.risk_level,
reasoning: row.reasoning,
created_at: row.created_at,
});
} catch (err) {
console.error('Error fetching AI risk:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
/***************************************************
* AI RISK ASSESSMENT ENDPOINT WRITE
****************************************************/
app.post('/api/ai-risk', async (req, res) => {
try {
const {
socCode,
careerName,
jobDescription,
tasks,
riskLevel,
reasoning,
} = req.body;
if (!socCode) {
return res.status(400).json({ error: 'socCode is required' });
}
// Store / upsert row
await storeRiskAnalysisInDB({
socCode,
careerName,
jobDescription,
tasks,
riskLevel,
reasoning,
});
res.status(201).json({ message: 'AI Risk Analysis stored successfully' });
} catch (err) {
console.error('Error storing AI risk data:', err);
res.status(500).json({ error: 'Failed to store AI risk data.' });
}
});
/************************************************** /**************************************************
* Start the Express server * Start the Express server
**************************************************/ **************************************************/

View File

@ -584,6 +584,92 @@ app.post('/api/premium/milestone/convert-ai', authenticatePremiumUser, async (re
} }
}); });
/***************************************************
AI CAREER RISK ANALYSIS ENDPOINT
****************************************************/
// server3.js
app.post('/api/premium/ai-risk-analysis', authenticatePremiumUser, async (req, res) => {
try {
const {
socCode,
careerName,
jobDescription,
tasks = []
} = req.body;
if (!socCode) {
return res.status(400).json({ error: 'socCode is required.' });
}
// 1) Check if we already have it
const cached = await getCachedRiskAnalysis(socCode);
if (cached) {
return res.json({
socCode: cached.soc_code,
careerName: cached.career_name,
jobDescription: cached.job_description,
tasks: cached.tasks ? JSON.parse(cached.tasks) : [],
riskLevel: cached.risk_level,
reasoning: cached.reasoning
});
}
// 2) If missing, call GPT-3.5 to generate analysis
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 in exactly this format:
{
"riskLevel": "Low|Moderate|High",
"reasoning": "Short explanation (< 50 words)."
}
`;
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;
// 3) Store in DB
await cacheRiskAnalysis({
socCode,
careerName,
jobDescription,
tasks,
riskLevel,
reasoning
});
// 4) Return the new analysis
res.json({
socCode,
careerName,
jobDescription,
tasks,
riskLevel,
reasoning
});
} catch (err) {
console.error('Error in /api/premium/ai-risk-analysis:', err);
res.status(500).json({ error: 'Failed to generate AI risk analysis.' });
}
});
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------
MILESTONE ENDPOINTS MILESTONE ENDPOINTS

View File

@ -4,11 +4,40 @@ 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);
console.log('CareerModal props:', { career, careerDetails}); // 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 });
// Handle your normal careerDetails loading logic
if (careerDetails?.error) { 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">
@ -30,12 +59,15 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</div> </div>
</div> </div>
); );
} }
if (error) return <div>{error}</div>; if (error) return <div>{error}</div>;
// Helper for "stability" rating
const calculateStabilityRating = (salaryData) => { const calculateStabilityRating = (salaryData) => {
const medianSalaryObj = salaryData.find(s => s.percentile === 'Median'); const medianSalaryObj = salaryData.find((s) => s.percentile === 'Median');
const medianSalary = medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0; const medianSalary =
medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0;
if (medianSalary >= 90000) return 5; if (medianSalary >= 90000) return 5;
if (medianSalary >= 70000) return 4; if (medianSalary >= 70000) return 4;
@ -44,22 +76,44 @@ 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">
{/* Title row */}
<div className="flex justify-between items-center mb-4 pb-2 border-b"> <div className="flex justify-between items-center mb-4 pb-2 border-b">
<h2 className="text-2xl font-bold text-blue-600">{careerDetails.title}</h2> <div>
<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 ? (
<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>
)}
</div>
{/* Buttons */}
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => { onClick={() => {
const stabilityRating = calculateStabilityRating(careerDetails.salaryData); const stabilityRating = calculateStabilityRating(
careerDetails.salaryData
);
addCareerToList({ addCareerToList({
...careerDetails, ...careerDetails,
ratings: { ratings: {
stability: stabilityRating stability: stabilityRating,
} },
}); });
}} }}
className="text-white bg-green-500 hover:bg-green-600 rounded px-3 py-1" className="text-white bg-green-500 hover:bg-green-600 rounded px-3 py-1"
@ -67,7 +121,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
Add to Comparison Add to Comparison
</button> </button>
<button <button
onClick={closeModal} onClick={closeModal}
className="text-white bg-red-500 hover:bg-red-600 rounded px-3 py-1" className="text-white bg-red-500 hover:bg-red-600 rounded px-3 py-1"
@ -83,7 +136,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
<p className="text-gray-700">{careerDetails.jobDescription}</p> <p className="text-gray-700">{careerDetails.jobDescription}</p>
</div> </div>
{/* Tasks (full width) */} {/* Tasks */}
<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">
@ -93,7 +146,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</ul> </ul>
</div> </div>
{/* Salary and Economic Projections side-by-side */} {/* 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 Data */}
<div className="md:w-1/2 overflow-x-auto"> <div className="md:w-1/2 overflow-x-auto">
@ -110,8 +163,12 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
{careerDetails.salaryData.map((s, i) => ( {careerDetails.salaryData.map((s, 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">{s.percentile}</td>
<td className="px-3 py-2 border-b">${s.regionalSalary.toLocaleString()}</td> <td className="px-3 py-2 border-b">
<td className="px-3 py-2 border-b">${s.nationalSalary.toLocaleString()}</td> ${s.regionalSalary.toLocaleString()}
</td>
<td className="px-3 py-2 border-b">
${s.nationalSalary.toLocaleString()}
</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -121,27 +178,25 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
{/* Economic Projections */} {/* Economic Projections */}
<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">
<thead className="bg-gray-100"> <thead className="bg-gray-100">
<tr> <tr>
<th className="px-3 py-2 border-b"></th> <th className="px-3 py-2 border-b"></th>
{/* If we have state data, show a column for it */}
{careerDetails.economicProjections.state && ( {careerDetails.economicProjections.state && (
<th className="px-3 py-2 border-b"> <th className="px-3 py-2 border-b">
{careerDetails.economicProjections.state.area} {careerDetails.economicProjections.state.area}
</th> </th>
)} )}
{/* If we have national data, show a column for it */}
{careerDetails.economicProjections.national && ( {careerDetails.economicProjections.national && (
<th className="px-3 py-2 border-b">National</th> <th className="px-3 py-2 border-b">National</th>
)} )}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{/* Row for Current Jobs */}
<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()}
@ -153,10 +208,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</td> </td>
)} )}
</tr> </tr>
{/* Row for Jobs in 10 yrs */}
<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 10 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()}
@ -168,8 +223,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</td> </td>
)} )}
</tr> </tr>
{/* Row for Growth % */}
<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 && (
@ -183,10 +236,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</td> </td>
)} )}
</tr> </tr>
{/* Row for Annual Openings */}
<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()}
@ -201,7 +254,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

Binary file not shown.