Added AI-risk to CareerCoach, fixed infinite loop, all/most contextual information provided to CareerCoach.
This commit is contained in:
parent
765492b856
commit
a9fe5ce8bc
@ -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.
|
||||
**************************************************/
|
||||
|
@ -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
|
||||
****************************************************/
|
||||
|
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 <strong>${riskLevel}</strong> automation risk over the next 10 years. ${riskReasoning}`
|
||||
: "";
|
||||
const riskMessage =
|
||||
riskLevel && riskReasoning
|
||||
? `Note: This role has a <strong>${riskLevel}</strong> automation risk over the next 10 years. ${riskReasoning}`
|
||||
: "";
|
||||
|
||||
const goalsMessage = goalsText
|
||||
? `Your goals include:<br />${goalsText
|
||||
.split(/\d+\.\s?/)
|
||||
.filter(Boolean)
|
||||
.map((goal) => `• ${goal.trim()}`)
|
||||
.join("<br />")}`
|
||||
: null;
|
||||
const goalsMessage = goalsText
|
||||
? `Your goals include:<br />${goalsText
|
||||
.split(/\d+\.\s?/)
|
||||
.filter(Boolean)
|
||||
.map((goal) => `• ${goal.trim()}`)
|
||||
.join("<br />")}`
|
||||
: 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 = `<br /><br /><em>If you provide ${
|
||||
missingProfileFields.length > 1
|
||||
? "a few more details"
|
||||
: "this information"
|
||||
}, I’ll be able to offer more tailored and precise advice.</em>`;
|
||||
}
|
||||
let advisoryMessage = "";
|
||||
if (missingProfileFields.length > 0) {
|
||||
advisoryMessage = `<em>If you provide ${
|
||||
missingProfileFields.length > 1
|
||||
? "a few more details"
|
||||
: "this information"
|
||||
}, I’ll be able to offer more tailored and precise advice.</em>`;
|
||||
}
|
||||
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `
|
||||
Hi! ${combinedMessage}<br /><br />
|
||||
${goalsMessage ? goalsMessage + "<br /><br />" : ""}
|
||||
${interestInventoryMessage}<br /><br />
|
||||
${riskMessage}<br />
|
||||
${advisoryMessage}<br />
|
||||
I'm here to support you with personalized coaching. What would you like to focus on today?
|
||||
`,
|
||||
return {
|
||||
role: "assistant",
|
||||
content: `
|
||||
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"
|
||||
|
@ -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,
|
||||
|
@ -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?" */}
|
||||
<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'}
|
||||
</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)}
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user