Avoid duplicates in AI Suggestions, response parsing, checkboxes all fixed.

This commit is contained in:
Josh 2025-05-28 12:25:07 +00:00
parent 8604883bff
commit c959367f38
2 changed files with 74 additions and 22 deletions

View File

@ -323,7 +323,8 @@ app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res)
userProfile = {}, userProfile = {},
scenarioRow = {}, scenarioRow = {},
financialProfile = {}, financialProfile = {},
collegeProfile = {} collegeProfile = {},
previouslyUsedTitles = []
} = req.body; } = req.body;
// 2) Build a summary for ChatGPT // 2) Build a summary for ChatGPT
@ -335,6 +336,13 @@ app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res)
collegeProfile collegeProfile
}); });
let avoidSection = '';
if (previouslyUsedTitles.length > 0) {
avoidSection = `\nDO NOT repeat the following milestone titles:\n${previouslyUsedTitles
.map((t) => `- ${t}`)
.join('\n')}\n`;
}
// 3) Dynamically compute "today's" date and future cutoffs // 3) Dynamically compute "today's" date and future cutoffs
const now = new Date(); const now = new Date();
const isoToday = now.toISOString().slice(0, 10); // e.g. "2025-06-01" const isoToday = now.toISOString().slice(0, 10); // e.g. "2025-06-01"
@ -371,12 +379,14 @@ Respond ONLY in the requested JSON format.`
Here is the user's current situation: Here is the user's current situation:
${summaryText} ${summaryText}
Please provide exactly 3 short-term (within 6 months) and 2 long-term (13 years) milestones. Please provide exactly 3 short-term (within 6 months) and 2 long-term (13 years) milestones. Avoid any previously suggested milestones.
Each milestone must have: Each milestone must have:
- "title" (up to 5 words) - "title" (up to 5 words)
- "date" in YYYY-MM-DD format (>= ${isoToday}) - "date" in YYYY-MM-DD format (>= ${isoToday})
- "description" (1-2 sentences) - "description" (1-2 sentences)
${avoidSection}
Return ONLY a JSON array, no extra text: Return ONLY a JSON array, no extra text:
[ [

View File

@ -329,6 +329,33 @@ 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(() => {
const storedRecs = localStorage.getItem('aiRecommendations');
if (storedRecs) {
try {
const arr = JSON.parse(storedRecs);
arr.forEach((m) => {
if (!m.id) {
m.id = crypto.randomUUID();
}
});
setRecommendations(arr);
} catch (err) {
console.error('Error parsing stored AI recs =>', err);
}
}
}, []);
useEffect(() => {
if (recommendations.length > 0) {
localStorage.setItem('aiRecommendations', JSON.stringify(recommendations));
} else {
// if it's empty, we can remove from localStorage if you want
localStorage.removeItem('aiRecommendations');
}
}, [recommendations]);
// 2) load local JSON => masterCareerRatings // 2) load local JSON => masterCareerRatings
useEffect(() => { useEffect(() => {
fetch('/careers_with_ratings.json') fetch('/careers_with_ratings.json')
@ -720,11 +747,27 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
// -- AI Handler -- // -- AI Handler --
async function handleAiClick() { async function handleAiClick() {
setAiLoading(true); setAiLoading(true);
setRecommendations([]);
setSelectedIds([]); 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 { try {
const payload = { userProfile, scenarioRow, financialProfile, collegeProfile }; 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', { const res = await authFetch('/api/premium/ai/next-steps', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -734,19 +777,17 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const data = await res.json(); const data = await res.json();
const rawText = data.recommendations || ''; const rawText = data.recommendations || '';
// Parse JSON
const arr = parseAiJson(rawText); const arr = parseAiJson(rawText);
setRecommendations(arr);
setRecommendations(arr);
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
} catch (err) { } catch (err) {
console.error('Error fetching AI recommendations:', err); console.error('Error fetching AI next steps =>', err);
} finally { } finally {
setAiLoading(false); setAiLoading(false);
} }
} }
// Check/uncheck a recommendation
function handleToggle(recId) { function handleToggle(recId) {
setSelectedIds((prev) => { setSelectedIds((prev) => {
if (prev.includes(recId)) { if (prev.includes(recId)) {
@ -759,17 +800,17 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
async function handleCreateSelectedMilestones() { async function handleCreateSelectedMilestones() {
if (!careerProfileId) return; if (!careerProfileId) return;
const confirm = window.confirm('Convert selected AI suggestions into milestones?'); const confirm = window.confirm('Create the selected AI suggestions as milestones?');
if (!confirm) return; if (!confirm) return;
// filter out those that are checked
const selectedRecs = recommendations.filter((r) => selectedIds.includes(r.id)); const selectedRecs = recommendations.filter((r) => selectedIds.includes(r.id));
if (!selectedRecs.length) return; if (!selectedRecs.length) return;
const newMils = selectedRecs.map((rec) => ({ // Use the AI-suggested date:
const payload = selectedRecs.map((rec) => ({
title: rec.title, title: rec.title,
description: rec.description || '', description: rec.description || '',
date: new Date().toISOString().slice(0, 10), // for demonstration date: rec.date, // <-- use AI's date, not today's date
career_profile_id: careerProfileId career_profile_id: careerProfileId
})); }));
@ -777,20 +818,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const r = await authFetch('/api/premium/milestone', { const r = await authFetch('/api/premium/milestone', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ milestones: newMils }) body: JSON.stringify({ milestones: payload })
}); });
if (!r.ok) throw new Error('Failed to create new milestones'); if (!r.ok) throw new Error('Failed to create new milestones');
// re-run the projection to reflect newly inserted milestones // re-run projection to see them in the chart
await buildProjection(); await buildProjection();
alert('Milestones successfully created! Check your timeline or projection.'); // optionally clear
alert('Milestones created successfully!');
// optionally clear them
setSelectedIds([]); setSelectedIds([]);
} catch (err) { } catch (err) {
console.error('Error saving new milestones:', err); console.error('Error creating milestones =>', err);
alert('Error saving AI milestones.'); alert('Error saving new AI milestones.');
} }
} }
@ -801,6 +841,8 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
if (!simulationYearsInput.trim()) setSimulationYearsInput('20'); if (!simulationYearsInput.trim()) setSimulationYearsInput('20');
} }
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-6">
<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>
@ -981,7 +1023,7 @@ 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}>
What Should I Do Next? {buttonLabel}
</Button> </Button>
{aiLoading && <p>Generating your next steps</p>} {aiLoading && <p>Generating your next steps</p>}