Added limits to aI Suggestions

This commit is contained in:
Josh 2025-05-28 15:08:32 +00:00
parent c959367f38
commit 6298eedaba

View File

@ -298,6 +298,10 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const [recommendations, setRecommendations] = useState([]); // parsed array
const [selectedIds, setSelectedIds] = useState([]); // which rec IDs are checked
const [lastClickTime, setLastClickTime] = useState(null);
const RATE_LIMIT_SECONDS = 15; // adjust as needed
const [buttonDisabled, setButtonDisabled] = useState(false);
const {
projectionData: initProjData = [],
loanPayoffMonth: initLoanMonth = null
@ -329,6 +333,14 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const userArea = userProfile?.area || 'U.S.';
const userState = getFullStateName(userProfile?.state || '') || 'United States';
useEffect(() => {
let timer;
if (buttonDisabled) {
timer = setTimeout(() => setButtonDisabled(false), RATE_LIMIT_SECONDS * 1000);
}
return () => clearTimeout(timer);
}, [buttonDisabled]);
useEffect(() => {
const storedRecs = localStorage.getItem('aiRecommendations');
if (storedRecs) {
@ -677,6 +689,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
};
});
const [clickCount, setClickCount] = useState(() => {
const storedCount = localStorage.getItem('aiClickCount');
const storedDate = localStorage.getItem('aiClickDate');
const today = new Date().toISOString().slice(0, 10);
if (storedDate !== today) {
localStorage.setItem('aiClickDate', today);
localStorage.setItem('aiClickCount', '0');
return 0;
}
return parseInt(storedCount || '0', 10);
});
const DAILY_CLICK_LIMIT = 10; // example limit per day
const hasStudentLoan = projectionData.some((p) => p.loanBalance > 0);
const annotationConfig = {};
if (loanPayoffMonth && hasStudentLoan) {
@ -745,48 +770,62 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const yearsInCareer = getYearsInCareer(scenarioRow?.start_date);
// -- AI Handler --
async function handleAiClick() {
setAiLoading(true);
setSelectedIds([]);
// gather all previously used titles from:
// A) existing recommendations
// B) accepted milestones in scenarioMilestones
// We'll pass them in the same request to /api/premium/ai/next-steps
// so the server can incorporate them in the prompt
const oldRecTitles = recommendations.map((r) => r.title.trim()).filter(Boolean);
const acceptedTitles = scenarioMilestones.map((m) => (m.title || '').trim()).filter(Boolean);
const allToAvoid = [...oldRecTitles, ...acceptedTitles];
try {
const payload = {
userProfile,
scenarioRow,
financialProfile,
collegeProfile,
previouslyUsedTitles: allToAvoid
};
// We'll rely on the server to integrate "previouslyUsedTitles" into the prompt
const res = await authFetch('/api/premium/ai/next-steps', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('AI request failed');
const data = await res.json();
const rawText = data.recommendations || '';
const arr = parseAiJson(rawText);
setRecommendations(arr);
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
} catch (err) {
console.error('Error fetching AI next steps =>', err);
} finally {
setAiLoading(false);
}
async function handleAiClick() {
if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
alert('You have reached the daily limit for suggestions.');
return;
}
if (aiLoading || clickCount >= DAILY_CLICK_LIMIT) {
alert('You have reached your daily limit of AI-generated recommendations. Please check back tomorrow.');
return;
}
setAiLoading(true);
setSelectedIds([]);
const oldRecTitles = recommendations.map(r => r.title.trim()).filter(Boolean);
const acceptedTitles = scenarioMilestones.map(m => (m.title || '').trim()).filter(Boolean);
const allToAvoid = [...oldRecTitles, ...acceptedTitles];
try {
const payload = {
userProfile,
scenarioRow,
financialProfile,
collegeProfile,
previouslyUsedTitles: allToAvoid
};
const res = await authFetch('/api/premium/ai/next-steps', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('AI request failed');
const data = await res.json();
const rawText = data.recommendations || '';
const arr = parseAiJson(rawText);
setRecommendations(arr);
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
// Update click count
setClickCount(prev => {
const newCount = prev + 1;
localStorage.setItem('aiClickCount', newCount);
return newCount;
});
} catch (err) {
console.error('Error fetching AI next steps =>', err);
} finally {
setAiLoading(false);
}
}
function handleToggle(recId) {
setSelectedIds((prev) => {
@ -844,25 +883,22 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
return (
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6">
<h2 className="text-2xl font-bold mb-4">Where Am I Now?</h2>
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-4">
<h2 className="text-2xl font-bold mb-4">Where Am I Now?</h2>
{/* 1) Career */}
<div className="bg-white p-4 rounded shadow mb-4">
<div className="mt-4">
<p>
<strong>Current Career:</strong>{' '}
{scenarioRow?.career_name || '(Select a career)'}
</p>
{yearsInCareer && (
<p>
<strong>Time in this career:</strong> {yearsInCareer}{' '}
{yearsInCareer === '<1' ? 'year' : 'years'}
</p>
)}
</div>
</div>
{/* 1) Career */}
<div className="bg-white p-4 rounded shadow mb-4 flex flex-col justify-center items-center min-h-[80px]">
<p>
<strong>Current Career:</strong>{' '}
{scenarioRow?.career_name || '(Select a career)'}
</p>
{yearsInCareer && (
<p>
<strong>Time in this career:</strong> {yearsInCareer}{' '}
{yearsInCareer === '<1' ? 'year' : 'years'}
</p>
)}
</div>
{/* 2) Salary Benchmarks */}
<div className="flex flex-col md:flex-row gap-4">
@ -874,22 +910,23 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
<p>
10th percentile:{' '}
{salaryData.regional.regional_PCT10
? `$${salaryData.regional.regional_PCT10.toLocaleString()}`
? `$${parseFloat(salaryData.regional.regional_PCT10).toLocaleString()}`
: 'N/A'}
</p>
<p>
Median:{' '}
{salaryData.regional.regional_MEDIAN
? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}`
? `$${parseFloat(salaryData.regional.regional_MEDIAN).toLocaleString()}`
: 'N/A'}
</p>
<p>
90th percentile:{' '}
{salaryData.regional.regional_PCT90
? `$${salaryData.regional.regional_PCT90.toLocaleString()}`
? `$${parseFloat(salaryData.regional.regional_PCT90).toLocaleString()}`
: 'N/A'}
</p>
<SalaryGauge
userSalary={userSalary}
percentileRow={salaryData.regional}
@ -904,19 +941,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
<p>
10th percentile:{' '}
{salaryData.national.national_PCT10
? `$${salaryData.national.national_PCT10.toLocaleString()}`
? `$${parseFloat(salaryData.national.national_PCT10).toLocaleString()}`
: 'N/A'}
</p>
<p>
Median:{' '}
{salaryData.national.national_MEDIAN
? `$${salaryData.national.national_MEDIAN.toLocaleString()}`
? `$${parseFloat(salaryData.national.national_MEDIAN).toLocaleString()}`
: 'N/A'}
</p>
<p>
90th percentile:{' '}
{salaryData.national.national_PCT90
? `$${salaryData.national.national_PCT90.toLocaleString()}`
? `$${parseFloat(salaryData.national.national_PCT90).toLocaleString()}`
: 'N/A'}
</p>
@ -1022,9 +1059,10 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
{/* 7) AI Next Steps */}
<div className="bg-white p-4 rounded shadow mt-4">
<Button onClick={handleAiClick}>
{buttonLabel}
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
{buttonLabel}
</Button>
{aiLoading && <p>Generating your next steps</p>}
{/* If we have structured recs, show checkboxes */}