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, jobDescription,
tasks, tasks,
riskLevel, riskLevel,
reasoning, reasoning
}) { }) {
// We'll use INSERT OR REPLACE so that if a row with the same soc_code // 1) get existing row if any
// already exists, it gets replaced (acts like an upsert). 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 = ` const sql = `
INSERT OR REPLACE INTO ai_risk_analysis ( INSERT OR REPLACE INTO ai_risk_analysis (
soc_code, soc_code,
@ -190,14 +207,15 @@ async function storeRiskAnalysisInDB({
`; `;
await userProfileDb.run(sql, [ await userProfileDb.run(sql, [
socCode, socCode,
careerName || '', careerName || existing?.career_name || '',
jobDescription || '', finalJobDesc || '',
tasks || '', finalTasks || '',
riskLevel || '', riskLevel || existing?.risk_level || '',
reasoning || '', reasoning || existing?.reasoning || ''
]); ]);
} }
/************************************************** /**************************************************
* O*Net routes, CIP routes, distance routes, etc. * O*Net routes, CIP routes, distance routes, etc.
**************************************************/ **************************************************/

View File

@ -479,6 +479,9 @@ ${riskText}
} }
// Example: ai/chat with correct milestone-saving logic // 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) => { app.post('/api/premium/ai/chat', authenticatePremiumUser, async (req, res) => {
try { try {
const { 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 // 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) { function buildStatusSituationMessage(status, situation, careerName) {
const sStatus = (status || "").toLowerCase(); // e.g. "planned", "current", "exploring" // For example: careerName = "Blockchain Engineers"
const sSituation = (situation || "").toLowerCase(); // e.g. "planning", "preparing", "enhancing", "retirement" const sStatus = (status || "").toLowerCase();
const sSituation = (situation || "").toLowerCase();
// "Where you are now" // Intro / "Now" part
let nowPart = ""; let nowPart = "";
switch (sStatus) { switch (sStatus) {
case "planned": 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; break;
case "current": 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; break;
case "exploring": 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; break;
default: 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; break;
} }
// "Where youd like to go next" // Next / "Where you're going" part
let nextPart = ""; let nextPart = "";
switch (sSituation) { switch (sSituation) {
case "planning": 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; break;
case "preparing": 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; break;
case "enhancing": 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; break;
case "retirement": 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; break;
default: 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; break;
} }
const combinedDescription = `${nowPart} ${nextPart}`.trim(); 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 = ` const friendlyNote = `
No worries if these selections feel a bit overlapping or if you're just exploring. Feel free to use AptivaAI however it best suits youtheres no "wrong" answer.
One portion highlights where you currently see yourself, and the other points to where you'd like to go. It doesnt matter so much where you've been; it's about where you want to go from here.
Feel free to refine these whenever you want, or just continue as is. 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(); `.trim();
return `${combinedDescription}\n\n${friendlyNote}`; 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 }) { function buildUserSummary({ userProfile, scenarioRow, financialProfile, collegeProfile, aiRisk }) {
// For illustration; adjust to your actual data fields.
const userName = userProfile.first_name || "N/A"; const userName = userProfile.first_name || "N/A";
const location = userProfile.location || "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 careerName = scenarioRow?.career_name || "this career";
const socCode = scenarioRow?.soc_code || "N/A"; 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 creditsCompleted = collegeProfile?.credits_completed || 0;
const graduationDate = collegeProfile?.expected_graduation || "Unknown"; 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 ` return `
[USER PROFILE] [USER PROFILE]
@ -598,14 +608,7 @@ ${aiRiskReport}
`.trim(); `.trim();
} }
// Example environment config // (No changes to your environment configs)
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";
// ------------------------------------------------ // ------------------------------------------------
// 2. AI Risk Fetch // 2. AI Risk Fetch
@ -662,11 +665,32 @@ ${aiRiskReport}
const systemPromptIntro = ` const systemPromptIntro = `
You are Jess, a professional career coach working inside AptivaAI. 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. 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. Remember: AptivaAIs mission is to help the workforce grow *with* AI, not be displaced by it.
Reflect and use the user's actual data. Avoid purely generic responses. 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 the user's actual data. Avoid purely generic responses.
`.trim(); `.trim();
const systemPromptStatusSituation = ` 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 openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const completion = await openai.chat.completions.create({ const completion = await openai.chat.completions.create({
@ -701,7 +725,7 @@ ${summaryText}
let coachResponse = completion?.choices?.[0]?.message?.content?.trim(); let coachResponse = completion?.choices?.[0]?.message?.content?.trim();
// ------------------------------------------------ // ------------------------------------------------
// 7. Detect and Possibly Save Milestones // 7. Detect and Possibly Save Milestones (unchanged)
// ------------------------------------------------ // ------------------------------------------------
let milestones = []; let milestones = [];
let isMilestoneFormat = false; let isMilestoneFormat = false;
@ -714,7 +738,10 @@ ${summaryText}
} }
if (isMilestoneFormat && milestones.length) { 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 => { const rawForMilestonesEndpoint = milestones.map(m => {
return { return {
title: m.title, title: m.title,
@ -724,7 +751,6 @@ ${summaryText}
}; };
}); });
// 7b. Bulk-create milestones
const mileRes = await fetch(MILESTONE_API_URL, { const mileRes = await fetch(MILESTONE_API_URL, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -735,10 +761,12 @@ ${summaryText}
console.error("Failed to save milestones =>", mileRes.status); console.error("Failed to save milestones =>", mileRes.status);
coachResponse = "I prepared milestones, but couldn't save them right now. Please try again later."; coachResponse = "I prepared milestones, but couldn't save them right now. Please try again later.";
} else { } else {
// 7c. newly created milestones with IDs
const createdMils = await mileRes.json(); 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++) { for (let i = 0; i < milestones.length; i++) {
const originalMilestone = milestones[i]; const originalMilestone = milestones[i];
const newMilestone = createdMils[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) { } catch (err) {
console.error("Error in /api/premium/ai/chat =>", err); console.error("Error in /api/premium/ai/chat =>", err);
res.status(500).json({ error: "Failed to generate conversational response." }); res.status(500).json({ error: "Failed to generate conversational response." });
} }
}); });
/*************************************************** /***************************************************
AI MILESTONE CONVERSION ENDPOINT AI MILESTONE CONVERSION ENDPOINT
****************************************************/ ****************************************************/

10
package-lock.json generated
View File

@ -37,6 +37,7 @@
"openai": "^4.97.0", "openai": "^4.97.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
"pluralize": "^8.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -14370,6 +14371,15 @@
"node": ">=4" "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": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "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", "openai": "^4.97.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"pdfjs-dist": "^3.11.174", "pdfjs-dist": "^3.11.174",
"pluralize": "^8.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -7,13 +7,17 @@ export default function CareerCoach({
scenarioRow, scenarioRow,
collegeProfile, collegeProfile,
onMilestonesCreated, onMilestonesCreated,
onAiRiskFetched
}) { }) {
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const chatContainerRef = useRef(null); const chatContainerRef = useRef(null);
const [hasSentMessage, setHasSentMessage] = useState(false); 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(() => { useEffect(() => {
if (chatContainerRef.current) { if (chatContainerRef.current) {
@ -22,137 +26,127 @@ export default function CareerCoach({
}, [messages, hasSentMessage]); }, [messages, hasSentMessage]);
useEffect(() => { useEffect(() => {
const introMessage = generatePersonalizedIntro(); if (!scenarioRow?.riskLevel) return;
setMessages([introMessage]);
}, [scenarioRow, userProfile]); // 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) { function buildStatusSituationMessage(status, situation, careerName) {
const sStatus = (status || "").toLowerCase(); const sStatus = (status || "").toLowerCase();
// e.g. "planned", "current", "exploring", etc. const sSituation = (situation || "").toLowerCase();
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 dont have much info on your current involvement with ${careerName}.`;
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.`;
break;
case "preparing":
nextPart = `You're actively developing the skills you need to step into ${careerName}.`;
break;
case "enhancing":
nextPart = `Youd 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 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.
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, let nowPart = "";
// OR you can show it unconditionally to make the user comfortable. switch (sStatus) {
case "planned":
// For maximum inclusivity, we can always show it. nowPart = `It appears youre looking ahead to a possible future as it pertains to ${careerName}.`;
break;
return `${combinedDescription}\n\n${friendlyNote}`; case "current":
} nowPart = `It appears youre currently working in a role as it pertains to ${careerName}.`;
break;
case "exploring":
nowPart = `It appears youre exploring how ${careerName} might fit your plans.`;
break;
default:
nowPart = `I dont have a clear picture of your involvement with ${careerName}, but Im 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 = `Youd 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 well keep your background in mind.`;
break;
}
const combinedDescription = `${nowPart} ${nextPart}`.trim();
const friendlyNote = `
Feel free to use AptivaAI however it best suits youtheres 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 generatePersonalizedIntro = () => {
const careerName = scenarioRow?.career_name || null; const careerName = scenarioRow?.career_name || "this career";
const goalsText = scenarioRow?.career_goals?.trim() || null; const goalsText = scenarioRow?.career_goals?.trim() || null;
const riskLevel = scenarioRow?.riskLevel; const riskLevel = scenarioRow?.riskLevel;
const riskReasoning = scenarioRow?.riskReasoning; const riskReasoning = scenarioRow?.riskReasoning;
const userSituation = userProfile?.career_situation?.toLowerCase(); const userSituation = userProfile?.career_situation?.toLowerCase();
const userStatus = scenarioRow?.status?.toLowerCase(); const userStatus = scenarioRow?.status?.toLowerCase();
const combinedMessage = buildStatusSituationMessage(userStatus, userSituation, careerName); const combinedMessage = buildStatusSituationMessage(
userStatus,
userSituation,
careerName
);
const interestInventoryMessage = userProfile?.riasec const interestInventoryMessage = userProfile?.riasec
? `With your Interest Inventory profile (${userProfile.riasec}), I can tailor suggestions more precisely.` ? `With your Interest Inventory profile (${userProfile.riasec}), I can tailor suggestions more precisely.`
: `If you complete the Interest Inventory, Ill be able to offer more targeted suggestions based on your interests.`; : `If you complete the Interest Inventory, Ill be able to offer more targeted suggestions based on your interests.`;
const riskMessage = const riskMessage =
riskLevel && riskReasoning riskLevel && riskReasoning
? `Note: This role has a <strong>${riskLevel}</strong> automation risk over the next 10 years. ${riskReasoning}` ? `Note: This role has a <strong>${riskLevel}</strong> automation risk over the next 10 years. ${riskReasoning}`
: ""; : "";
const goalsMessage = goalsText const goalsMessage = goalsText
? `Your goals include:<br />${goalsText ? `Your goals include:<br />${goalsText
.split(/\d+\.\s?/) .split(/\d+\.\s?/)
.filter(Boolean) .filter(Boolean)
.map((goal) => `${goal.trim()}`) .map((goal) => `${goal.trim()}`)
.join("<br />")}` .join("<br />")}`
: null; : null;
const missingProfileFields = []; const missingProfileFields = [];
if (!careerName) missingProfileFields.push("career choice"); if (!scenarioRow?.career_name) missingProfileFields.push("career choice");
if (!goalsText) missingProfileFields.push("career goals"); if (!goalsText) missingProfileFields.push("career goals");
if (!userSituation) missingProfileFields.push("career phase"); if (!userSituation) missingProfileFields.push("career phase");
let advisoryMessage = ""; let advisoryMessage = "";
if (missingProfileFields.length > 0) { if (missingProfileFields.length > 0) {
advisoryMessage = `<br /><br /><em>If you provide ${ advisoryMessage = `<em>If you provide ${
missingProfileFields.length > 1 missingProfileFields.length > 1
? "a few more details" ? "a few more details"
: "this information" : "this information"
}, Ill be able to offer more tailored and precise advice.</em>`; }, Ill be able to offer more tailored and precise advice.</em>`;
} }
return { return {
role: "assistant", role: "assistant",
content: ` content: `
Hi! ${combinedMessage}<br /><br /> Hi! ${combinedMessage}<br/>
${goalsMessage ? goalsMessage + "<br /><br />" : ""} ${goalsMessage ? goalsMessage + "<br/>" : ""}
${interestInventoryMessage}<br /><br /> ${interestInventoryMessage}<br/>
${riskMessage}<br /> ${riskMessage}<br/>
${advisoryMessage}<br /> ${advisoryMessage}<br/>
I'm here to support you with personalized coaching. What would you like to focus on today? I'm here to support you with personalized coaching. What would you like to focus on today?
`, `,
};
}; };
};
const handleSendMessage = async () => { const handleSendMessage = async () => {
if (!input.trim() || loading) return; 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"); 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) => [ setMessages((prev) => [...prev, { role: "assistant", content: reply }]);
...prev,
{ role: "assistant", content: reply }, // OPTIONAL: store or use the AI risk data
]); if (riskDataFromServer && onAiRiskFetched) {
onAiRiskFetched(riskDataFromServer);
}
if ( if (
reply.includes("created those milestones") || 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> </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"> <form onSubmit={handleSubmit} className="flex space-x-2">
<input <input
type="text" type="text"

View File

@ -395,18 +395,19 @@ function CareerExplorer() {
// if not found => call server3 => store in server2. // if not found => call server3 => store in server2.
// ---------------------------------------------------- // ----------------------------------------------------
let aiRisk = null; let aiRisk = null;
const strippedSoc = socCode.split('.')[0]; const strippedSocCode = socCode.split('.')[0];
try { try {
// Check local DB first (SQLite -> server2) // 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; aiRisk = localRiskRes.data;
} catch (err) { } catch (err) {
// If 404, we call server3's ChatGPT route at the SAME base url // If 404, we call server3's ChatGPT route at the SAME base url
if (err.response && err.response.status === 404) { if (err.response && err.response.status === 404) {
try { try {
const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, { const aiRes = await axios.post(`${apiUrl}/public/ai-risk-analysis`, {
socCode: strippedSoc, socCode,
careerName: career.title, careerName: career.title,
jobDescription: description, jobDescription: description,
tasks, tasks,
@ -416,7 +417,7 @@ function CareerExplorer() {
// store it back in server2 to avoid repeated GPT calls // store it back in server2 to avoid repeated GPT calls
await axios.post(`${apiUrl}/ai-risk`, { await axios.post(`${apiUrl}/ai-risk`, {
socCode: strippedSoc, socCode,
careerName: aiRes.data.careerName, careerName: aiRes.data.careerName,
jobDescription: aiRes.data.jobDescription, jobDescription: aiRes.data.jobDescription,
tasks: aiRes.data.tasks, tasks: aiRes.data.tasks,
@ -426,7 +427,7 @@ function CareerExplorer() {
// build final object // build final object
aiRisk = { aiRisk = {
socCode: strippedSoc, socCode,
careerName: career.title, careerName: career.title,
jobDescription: description, jobDescription: description,
tasks, tasks,

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Line, Bar } from 'react-chartjs-2'; import { Line, Bar } from 'react-chartjs-2';
import axios from 'axios';
import { import {
Chart as ChartJS, Chart as ChartJS,
LineElement, LineElement,
@ -26,6 +27,8 @@ import parseAIJson from "../utils/parseAIJson.js"; // your shared parser
import './CareerRoadmap.css'; import './CareerRoadmap.css';
import './MilestoneTimeline.css'; import './MilestoneTimeline.css';
const apiUrl = process.env.REACT_APP_API_URL || '';
// -------------- // --------------
// Register ChartJS Plugins // Register ChartJS Plugins
// -------------- // --------------
@ -254,6 +257,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
const [scenarioRow, setScenarioRow] = useState(null); const [scenarioRow, setScenarioRow] = useState(null);
const [collegeProfile, setCollegeProfile] = useState(null); const [collegeProfile, setCollegeProfile] = useState(null);
const [fullSocCode, setFullSocCode] = useState(null); // new line
const [strippedSocCode, setStrippedSocCode] = useState(null); const [strippedSocCode, setStrippedSocCode] = useState(null);
const [salaryData, setSalaryData] = useState(null); const [salaryData, setSalaryData] = useState(null);
const [economicProjections, setEconomicProjections] = useState(null); const [economicProjections, setEconomicProjections] = useState(null);
@ -277,6 +281,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
const [lastClickTime, setLastClickTime] = useState(null); const [lastClickTime, setLastClickTime] = useState(null);
const RATE_LIMIT_SECONDS = 15; // adjust as needed const RATE_LIMIT_SECONDS = 15; // adjust as needed
const [buttonDisabled, setButtonDisabled] = useState(false); const [buttonDisabled, setButtonDisabled] = useState(false);
const [aiRisk, setAiRisk] = useState(null);
const { const {
projectionData: initProjData = [], projectionData: initProjData = [],
@ -417,6 +422,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
useEffect(() => { useEffect(() => {
if (!scenarioRow?.career_name || !masterCareerRatings.length) { if (!scenarioRow?.career_name || !masterCareerRatings.length) {
setStrippedSocCode(null); setStrippedSocCode(null);
setFullSocCode(null);
return; return;
} }
const lower = scenarioRow.career_name.trim().toLowerCase(); const lower = scenarioRow.career_name.trim().toLowerCase();
@ -426,11 +432,103 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
if (!found) { if (!found) {
console.warn('No matching SOC =>', scenarioRow.career_name); console.warn('No matching SOC =>', scenarioRow.career_name);
setStrippedSocCode(null); setStrippedSocCode(null);
setFullSocCode(null);
return; return;
} }
setStrippedSocCode(stripSocCode(found.soc_code)); setStrippedSocCode(stripSocCode(found.soc_code));
setFullSocCode(found.soc_code);
}, [scenarioRow, masterCareerRatings]); }, [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 // 6) Salary
useEffect(() => { useEffect(() => {
if (!strippedSocCode) { if (!strippedSocCode) {
@ -456,6 +554,7 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
})(); })();
}, [strippedSocCode, userArea, apiURL]); }, [strippedSocCode, userArea, apiURL]);
// 7) Econ // 7) Econ
useEffect(() => { useEffect(() => {
if (!strippedSocCode || !userState) { if (!strippedSocCode || !userState) {
@ -832,7 +931,13 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
onMilestonesCreated={() => { onMilestonesCreated={() => {
/* refresh or reload logic here */ /* refresh or reload logic here */
}} }}
/>
onAiRiskFetched={(riskData) => {
// store it in local state
setAiRisk(riskData);
}}
/>
{/* 1) Then your "Where Am I Now?" */} {/* 1) Then your "Where Am I Now?" */}
<h2 className="text-2xl font-bold mb-4">Where you are now and where you are going.</h2> <h2 className="text-2xl font-bold mb-4">Where you are now and where you are going.</h2>
@ -849,6 +954,14 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
{yearsInCareer === '<1' ? 'year' : 'years'} {yearsInCareer === '<1' ? 'year' : 'years'}
</p> </p>
)} )}
{aiRisk?.riskLevel && (
<p className="text-center mt-2">
<strong>AI Automation Risk:</strong>{' '}
{aiRisk.riskLevel} <br />
<em>{aiRisk.reasoning}</em>
</p>
)}
</div> </div>
{/* 2) Salary Benchmarks */} {/* 2) Salary Benchmarks */}
@ -1009,7 +1122,7 @@ if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
/> />
{/* (E1) Interest Strategy */} {/* (E1) Interest Strategy */}
<label className="ml-4 font-medium">Interest Strategy:</label> <label className="ml-4 font-medium">Interest Rate:</label>
<select <select
value={interestStrategy} value={interestStrategy}
onChange={(e) => setInterestStrategy(e.target.value)} onChange={(e) => setInterestStrategy(e.target.value)}

Binary file not shown.