Added limits to aI Suggestions
This commit is contained in:
parent
c959367f38
commit
6298eedaba
@ -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 */}
|
||||
|
Loading…
Reference in New Issue
Block a user