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 [recommendations, setRecommendations] = useState([]); // parsed array
|
||||||
const [selectedIds, setSelectedIds] = useState([]); // which rec IDs are checked
|
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 {
|
const {
|
||||||
projectionData: initProjData = [],
|
projectionData: initProjData = [],
|
||||||
loanPayoffMonth: initLoanMonth = null
|
loanPayoffMonth: initLoanMonth = null
|
||||||
@ -329,6 +333,14 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
const userArea = userProfile?.area || 'U.S.';
|
const userArea = userProfile?.area || 'U.S.';
|
||||||
const userState = getFullStateName(userProfile?.state || '') || 'United States';
|
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(() => {
|
useEffect(() => {
|
||||||
const storedRecs = localStorage.getItem('aiRecommendations');
|
const storedRecs = localStorage.getItem('aiRecommendations');
|
||||||
if (storedRecs) {
|
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 hasStudentLoan = projectionData.some((p) => p.loanBalance > 0);
|
||||||
const annotationConfig = {};
|
const annotationConfig = {};
|
||||||
if (loanPayoffMonth && hasStudentLoan) {
|
if (loanPayoffMonth && hasStudentLoan) {
|
||||||
@ -746,16 +771,20 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
|
|
||||||
// -- AI Handler --
|
// -- AI Handler --
|
||||||
async function handleAiClick() {
|
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);
|
setAiLoading(true);
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
|
|
||||||
// gather all previously used titles from:
|
const oldRecTitles = recommendations.map(r => r.title.trim()).filter(Boolean);
|
||||||
// A) existing recommendations
|
const acceptedTitles = scenarioMilestones.map(m => (m.title || '').trim()).filter(Boolean);
|
||||||
// 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];
|
const allToAvoid = [...oldRecTitles, ...acceptedTitles];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -767,12 +796,12 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
previouslyUsedTitles: allToAvoid
|
previouslyUsedTitles: allToAvoid
|
||||||
};
|
};
|
||||||
|
|
||||||
// We'll rely on the server to integrate "previouslyUsedTitles" into the prompt
|
|
||||||
const res = await authFetch('/api/premium/ai/next-steps', {
|
const res = await authFetch('/api/premium/ai/next-steps', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error('AI request failed');
|
if (!res.ok) throw new Error('AI request failed');
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -781,12 +810,22 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
|
|
||||||
setRecommendations(arr);
|
setRecommendations(arr);
|
||||||
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
|
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
|
||||||
|
|
||||||
|
// Update click count
|
||||||
|
setClickCount(prev => {
|
||||||
|
const newCount = prev + 1;
|
||||||
|
localStorage.setItem('aiClickCount', newCount);
|
||||||
|
return newCount;
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching AI next steps =>', err);
|
console.error('Error fetching AI next steps =>', err);
|
||||||
} finally {
|
} finally {
|
||||||
setAiLoading(false);
|
setAiLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function handleToggle(recId) {
|
function handleToggle(recId) {
|
||||||
setSelectedIds((prev) => {
|
setSelectedIds((prev) => {
|
||||||
@ -844,13 +883,11 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
|
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="milestone-tracker max-w-screen-lg mx-auto px-4 py-6 space-y-6">
|
<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>
|
<h2 className="text-2xl font-bold mb-4">Where Am I Now?</h2>
|
||||||
|
|
||||||
{/* 1) Career */}
|
{/* 1) Career */}
|
||||||
<div className="bg-white p-4 rounded shadow mb-4">
|
<div className="bg-white p-4 rounded shadow mb-4 flex flex-col justify-center items-center min-h-[80px]">
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Current Career:</strong>{' '}
|
<strong>Current Career:</strong>{' '}
|
||||||
{scenarioRow?.career_name || '(Select a career)'}
|
{scenarioRow?.career_name || '(Select a career)'}
|
||||||
@ -862,7 +899,6 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 2) Salary Benchmarks */}
|
{/* 2) Salary Benchmarks */}
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
@ -874,22 +910,23 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
<p>
|
<p>
|
||||||
10th percentile:{' '}
|
10th percentile:{' '}
|
||||||
{salaryData.regional.regional_PCT10
|
{salaryData.regional.regional_PCT10
|
||||||
? `$${salaryData.regional.regional_PCT10.toLocaleString()}`
|
? `$${parseFloat(salaryData.regional.regional_PCT10).toLocaleString()}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Median:{' '}
|
Median:{' '}
|
||||||
{salaryData.regional.regional_MEDIAN
|
{salaryData.regional.regional_MEDIAN
|
||||||
? `$${salaryData.regional.regional_MEDIAN.toLocaleString()}`
|
? `$${parseFloat(salaryData.regional.regional_MEDIAN).toLocaleString()}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
90th percentile:{' '}
|
90th percentile:{' '}
|
||||||
{salaryData.regional.regional_PCT90
|
{salaryData.regional.regional_PCT90
|
||||||
? `$${salaryData.regional.regional_PCT90.toLocaleString()}`
|
? `$${parseFloat(salaryData.regional.regional_PCT90).toLocaleString()}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<SalaryGauge
|
<SalaryGauge
|
||||||
userSalary={userSalary}
|
userSalary={userSalary}
|
||||||
percentileRow={salaryData.regional}
|
percentileRow={salaryData.regional}
|
||||||
@ -904,19 +941,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
<p>
|
<p>
|
||||||
10th percentile:{' '}
|
10th percentile:{' '}
|
||||||
{salaryData.national.national_PCT10
|
{salaryData.national.national_PCT10
|
||||||
? `$${salaryData.national.national_PCT10.toLocaleString()}`
|
? `$${parseFloat(salaryData.national.national_PCT10).toLocaleString()}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Median:{' '}
|
Median:{' '}
|
||||||
{salaryData.national.national_MEDIAN
|
{salaryData.national.national_MEDIAN
|
||||||
? `$${salaryData.national.national_MEDIAN.toLocaleString()}`
|
? `$${parseFloat(salaryData.national.national_MEDIAN).toLocaleString()}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
90th percentile:{' '}
|
90th percentile:{' '}
|
||||||
{salaryData.national.national_PCT90
|
{salaryData.national.national_PCT90
|
||||||
? `$${salaryData.national.national_PCT90.toLocaleString()}`
|
? `$${parseFloat(salaryData.national.national_PCT90).toLocaleString()}`
|
||||||
: 'N/A'}
|
: 'N/A'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -1022,9 +1059,10 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
|
|||||||
|
|
||||||
{/* 7) AI Next Steps */}
|
{/* 7) AI Next Steps */}
|
||||||
<div className="bg-white p-4 rounded shadow mt-4">
|
<div className="bg-white p-4 rounded shadow mt-4">
|
||||||
<Button onClick={handleAiClick}>
|
<Button onClick={handleAiClick} disabled={aiLoading || clickCount >= DAILY_CLICK_LIMIT}>
|
||||||
{buttonLabel}
|
{buttonLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{aiLoading && <p>Generating your next steps…</p>}
|
{aiLoading && <p>Generating your next steps…</p>}
|
||||||
|
|
||||||
{/* If we have structured recs, show checkboxes */}
|
{/* If we have structured recs, show checkboxes */}
|
||||||
|
Loading…
Reference in New Issue
Block a user