From 6298eedabac44a008ecd8c1e023f08104dbbaddc Mon Sep 17 00:00:00 2001 From: Josh Date: Wed, 28 May 2025 15:08:32 +0000 Subject: [PATCH] Added limits to aI Suggestions --- src/components/MilestoneTracker.js | 172 ++++++++++++++++++----------- 1 file changed, 105 insertions(+), 67 deletions(-) diff --git a/src/components/MilestoneTracker.js b/src/components/MilestoneTracker.js index daf1d8a..fe19cb0 100644 --- a/src/components/MilestoneTracker.js +++ b/src/components/MilestoneTracker.js @@ -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 ( -
-

Where Am I Now?

+
+

Where Am I Now?

- {/* 1) Career */} -
- -
-

- Current Career:{' '} - {scenarioRow?.career_name || '(Select a career)'} -

- {yearsInCareer && ( -

- Time in this career: {yearsInCareer}{' '} - {yearsInCareer === '<1' ? 'year' : 'years'} -

- )} -
-
+ {/* 1) Career */} +
+

+ Current Career:{' '} + {scenarioRow?.career_name || '(Select a career)'} +

+ {yearsInCareer && ( +

+ Time in this career: {yearsInCareer}{' '} + {yearsInCareer === '<1' ? 'year' : 'years'} +

+ )} +
{/* 2) Salary Benchmarks */}
@@ -874,22 +910,23 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {

10th percentile:{' '} {salaryData.regional.regional_PCT10 - ? `$${salaryData.regional.regional_PCT10.toLocaleString()}` + ? `$${parseFloat(salaryData.regional.regional_PCT10).toLocaleString()}` : 'N/A'}

Median:{' '} {salaryData.regional.regional_MEDIAN - ? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}` + ? `$${parseFloat(salaryData.regional.regional_MEDIAN).toLocaleString()}` : 'N/A'}

90th percentile:{' '} {salaryData.regional.regional_PCT90 - ? `$${salaryData.regional.regional_PCT90.toLocaleString()}` + ? `$${parseFloat(salaryData.regional.regional_PCT90).toLocaleString()}` : 'N/A'}

+ 10th percentile:{' '} {salaryData.national.national_PCT10 - ? `$${salaryData.national.national_PCT10.toLocaleString()}` + ? `$${parseFloat(salaryData.national.national_PCT10).toLocaleString()}` : 'N/A'}

Median:{' '} {salaryData.national.national_MEDIAN - ? `$${salaryData.national.national_MEDIAN.toLocaleString()}` + ? `$${parseFloat(salaryData.national.national_MEDIAN).toLocaleString()}` : 'N/A'}

90th percentile:{' '} {salaryData.national.national_PCT90 - ? `$${salaryData.national.national_PCT90.toLocaleString()}` + ? `$${parseFloat(salaryData.national.national_PCT90).toLocaleString()}` : 'N/A'}

@@ -1022,9 +1059,10 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) { {/* 7) AI Next Steps */}
- + {aiLoading &&

Generating your next steps…

} {/* If we have structured recs, show checkboxes */}