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 (
Loading career details...
+ return ( +Loading career details...
+Loading AI risk...
+ ) : aiRisk ? ( ++ No AI risk data available +
+ )} +- {/* If we have state data, show a column for it */} {careerDetails.economicProjections.state && ( | {careerDetails.economicProjections.state.area} | )} - {/* If we have national data, show a column for it */} {careerDetails.economicProjections.national && (National | )}
---|---|---|
Current Jobs | ++ Current Jobs + | {careerDetails.economicProjections.state && ({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 && ({careerDetails.economicProjections.state.projection.toLocaleString()} @@ -168,8 +223,6 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { | )}
Growth % | {careerDetails.economicProjections.state && ( @@ -183,10 +236,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { )}||
Annual Openings | ++ Annual Openings + | {careerDetails.economicProjections.state && ({careerDetails.economicProjections.state.annualOpenings.toLocaleString()} @@ -201,11 +254,10 @@ function CareerModal({ career, careerDetails, closeModal, addCareerToList }) { |