Added AI-risk to CareerCoach, fixed infinite loop, all/most contextual information provided to CareerCoach.

This commit is contained in:
Josh 2025-06-09 13:01:48 +00:00
parent 765492b856
commit a9fe5ce8bc
8 changed files with 356 additions and 177 deletions

View File

@ -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.
**************************************************/

View File

@ -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 dont 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 youd 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 = `Youd 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 youtheres no "wrong" answer.
It doesnt 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 youre most eager to explore right now.
If you complete the Interest Inventory, Ill be able to offer more targeted suggestions based on your interests.
I'm here to support you with personalized coachingwhat 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
@ -665,8 +668,29 @@ 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.
Your job is to leverage *all* this context to provide specific, empathetic, and helpful advice.
Remember: AptivaAIs mission is to help the workforce grow *with* AI, not be displaced by it.
Just like previous revolutionsindustrial, digitalour 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 and use the user's actual data. Avoid purely generic responses.
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
****************************************************/

10
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,84 +26,71 @@ export default function CareerCoach({
}, [messages, hasSentMessage]);
useEffect(() => {
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, userProfile]);
}, [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}.`;
nowPart = `It appears youre looking ahead to a possible future as it pertains to ${careerName}.`;
break;
case "current":
nowPart = `It sounds like you're already in the ${careerName} field.`;
nowPart = `It appears youre currently working in a role as it pertains to ${careerName}.`;
break;
case "exploring":
nowPart = `It sounds like you're still exploring how ${careerName} might fit your plans.`;
nowPart = `It appears youre exploring how ${careerName} might fit your plans.`;
break;
default:
nowPart = `I dont have much info on your current involvement with ${careerName}.`;
nowPart = `I dont have a clear picture of your involvement with ${careerName}, but Im here to help.`;
break;
}
// -----------------------------
// "Where youd like to go next"
// -----------------------------
let nextPart = "";
switch (sSituation) {
case "planning":
nextPart = `You're aiming to figure out your strategy for moving into this field.`;
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 to step into ${careerName}.`;
nextPart = `You're actively developing the skills you need for new opportunities.`;
break;
case "enhancing":
nextPart = `Youd like to deepen or broaden your responsibilities within ${careerName}.`;
nextPart = `Youd like to deepen or broaden your responsibilities.`;
break;
case "retirement":
nextPart = `You're contemplating how to transition toward retirement in this field.`;
nextPart = `You're considering how to transition toward retirement.`;
break;
default:
nextPart = `I'm not entirely sure of your next direction.`;
nextPart = `I'm not entirely sure of your next direction, but well keep your background in mind.`;
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 = `
const friendlyNote = `
Feel free to use AptivaAI however it best suits youtheres 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.
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();
// 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}`;
}
return `${combinedDescription}\n${friendlyNote}`;
}
const generatePersonalizedIntro = () => {
const careerName = scenarioRow?.career_name || null;
const careerName = scenarioRow?.career_name || "this career";
const goalsText = scenarioRow?.career_goals?.trim() || null;
const riskLevel = scenarioRow?.riskLevel;
const riskReasoning = scenarioRow?.riskReasoning;
@ -107,7 +98,11 @@ We can refine details anytime or just jump straight to what you're most interest
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.`
@ -127,13 +122,13 @@ We can refine details anytime or just jump straight to what you're most interest
: null;
const missingProfileFields = [];
if (!careerName) missingProfileFields.push("career choice");
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 = `<br /><br /><em>If you provide ${
advisoryMessage = `<em>If you provide ${
missingProfileFields.length > 1
? "a few more details"
: "this information"
@ -143,16 +138,15 @@ We can refine details anytime or just jump straight to what you're most interest
return {
role: "assistant",
content: `
Hi! ${combinedMessage}<br /><br />
${goalsMessage ? goalsMessage + "<br /><br />" : ""}
${interestInventoryMessage}<br /><br />
${riskMessage}<br />
${advisoryMessage}<br />
Hi! ${combinedMessage}<br/>
${goalsMessage ? goalsMessage + "<br/>" : ""}
${interestInventoryMessage}<br/>
${riskMessage}<br/>
${advisoryMessage}<br/>
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
)}
</div>
{/* Optionally display AI risk info here if you'd like */}
{aiRisk && aiRisk.riskLevel && (
<div className="p-2 my-2 bg-yellow-100 text-yellow-900 rounded">
<strong>Automation Risk:</strong> {aiRisk.riskLevel}
<br />
<em>{aiRisk.reasoning}</em>
</div>
)}
<form onSubmit={handleSubmit} className="flex space-x-2">
<input
type="text"

View File

@ -395,18 +395,19 @@ function CareerExplorer() {
// if not found => 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,

View File

@ -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,6 +931,12 @@ 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?" */}
@ -849,6 +954,14 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
{yearsInCareer === '<1' ? 'year' : 'years'}
</p>
)}
{aiRisk?.riskLevel && (
<p className="text-center mt-2">
<strong>AI Automation Risk:</strong>{' '}
{aiRisk.riskLevel} <br />
<em>{aiRisk.reasoning}</em>
</p>
)}
</div>
{/* 2) Salary Benchmarks */}
@ -1009,7 +1122,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
/>
{/* (E1) Interest Strategy */}
<label className="ml-4 font-medium">Interest Strategy:</label>
<label className="ml-4 font-medium">Interest Rate:</label>
<select
value={interestStrategy}
onChange={(e) => setInterestStrategy(e.target.value)}

Binary file not shown.