number checks for simulator and fixed impacts to numbers.

This commit is contained in:
Josh 2025-06-12 12:31:02 +00:00
parent a76c1babd2
commit 8232fd697e
3 changed files with 167 additions and 89 deletions

View File

@ -3,6 +3,21 @@ import authFetch from "../utils/authFetch.js";
const isoToday = new Date().toISOString().slice(0,10); // top-level helper const isoToday = new Date().toISOString().slice(0,10); // top-level helper
function buildInterviewPrompt(careerName, jobDescription = "") {
return `
You are an expert interviewer for the role **${careerName}**.
Ask one challenging behavioural or technical question **specific to this career**,
wait for the candidate's reply, then:
Score the answer 15
Give concise feedback (1-2 sentences)
Ask the next question (up to 5 total)
After 5 questions or if the user types "quit interview", end the session.
Do NOT output milestones JSON.`;
}
/* ---------------------------------------------- /* ----------------------------------------------
Hidden prompts for the quick-action buttons Hidden prompts for the quick-action buttons
---------------------------------------------- */ ---------------------------------------------- */
@ -193,23 +208,40 @@ I'm here to support you with personalized coaching. What would you like to focus
/* ------------ quick-action buttons ------------- */ /* ------------ quick-action buttons ------------- */
function triggerQuickAction(type) { function triggerQuickAction(type) {
if (loading) return; if (loading) return;
// 1. Add a visible note for user *without* showing the raw system prompt if (type === "interview") {
const career = scenarioRow?.career_name || "the target role";
const desc = scenarioRow?.job_description || "";
const hiddenSystem = {
role: "system",
content: buildInterviewPrompt(career, desc) // ← dynamic prompt
};
const note = { const note = {
role: "assistant", role: "assistant",
content: content:
type === "interview" "Starting mock interview focused on **" + career + "**. Answer each question and I'll give feedback!"
? "Starting mock interview! (answer each question and Ill give feedback)"
: `Sure! Let me create a ${type === "networking" ? "Networking" : "Job-Search"} roadmap for you…`
}; };
const hiddenSystem = { role: "system", content: QUICK_PROMPTS[type] }; const updated = [...messages, note, hiddenSystem];
setMessages([...messages, note]);
const updatedHistory = [...messages, note, hiddenSystem]; callAi(updated);
setMessages([...messages, note]); // show only the friendly note return;
callAi(updatedHistory);
} }
/* networking / jobSearch unchanged */
const note = {
role: "assistant",
content:
type === "networking"
? "Sure! Let me create a Networking roadmap for you…"
: "Sure! Let me create a Job-Search roadmap for you…"
};
const hiddenSystem = { role: "system", content: QUICK_PROMPTS[type] };
const updated = [...messages, note, hiddenSystem];
setMessages([...messages, note]);
callAi(updated);
}
/* ------------ render ------------- */ /* ------------ render ------------- */
return ( return (
<div className="border rounded-lg shadow bg-white p-6 mb-6"> <div className="border rounded-lg shadow bg-white p-6 mb-6">

View File

@ -1,5 +1,21 @@
import moment from 'moment'; import moment from 'moment';
/** -------------------------------------------------
* Utility: coerce ANY input to a safe number
* - Empty string, null, undefined, NaN 0
* - Valid numeric string Number(value)
* - Already-a-number Number(value)
* Keeps everything finite so .toFixed() is safe
* -------------------------------------------------*/
const n = (val, fallback = 0) => {
const num = Number(val);
return Number.isFinite(num) ? num : fallback;
};
const num = (v) =>
v === null || v === undefined || v === '' || Number.isNaN(+v) ? 0 : +v;
/*************************************************** /***************************************************
* HELPER: Approx State Tax Rates * HELPER: Approx State Tax Rates
***************************************************/ ***************************************************/
@ -89,65 +105,95 @@ export function simulateFinancialProjection(userProfile) {
/*************************************************** /***************************************************
* 1) DESTRUCTURE USER PROFILE * 1) DESTRUCTURE USER PROFILE
***************************************************/ ***************************************************/
const { /* 1⃣ Destructure exactly the same keys you already use … */
// Basic incomes /* but add an underscore so we can soon normalise each one. */
currentSalary = 0, const {
monthlyExpenses = 0, // Basic incomes ----------------------------------------------
monthlyDebtPayments = 0, currentSalary: _currentSalary = 0,
partTimeIncome = 0, monthlyExpenses: _monthlyExpenses = 0,
extraPayment = 0, monthlyDebtPayments: _monthlyDebtPayments = 0,
partTimeIncome: _partTimeIncome = 0,
extraPayment: _extraPayment = 0,
// Student loan config // Student-loan config ----------------------------------------
studentLoanAmount = 0, studentLoanAmount: _studentLoanAmount = 0,
interestRate = 5, interestRate: _interestRate = 5,
loanTerm = 10, loanTerm: _loanTerm = 10,
loanDeferralUntilGraduation = false, loanDeferralUntilGraduation = false,
// College config // College ----------------------------------------------------
inCollege = false, // <<==== user-provided inCollege = false,
programType, programType,
hoursCompleted = 0, hoursCompleted: _hoursCompleted = 0,
creditHoursPerYear, creditHoursPerYear: _creditHoursPerYear,
calculatedTuition, calculatedTuition: _calculatedTuition,
enrollmentDate, enrollmentDate,
gradDate, gradDate,
startDate, startDate,
academicCalendar = 'monthly', academicCalendar = 'monthly',
annualFinancialAid = 0, annualFinancialAid: _annualFinancialAid = 0,
// Post-college salary // Post-college salary ----------------------------------------
expectedSalary = 0, expectedSalary: _expectedSalary = 0,
// Savings & monthly contributions // Savings & contributions ------------------------------------
emergencySavings = 0, emergencySavings: _emergencySavings = 0,
retirementSavings = 0, retirementSavings: _retirementSavings = 0,
monthlyRetirementContribution = 0, monthlyRetirementContribution:_monthlyRetirementContribution = 0,
monthlyEmergencyContribution = 0, monthlyEmergencyContribution:_monthlyEmergencyContribution = 0,
// Surplus distribution // Surplus distribution ---------------------------------------
surplusEmergencyAllocation = 50, surplusEmergencyAllocation: _surplusEmergencyAllocation = 50,
surplusRetirementAllocation = 50, surplusRetirementAllocation: _surplusRetirementAllocation = 50,
// Program length override // Other -------------------------------------------------------
programLength, programLength,
stateCode = 'GA',
milestoneImpacts = [],
simulationYears = 20,
// State code interestStrategy = 'NONE',
stateCode = 'GA', flatAnnualRate: _flatAnnualRate = 0.06,
monthlyReturnSamples = [],
randomRangeMin: _randomRangeMin = -0.02,
randomRangeMax: _randomRangeMax = 0.02
} = userProfile;
// Financial milestone impacts /* 2⃣ Immediately convert every money/percentage/count field to a real Number */
milestoneImpacts = [], const currentSalary = num(_currentSalary);
const monthlyExpenses = num(_monthlyExpenses);
const monthlyDebtPayments = num(_monthlyDebtPayments);
const partTimeIncome = num(_partTimeIncome);
const extraPayment = num(_extraPayment);
// Simulation duration const studentLoanAmount = num(_studentLoanAmount);
simulationYears = 20, const interestRate = num(_interestRate);
const loanTerm = num(_loanTerm);
interestStrategy = 'NONE', // 'NONE' | 'FLAT' | 'MONTE_CARLO' const hoursCompleted = num(_hoursCompleted);
flatAnnualRate = 0.06, // 6% default if using FLAT const creditHoursPerYear = num(_creditHoursPerYear);
monthlyReturnSamples = [], // if using historical-based random sampling const calculatedTuition = num(_calculatedTuition);
randomRangeMin = -0.02, // if using a random range approach const annualFinancialAid = num(_annualFinancialAid);
randomRangeMax = 0.02, const expectedSalary = num(_expectedSalary);
} = userProfile; const emergencySavings = num(_emergencySavings);
const retirementSavings = num(_retirementSavings);
const monthlyRetirementContribution= num(_monthlyRetirementContribution);
const monthlyEmergencyContribution = num(_monthlyEmergencyContribution);
const surplusEmergencyAllocation = num(_surplusEmergencyAllocation);
const surplusRetirementAllocation = num(_surplusRetirementAllocation);
const flatAnnualRate = num(_flatAnnualRate);
const randomRangeMin = num(_randomRangeMin);
const randomRangeMax = num(_randomRangeMax);
/* -------------------------------------------------
* Use the Safe variables below instead of the
* raw ones whenever youll call .toFixed() or do
* arithmetic. All names are preserved; only the
* Safe suffix distinguishes the sanitized values.
* -------------------------------------------------*/
/*************************************************** /***************************************************
* HELPER: Retirement Interest Rate * HELPER: Retirement Interest Rate
@ -325,40 +371,40 @@ for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
} }
/************************************************ /************************************************
* 7.3 MILESTONE IMPACTS * 7.3 MILESTONE IMPACTS strict number handling
************************************************/ ************************************************/
let extraImpactsThisMonth = 0; let extraImpactsThisMonth = 0;
milestoneImpacts.forEach((impact) => {
const startDateClamped = moment(impact.start_date).startOf('month');
let startOffset = startDateClamped.diff(scenarioStartClamped, 'months');
if (startOffset < 0) startOffset = 0;
let endOffset = Infinity; milestoneImpacts.forEach((rawImpact) => {
if (impact.end_date && impact.end_date.trim() !== '') { /* --- safety / coercion ------------------------------------------------ */
const endDateClamped = moment(impact.end_date).startOf('month'); const amount = Number(rawImpact.amount) || 0; // ← always a number
endOffset = endDateClamped.diff(scenarioStartClamped, 'months'); const type = (rawImpact.impact_type || 'MONTHLY').toUpperCase(); // 'ONE_TIME' | 'MONTHLY'
if (endOffset < 0) endOffset = 0; const direction = (rawImpact.direction || 'subtract').toLowerCase(); // 'add' | 'subtract'
}
/* --- date math -------------------------------------------------------- */
const startDateClamped = moment(rawImpact.start_date).startOf('month');
let startOffset = startDateClamped.diff(scenarioStartClamped, 'months');
if (startOffset < 0) startOffset = 0;
let endOffset = Infinity;
if (rawImpact.end_date && rawImpact.end_date.trim() !== '') {
const endDateClamped = moment(rawImpact.end_date).startOf('month');
endOffset = endDateClamped.diff(scenarioStartClamped, 'months');
if (endOffset < 0) endOffset = 0;
}
/* --- apply impact ----------------------------------------------------- */
const applyAmount = (dir) =>
dir === 'add' ? (baseMonthlyIncome += amount) : (extraImpactsThisMonth += amount);
if (type === 'ONE_TIME') {
if (monthIndex === startOffset) applyAmount(direction);
} else {
// MONTHLY (or anything else) apply for the whole span
if (monthIndex >= startOffset && monthIndex <= endOffset) applyAmount(direction);
}
});
if (impact.impact_type === 'ONE_TIME') {
if (monthIndex === startOffset) {
if (impact.direction === 'add') {
baseMonthlyIncome += impact.amount;
} else {
extraImpactsThisMonth += impact.amount;
}
}
} else {
// 'MONTHLY'
if (monthIndex >= startOffset && monthIndex <= endOffset) {
if (impact.direction === 'add') {
baseMonthlyIncome += impact.amount;
} else {
extraImpactsThisMonth += impact.amount;
}
}
}
});
/************************************************ /************************************************
* 7.4 CALCULATE TAXES * 7.4 CALCULATE TAXES

Binary file not shown.