From c72e680f2bb3207d3763c8ce9f2790ef571edd55 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 29 May 2025 11:52:07 +0000 Subject: [PATCH] Halfway through adding AI Risk Analysis. GPT has completely forgotten what we're doing. --- backend/server2.js | 103 +++++++++++++++++++++++ backend/server3.js | 86 +++++++++++++++++++ src/components/CareerModal.js | 154 +++++++++++++++++++++++----------- user_profile.db | Bin 126976 -> 135168 bytes 4 files changed, 292 insertions(+), 51 deletions(-) diff --git a/backend/server2.js b/backend/server2.js index 099ead4..25ca394 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -157,6 +157,47 @@ try { 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. **************************************************/ @@ -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 **************************************************/ diff --git a/backend/server3.js b/backend/server3.js index 6fc2311..32b6458 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -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 diff --git a/src/components/CareerModal.js b/src/components/CareerModal.js index 9c0ddb8..c065272 100644 --- a/src/components/CareerModal.js +++ b/src/components/CareerModal.js @@ -4,11 +4,40 @@ 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); - 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) { return (
@@ -23,50 +52,74 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { } if (!careerDetails?.salaryData) { - return ( -
-
-

Loading career details...

+ return ( +
+
+

Loading career details...

+
-
- ); -} - if (error) return
{error}
; + ); + } + if (error) return
{error}
; + + // Helper for "stability" rating const calculateStabilityRating = (salaryData) => { - const medianSalaryObj = salaryData.find(s => s.percentile === 'Median'); - const medianSalary = medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0; - + const medianSalaryObj = salaryData.find((s) => s.percentile === 'Median'); + const medianSalary = + medianSalaryObj?.regionalSalary || medianSalaryObj?.nationalSalary || 0; + if (medianSalary >= 90000) return 5; if (medianSalary >= 70000) return 4; if (medianSalary >= 50000) return 3; if (medianSalary >= 30000) return 2; return 1; }; - return (
-
-

{careerDetails.title}

-
- + {/* Title row */} +
+
+

+ {careerDetails.title} +

+ {/* AI RISK SECTION */} + {loadingRisk ? ( +

Loading AI risk...

+ ) : aiRisk ? ( +
+ AI Risk Level: {aiRisk.riskLevel} +
+ {aiRisk.reasoning} +
+ ) : ( +

+ No AI risk data available +

+ )} +
+ {/* Buttons */} +
+
- {/* Tasks (full width) */} + {/* Tasks */}

Tasks:

    @@ -93,7 +146,7 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
- {/* Salary and Economic Projections side-by-side */} + {/* Salary & Projections side-by-side */}
{/* Salary Data */}
@@ -110,8 +163,12 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { {careerDetails.salaryData.map((s, i) => ( {s.percentile} - ${s.regionalSalary.toLocaleString()} - ${s.nationalSalary.toLocaleString()} + + ${s.regionalSalary.toLocaleString()} + + + ${s.nationalSalary.toLocaleString()} + ))} @@ -121,27 +178,25 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { {/* Economic Projections */}

Economic Projections

- - {/* If we have state data, show a column for it */} {careerDetails.economicProjections.state && ( )} - {/* If we have national data, show a column for it */} {careerDetails.economicProjections.national && ( )} - {/* Row for Current Jobs */} - + {careerDetails.economicProjections.state && ( )} - - {/* Row for Jobs in 10 yrs */} - + {careerDetails.economicProjections.state && ( )} - - {/* Row for Growth % */} {careerDetails.economicProjections.state && ( @@ -183,10 +236,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { )} - - {/* Row for Annual Openings */} - + {careerDetails.economicProjections.state && (
{careerDetails.economicProjections.state.area} National
Current Jobs + Current Jobs + {careerDetails.economicProjections.state.base.toLocaleString()} @@ -153,10 +208,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
Jobs in 10 yrs + Jobs in 10 yrs + {careerDetails.economicProjections.state.projection.toLocaleString()} @@ -168,8 +223,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
Growth %
Annual Openings + Annual Openings + {careerDetails.economicProjections.state.annualOpenings.toLocaleString()} @@ -201,11 +254,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) {
-
); } -export default CareerModal; \ No newline at end of file +export default CareerModal; diff --git a/user_profile.db b/user_profile.db index 64f498fec948fb655d091ec76a879f5b319e55e6..3589a1570da62c1626168f24d71cf6134518c2cb 100644 GIT binary patch delta 318 zcmZp8z}~QcV}i8cas~zlMIeTO?1?(YjLRDnwk9wxnXkadT+P6LmG=(sa-RF#J=~t0 z&pBssda`cjWMbLHT+LFsu`!%Esvp73GF)uNvvN*FC zov+{=%C5gq^#c&?j;+)j7)EtCRYGQGIUS?i8SY-NvRg6X)T$=W}Y~sPX l)BRU6N-LnaHr^23MT*;ZEn#%?o-~1FG0Ovi%`6Rn_yNq7Xt)3X delta 77 zcmZozz|ru4eS);$G6n_)c_4;?tcg0tjLRAmwk9wxnJ>e`vWtQLD(@ZMJ0 bpL5RO^km)4$;7g2W8*BA?YovRx_JWt2nQBq