From a9fe5ce8bc517f0d231eeb40774c4080c253c881 Mon Sep 17 00:00:00 2001
From: Josh
Date: Mon, 9 Jun 2025 13:01:48 +0000
Subject: [PATCH] Added AI-risk to CareerCoach, fixed infinite loop, all/most
contextual information provided to CareerCoach.
---
backend/server2.js | 34 ++++-
backend/server3.js | 109 +++++++++-----
package-lock.json | 10 ++
package.json | 1 +
src/components/CareerCoach.js | 251 ++++++++++++++++---------------
src/components/CareerExplorer.js | 11 +-
src/components/CareerRoadmap.js | 117 +++++++++++++-
user_profile.db | Bin 151552 -> 151552 bytes
8 files changed, 356 insertions(+), 177 deletions(-)
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 && (
+