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,
|
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.
|
||||||
**************************************************/
|
**************************************************/
|
||||||
|
@ -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 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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Where you’d 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 = `You’d 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 you—there’s no "wrong" answer.
|
||||||
One portion highlights where you currently see yourself, and the other points to where you'd like to go.
|
It doesn’t 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 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();
|
`.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: AptivaAI’s 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 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();
|
`.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
10
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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 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();
|
|
||||||
|
|
||||||
// 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 you’re looking ahead to a possible future as it pertains to ${careerName}.`;
|
||||||
|
break;
|
||||||
return `${combinedDescription}\n\n${friendlyNote}`;
|
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 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, I’ll be able to offer more targeted suggestions based on your interests.`;
|
: `If you complete the Interest Inventory, I’ll 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"
|
||||||
}, I’ll be able to offer more tailored and precise advice.</em>`;
|
}, I’ll 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"
|
||||||
|
@ -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,
|
||||||
|
@ -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)}
|
||||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user