diff --git a/backend/server2.js b/backend/server2.js index 25ca394..6f35820 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -173,10 +173,27 @@ async function storeRiskAnalysisInDB({ jobDescription, tasks, riskLevel, - reasoning, + 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). + // 1) get existing row if any + const existing = await userProfileDb.get( + `SELECT * FROM ai_risk_analysis WHERE soc_code = ?`, + [socCode] + ); + + let finalJobDesc = jobDescription ?? ""; + let finalTasks = tasks ?? ""; + + // 2) If existing row and the new jobDescription is blank => keep existing + if (existing) { + if (!jobDescription?.trim()) { + finalJobDesc = existing.job_description; + } + if (!tasks?.trim()) { + finalTasks = existing.tasks; + } + } + const sql = ` INSERT OR REPLACE INTO ai_risk_analysis ( soc_code, @@ -190,14 +207,15 @@ async function storeRiskAnalysisInDB({ `; await userProfileDb.run(sql, [ socCode, - careerName || '', - jobDescription || '', - tasks || '', - riskLevel || '', - reasoning || '', + careerName || existing?.career_name || '', + finalJobDesc || '', + finalTasks || '', + riskLevel || existing?.risk_level || '', + reasoning || existing?.reasoning || '' ]); } + /************************************************** * O*Net routes, CIP routes, distance routes, etc. **************************************************/ diff --git a/backend/server3.js b/backend/server3.js index fa5b46f..1f31f67 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -479,6 +479,9 @@ ${riskText} } // Example: ai/chat with correct milestone-saving logic +// At the top of server3.js, leave your imports and setup as-is +// (No need to import 'pluralize' if we're no longer using it!) + app.post('/api/premium/ai/chat', authenticatePremiumUser, async (req, res) => { try { const { @@ -494,65 +497,70 @@ app.post('/api/premium/ai/chat', authenticatePremiumUser, async (req, res) => { // ------------------------------------------------ // A. Build a "where you are now" vs. "where you want to go" message + // with phrasing that works for plural career names. function buildStatusSituationMessage(status, situation, careerName) { - const sStatus = (status || "").toLowerCase(); // e.g. "planned", "current", "exploring" - const sSituation = (situation || "").toLowerCase(); // e.g. "planning", "preparing", "enhancing", "retirement" + // For example: careerName = "Blockchain Engineers" + const sStatus = (status || "").toLowerCase(); + const sSituation = (situation || "").toLowerCase(); - // "Where you are now" + // Intro / "Now" part let nowPart = ""; switch (sStatus) { case "planned": - nowPart = `It appears you're looking ahead to a possible future in ${careerName}.`; + nowPart = `Hi! It sounds like you're looking ahead to potential opportunities in ${careerName}.`; break; case "current": - nowPart = `It appears you're already working in the ${careerName} field.`; + nowPart = `Hi! It looks like you're currently involved in ${careerName}.`; break; case "exploring": - nowPart = `It appears you're exploring how ${careerName} might fit your future plans.`; + nowPart = `Hi! You're exploring how ${careerName} might fit your plans.`; break; default: - nowPart = `I don’t have a clear picture of where you stand currently with ${careerName}.`; + nowPart = `Hi! I'm not fully sure about your current involvement with ${careerName}, but I'd love to learn more.`; break; } - // "Where you’d like to go next" + // Next / "Where you're going" part let nextPart = ""; switch (sSituation) { case "planning": - nextPart = `You're aiming to clarify your strategy for moving into this field.`; + nextPart = `You're aiming to clarify your strategy for moving into a role within ${careerName}.`; break; case "preparing": - nextPart = `You're actively developing the skills you need to step into ${careerName}.`; + nextPart = `You're actively developing the skills you need for future opportunities in ${careerName}.`; break; case "enhancing": - nextPart = `You’d like to deepen or expand your responsibilities within ${careerName}.`; + nextPart = `You'd like to deepen or broaden your responsibilities within ${careerName}.`; break; case "retirement": - nextPart = `You're considering how to transition toward retirement in this role.`; + nextPart = `You're considering how to transition toward retirement from ${careerName}.`; break; default: - nextPart = `I'm not entirely sure of your next direction.`; + nextPart = `I'm not entirely sure what your next steps might be regarding ${careerName}, but we'll figure it out together.`; break; } const combinedDescription = `${nowPart} ${nextPart}`.trim(); - // Add a friendly note about how there's no "wrong" answer + // Friendly note - feel free to tweak the wording const friendlyNote = ` -No worries if these selections feel a bit overlapping or if you're just exploring. -One portion highlights where you currently see yourself, and the other points to where you'd like to go. -Feel free to refine these whenever you want, or just continue as is. +Feel free to use AptivaAI however it best suits you—there’s no "wrong" answer. +It doesn’t matter so much where you've been; it's about where you want to go from here. +We can refine details any time or jump straight to what you’re most eager to explore right now. + +If you complete the Interest Inventory, I’ll be able to offer more targeted suggestions based on your interests. + +I'm here to support you with personalized coaching—what would you like to focus on next? `.trim(); return `${combinedDescription}\n\n${friendlyNote}`; } - // B. Build a user summary that references all available info + // B. Build a user summary that references all available info (unchanged from your code) function buildUserSummary({ userProfile, scenarioRow, financialProfile, collegeProfile, aiRisk }) { - // For illustration; adjust to your actual data fields. const userName = userProfile.first_name || "N/A"; const location = userProfile.location || "N/A"; - const userGoals = userProfile.goals || []; // maybe an array + const userGoals = userProfile.goals || []; const careerName = scenarioRow?.career_name || "this career"; const socCode = scenarioRow?.soc_code || "N/A"; @@ -569,7 +577,9 @@ Feel free to refine these whenever you want, or just continue as is. const creditsCompleted = collegeProfile?.credits_completed || 0; const graduationDate = collegeProfile?.expected_graduation || "Unknown"; - const aiRiskReport = aiRisk?.report || "No AI risk info provided."; + const aiRiskReport = aiRisk?.riskLevel + ? `Risk Level: ${aiRisk.riskLevel}\nReasoning: ${aiRisk.reasoning}` + : "No AI risk info provided."; return ` [USER PROFILE] @@ -598,14 +608,7 @@ ${aiRiskReport} `.trim(); } - // Example environment config - const MILESTONE_API_URL = process.env.APTIVA_INTERNAL_API - ? `${process.env.APTIVA_INTERNAL_API}/premium/milestone` - : "http://localhost:5002/api/premium/milestone"; - - const IMPACT_API_URL = process.env.APTIVA_INTERNAL_API - ? `${process.env.APTIVA_INTERNAL_API}/premium/milestone-impacts` - : "http://localhost:5002/api/premium/milestone-impacts"; + // (No changes to your environment configs) // ------------------------------------------------ // 2. AI Risk Fetch @@ -662,11 +665,32 @@ ${aiRiskReport} const systemPromptIntro = ` You are Jess, a professional career coach working inside AptivaAI. -The user has already provided detailed information about their situation, career goals, finances, education, and more. +The user has already provided detailed information about their situation, career goals, finances, education, and more. Your job is to leverage *all* this context to provide specific, empathetic, and helpful advice. -Do not re-ask for the details below unless you need clarifications. -Reflect and use the user's actual data. Avoid purely generic responses. +Remember: AptivaAI’s mission is to help the workforce grow *with* AI, not be displaced by it. +Just like previous revolutions—industrial, digital—our goal is to show individuals how to +utilize AI tools, enhance their own capabilities, and pivot into new opportunities if automation +begins to handle older tasks. + +Speak in a warm, empathetic tone. Validate the user's ambitions, +explain how to break down big goals into realistic steps, +and highlight how AI can serve as a *collaborative* tool rather than a rival. + +Reference the user's location and any relevant experiences or ambitions they've shared. +Validate their ambitions, explain how to break down big goals into realistic steps, +and gently highlight how the user might further explore or refine their plans with AptivaAI's Interest Inventory. + +If the user has mentioned ambitious financial or lifestyle goals (e.g., wanting to buy a Ferrari, +become a millionaire, etc.), acknowledge them as "bold" or "exciting," and clarify +how the user might move toward them via skill-building, networking, or +other relevant steps. + +Use bullet points to restate user goals or interests. +End with an open-ended question about what they'd like to tackle next in their plan. + +Do not re-ask for the details below unless you need clarifications. +Reflect the user's actual data. Avoid purely generic responses. `.trim(); const systemPromptStatusSituation = ` @@ -688,7 +712,7 @@ ${summaryText} ]; // ------------------------------------------------ - // 6. Call GPT + // 6. Call GPT (unchanged) // ------------------------------------------------ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const completion = await openai.chat.completions.create({ @@ -701,7 +725,7 @@ ${summaryText} let coachResponse = completion?.choices?.[0]?.message?.content?.trim(); // ------------------------------------------------ - // 7. Detect and Possibly Save Milestones + // 7. Detect and Possibly Save Milestones (unchanged) // ------------------------------------------------ let milestones = []; let isMilestoneFormat = false; @@ -714,7 +738,10 @@ ${summaryText} } if (isMilestoneFormat && milestones.length) { - // 7a. Prepare data for milestone creation + const MILESTONE_API_URL = process.env.APTIVA_INTERNAL_API + ? `${process.env.APTIVA_INTERNAL_API}/premium/milestone` + : "http://localhost:5002/api/premium/milestone"; + const rawForMilestonesEndpoint = milestones.map(m => { return { title: m.title, @@ -724,7 +751,6 @@ ${summaryText} }; }); - // 7b. Bulk-create milestones const mileRes = await fetch(MILESTONE_API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -735,10 +761,12 @@ ${summaryText} console.error("Failed to save milestones =>", mileRes.status); coachResponse = "I prepared milestones, but couldn't save them right now. Please try again later."; } else { - // 7c. newly created milestones with IDs const createdMils = await mileRes.json(); - // 7d. For each milestone, if it has "impacts", create them + const IMPACT_API_URL = process.env.APTIVA_INTERNAL_API + ? `${process.env.APTIVA_INTERNAL_API}/premium/milestone-impacts` + : "http://localhost:5002/api/premium/milestone-impacts"; + for (let i = 0; i < milestones.length; i++) { const originalMilestone = milestones[i]; const newMilestone = createdMils[i]; @@ -775,15 +803,16 @@ ${summaryText} } // ------------------------------------------------ - // 8. Send JSON Response + // 8. Send JSON Response (unchanged) // ------------------------------------------------ - res.json({ reply: coachResponse }); + res.json({ reply: coachResponse, aiRisk }); } catch (err) { console.error("Error in /api/premium/ai/chat =>", err); res.status(500).json({ error: "Failed to generate conversational response." }); } }); + /*************************************************** AI MILESTONE CONVERSION ENDPOINT ****************************************************/ diff --git a/package-lock.json b/package-lock.json index b39724e..a747312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "openai": "^4.97.0", "pdf-parse": "^1.1.1", "pdfjs-dist": "^3.11.174", + "pluralize": "^8.0.0", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", @@ -14370,6 +14371,15 @@ "node": ">=4" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 87b81ad..6ba4880 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "openai": "^4.97.0", "pdf-parse": "^1.1.1", "pdfjs-dist": "^3.11.174", + "pluralize": "^8.0.0", "react": "^18.2.0", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", diff --git a/src/components/CareerCoach.js b/src/components/CareerCoach.js index 5771385..6b5f3bc 100644 --- a/src/components/CareerCoach.js +++ b/src/components/CareerCoach.js @@ -7,13 +7,17 @@ export default function CareerCoach({ scenarioRow, collegeProfile, onMilestonesCreated, + onAiRiskFetched }) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const chatContainerRef = useRef(null); const [hasSentMessage, setHasSentMessage] = useState(false); + const [prevRiskLevel, setPrevRiskLevel] = useState(null); + // NEW: optional local state to hold the AI risk data if you want to show it somewhere + const [aiRisk, setAiRisk] = useState(null); useEffect(() => { if (chatContainerRef.current) { @@ -22,137 +26,127 @@ export default function CareerCoach({ }, [messages, hasSentMessage]); useEffect(() => { - const introMessage = generatePersonalizedIntro(); - setMessages([introMessage]); - }, [scenarioRow, userProfile]); + if (!scenarioRow?.riskLevel) return; + + // If it hasn't changed, skip + if (scenarioRow.riskLevel === prevRiskLevel) return; + + setPrevRiskLevel(scenarioRow.riskLevel); + + // Now generate the intro once + const introMessage = generatePersonalizedIntro(); + setMessages([introMessage]); +}, [scenarioRow]); function buildStatusSituationMessage(status, situation, careerName) { - const sStatus = (status || "").toLowerCase(); - // e.g. "planned", "current", "exploring", etc. - - const sSituation = (situation || "").toLowerCase(); - // e.g. "planning", "preparing", "enhancing", "retirement" - - // ----------------------------- - // "Where you are right now" - // ----------------------------- - let nowPart = ""; - switch (sStatus) { - case "planned": - nowPart = `It sounds like you're looking ahead to a possible future in ${careerName}.`; - break; - case "current": - nowPart = `It sounds like you're already in the ${careerName} field.`; - break; - case "exploring": - nowPart = `It sounds like you're still exploring how ${careerName} might fit your plans.`; - break; - default: - nowPart = `I don’t have much info on your current involvement with ${careerName}.`; - break; - } - - // ----------------------------- - // "Where you’d like to go next" - // ----------------------------- - let nextPart = ""; - switch (sSituation) { - case "planning": - nextPart = `You're aiming to figure out your strategy for moving into this field.`; - break; - case "preparing": - nextPart = `You're actively developing the skills you need to step into ${careerName}.`; - break; - case "enhancing": - nextPart = `You’d like to deepen or broaden your responsibilities within ${careerName}.`; - break; - case "retirement": - nextPart = `You're contemplating how to transition toward retirement in this field.`; - break; - default: - nextPart = `I'm not entirely sure of your next direction.`; - break; - } - - // ----------------------------- - // Combine the descriptive text - // ----------------------------- - const combinedDescription = `${nowPart} ${nextPart}`.trim(); - - // ----------------------------- - // Optional “friendly note” if they seem to span different phases - // but we do *not* treat it as a mismatch or error. - // ----------------------------- - let friendlyNote = ` -Feel free to use AptivaAI however it best suits you—there’s no "wrong" answer. -One part highlights your current situation, the other indicates what you're aiming for next. -Some folks aren't working and want premium-level guidance, and that's where we shine. -We can refine details anytime or just jump straight to what you're most interested in exploring now! - `.trim(); + const sStatus = (status || "").toLowerCase(); + const sSituation = (situation || "").toLowerCase(); - // You could conditionally show the friendlyNote only if you detect certain combos, - // OR you can show it unconditionally to make the user comfortable. - - // For maximum inclusivity, we can always show it. - - return `${combinedDescription}\n\n${friendlyNote}`; -} + let nowPart = ""; + switch (sStatus) { + case "planned": + nowPart = `It appears you’re looking ahead to a possible future as it pertains to ${careerName}.`; + break; + case "current": + nowPart = `It appears you’re currently working in a role as it pertains to ${careerName}.`; + break; + case "exploring": + nowPart = `It appears you’re exploring how ${careerName} might fit your plans.`; + break; + default: + nowPart = `I don’t have a clear picture of your involvement with ${careerName}, but I’m here to help.`; + break; + } + + let nextPart = ""; + switch (sSituation) { + case "planning": + nextPart = `You're aiming to clarify your strategy for moving into this field.`; + break; + case "preparing": + nextPart = `You're actively developing the skills you need for new opportunities.`; + break; + case "enhancing": + nextPart = `You’d like to deepen or broaden your responsibilities.`; + break; + case "retirement": + nextPart = `You're considering how to transition toward retirement.`; + break; + default: + nextPart = `I'm not entirely sure of your next direction, but we’ll keep your background in mind.`; + break; + } + + const combinedDescription = `${nowPart} ${nextPart}`.trim(); + + const friendlyNote = ` +Feel free to use AptivaAI however it best suits you—there’s no "wrong" answer. +AptivaAI asks for some of your current situation so we can provide the best guidance on what you should do next to reach your goals. +It's really about where you want to go from here (that's all you can control anyway). +We can refine details anytime or just jump straight to what you're most interested in exploring now! + `.trim(); + + return `${combinedDescription}\n${friendlyNote}`; + } const generatePersonalizedIntro = () => { - const careerName = scenarioRow?.career_name || null; - const goalsText = scenarioRow?.career_goals?.trim() || null; - const riskLevel = scenarioRow?.riskLevel; - const riskReasoning = scenarioRow?.riskReasoning; + const careerName = scenarioRow?.career_name || "this career"; + const goalsText = scenarioRow?.career_goals?.trim() || null; + const riskLevel = scenarioRow?.riskLevel; + const riskReasoning = scenarioRow?.riskReasoning; - const userSituation = userProfile?.career_situation?.toLowerCase(); - const userStatus = scenarioRow?.status?.toLowerCase(); + const userSituation = userProfile?.career_situation?.toLowerCase(); + const userStatus = scenarioRow?.status?.toLowerCase(); - const combinedMessage = buildStatusSituationMessage(userStatus, userSituation, careerName); + const combinedMessage = buildStatusSituationMessage( + userStatus, + userSituation, + careerName + ); - const interestInventoryMessage = userProfile?.riasec - ? `With your Interest Inventory profile (${userProfile.riasec}), I can tailor suggestions more precisely.` - : `If you complete the Interest Inventory, I’ll be able to offer more targeted suggestions based on your interests.`; + const interestInventoryMessage = userProfile?.riasec + ? `With your Interest Inventory profile (${userProfile.riasec}), I can tailor suggestions more precisely.` + : `If you complete the Interest Inventory, I’ll be able to offer more targeted suggestions based on your interests.`; - const riskMessage = - riskLevel && riskReasoning - ? `Note: This role has a ${riskLevel} automation risk over the next 10 years. ${riskReasoning}` - : ""; + const riskMessage = + riskLevel && riskReasoning + ? `Note: This role has a ${riskLevel} automation risk over the next 10 years. ${riskReasoning}` + : ""; - const goalsMessage = goalsText - ? `Your goals include:
${goalsText - .split(/\d+\.\s?/) - .filter(Boolean) - .map((goal) => `• ${goal.trim()}`) - .join("
")}` - : null; + const goalsMessage = goalsText + ? `Your goals include:
${goalsText + .split(/\d+\.\s?/) + .filter(Boolean) + .map((goal) => `• ${goal.trim()}`) + .join("
")}` + : null; - const missingProfileFields = []; - if (!careerName) missingProfileFields.push("career choice"); - if (!goalsText) missingProfileFields.push("career goals"); - if (!userSituation) missingProfileFields.push("career phase"); + const missingProfileFields = []; + if (!scenarioRow?.career_name) missingProfileFields.push("career choice"); + if (!goalsText) missingProfileFields.push("career goals"); + if (!userSituation) missingProfileFields.push("career phase"); - let advisoryMessage = ""; - if (missingProfileFields.length > 0) { - advisoryMessage = `

If you provide ${ - missingProfileFields.length > 1 - ? "a few more details" - : "this information" - }, I’ll be able to offer more tailored and precise advice.`; - } + let advisoryMessage = ""; + if (missingProfileFields.length > 0) { + advisoryMessage = `If you provide ${ + missingProfileFields.length > 1 + ? "a few more details" + : "this information" + }, I’ll be able to offer more tailored and precise advice.`; + } - return { - role: "assistant", - content: ` - Hi! ${combinedMessage}

- ${goalsMessage ? goalsMessage + "

" : ""} - ${interestInventoryMessage}

- ${riskMessage}
- ${advisoryMessage}
- I'm here to support you with personalized coaching. What would you like to focus on today? - `, + return { + role: "assistant", + content: ` + Hi! ${combinedMessage}
+ ${goalsMessage ? goalsMessage + "
" : ""} + ${interestInventoryMessage}
+ ${riskMessage}
+ ${advisoryMessage}
+ I'm here to support you with personalized coaching. What would you like to focus on today? + `, + }; }; -}; - const handleSendMessage = async () => { if (!input.trim() || loading) return; @@ -181,12 +175,16 @@ We can refine details anytime or just jump straight to what you're most interest if (!res.ok) throw new Error("AI request failed"); - const { reply } = await res.json(); + // Here we destructure out aiRisk from the JSON + // so we can store it or display it in the frontend + const { reply, aiRisk: riskDataFromServer } = await res.json(); - setMessages((prev) => [ - ...prev, - { role: "assistant", content: reply }, - ]); + setMessages((prev) => [...prev, { role: "assistant", content: reply }]); + + // OPTIONAL: store or use the AI risk data + if (riskDataFromServer && onAiRiskFetched) { + onAiRiskFetched(riskDataFromServer); + } if ( reply.includes("created those milestones") || @@ -249,6 +247,15 @@ We can refine details anytime or just jump straight to what you're most interest )} + {/* Optionally display AI risk info here if you'd like */} + {aiRisk && aiRisk.riskLevel && ( +
+ Automation Risk: {aiRisk.riskLevel} +
+ {aiRisk.reasoning} +
+ )} +
call server3 => store in server2. // ---------------------------------------------------- let aiRisk = null; - const strippedSoc = socCode.split('.')[0]; + const strippedSocCode = socCode.split('.')[0]; + try { // Check local DB first (SQLite -> server2) - const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${strippedSoc}`); + const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${socCode}`); 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, + socCode, careerName: career.title, jobDescription: description, tasks, @@ -416,7 +417,7 @@ function CareerExplorer() { // store it back in server2 to avoid repeated GPT calls await axios.post(`${apiUrl}/ai-risk`, { - socCode: strippedSoc, + socCode, careerName: aiRes.data.careerName, jobDescription: aiRes.data.jobDescription, tasks: aiRes.data.tasks, @@ -426,7 +427,7 @@ function CareerExplorer() { // build final object aiRisk = { - socCode: strippedSoc, + socCode, careerName: career.title, jobDescription: description, tasks, diff --git a/src/components/CareerRoadmap.js b/src/components/CareerRoadmap.js index d1afd6c..b3f397a 100644 --- a/src/components/CareerRoadmap.js +++ b/src/components/CareerRoadmap.js @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { Line, Bar } from 'react-chartjs-2'; +import axios from 'axios'; import { Chart as ChartJS, LineElement, @@ -26,6 +27,8 @@ import parseAIJson from "../utils/parseAIJson.js"; // your shared parser import './CareerRoadmap.css'; import './MilestoneTimeline.css'; +const apiUrl = process.env.REACT_APP_API_URL || ''; + // -------------- // Register ChartJS Plugins // -------------- @@ -254,6 +257,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { const [scenarioRow, setScenarioRow] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null); + const [fullSocCode, setFullSocCode] = useState(null); // new line const [strippedSocCode, setStrippedSocCode] = useState(null); const [salaryData, setSalaryData] = useState(null); const [economicProjections, setEconomicProjections] = useState(null); @@ -277,6 +281,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { const [lastClickTime, setLastClickTime] = useState(null); const RATE_LIMIT_SECONDS = 15; // adjust as needed const [buttonDisabled, setButtonDisabled] = useState(false); + const [aiRisk, setAiRisk] = useState(null); const { projectionData: initProjData = [], @@ -417,6 +422,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { useEffect(() => { if (!scenarioRow?.career_name || !masterCareerRatings.length) { setStrippedSocCode(null); + setFullSocCode(null); return; } const lower = scenarioRow.career_name.trim().toLowerCase(); @@ -426,11 +432,103 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { if (!found) { console.warn('No matching SOC =>', scenarioRow.career_name); setStrippedSocCode(null); + setFullSocCode(null); return; } setStrippedSocCode(stripSocCode(found.soc_code)); + setFullSocCode(found.soc_code); }, [scenarioRow, masterCareerRatings]); + useEffect(() => { + if (!fullSocCode || !scenarioRow || scenarioRow.riskLevel) return; + (async () => { + const risk = await fetchAiRisk( + fullSocCode, + scenarioRow?.career_name, + scenarioRow?.job_description || "", + scenarioRow?.tasks || [] + ); + setAiRisk(risk); + if (risk && scenarioRow) { + const updated = { + ...scenarioRow, + riskLevel: risk.riskLevel, + riskReasoning: risk.reasoning + }; + setScenarioRow(updated); + } + })(); +}, [fullSocCode, scenarioRow]); + +async function fetchAiRisk(socCode, careerName, description, tasks) { + let aiRisk = null; + +try { + // 1) Check server2 for existing entry + const localRiskRes = await axios.get(`${apiUrl}/ai-risk/${socCode}`); + aiRisk = localRiskRes.data; // { socCode, riskLevel, ... } +} catch (err) { + // 2) If 404 => call server3 + if (err.response && err.response.status === 404) { + try { + // Call GPT via server3 + const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, { + socCode, + careerName, + jobDescription: description, + tasks + }); + + const { riskLevel, reasoning } = aiRes.data; + + // Prepare the upsert payload + const storePayload = { + socCode, + careerName, + riskLevel, + reasoning + }; + + // Only set jobDescription if non-empty + if ( + aiRes.data.jobDescription && + aiRes.data.jobDescription.trim().length > 0 + ) { + storePayload.jobDescription = aiRes.data.jobDescription; + } + + // Only set tasks if it's a non-empty array + if ( + Array.isArray(aiRes.data.tasks) && + aiRes.data.tasks.length > 0 + ) { + storePayload.tasks = aiRes.data.tasks; + } + + // 3) Store in server2 + await axios.post(`${apiUrl}/ai-risk`, storePayload); + + // Construct final object for usage here + aiRisk = { + socCode, + careerName, + 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); + } +} + + return aiRisk; +} + // 6) Salary useEffect(() => { if (!strippedSocCode) { @@ -456,6 +554,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) { })(); }, [strippedSocCode, userArea, apiURL]); + // 7) Econ useEffect(() => { if (!strippedSocCode || !userState) { @@ -832,7 +931,13 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) { onMilestonesCreated={() => { /* refresh or reload logic here */ }} - /> + + + onAiRiskFetched={(riskData) => { + // store it in local state + setAiRisk(riskData); + }} + /> {/* 1) Then your "Where Am I Now?" */}

Where you are now and where you are going.

@@ -849,6 +954,14 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) { {yearsInCareer === '<1' ? 'year' : 'years'}

)} + + {aiRisk?.riskLevel && ( +

+ AI Automation Risk:{' '} + {aiRisk.riskLevel}
+ {aiRisk.reasoning} +

+ )} {/* 2) Salary Benchmarks */} @@ -1009,7 +1122,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) { /> {/* (E1) Interest Strategy */} - +