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 = {},
scenarioRow = {},
financialProfile = {},
collegeProfile = {}
collegeProfile = {},
previouslyUsedTitles = []
} = req.body;
// 2) Build a summary for ChatGPT
@ -335,6 +336,13 @@ app.post('/api/premium/ai/next-steps', authenticatePremiumUser, async (req, res)
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
const now = new Date();
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:
${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:
- "title" (up to 5 words)
- "date" in YYYY-MM-DD format (>= ${isoToday})
- "description" (1-2 sentences)
${avoidSection}
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 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
useEffect(() => {
fetch('/careers_with_ratings.json')
@ -720,11 +747,27 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
// -- AI Handler --
async function handleAiClick() {
setAiLoading(true);
setRecommendations([]);
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 };
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' },
@ -734,19 +777,17 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const data = await res.json();
const rawText = data.recommendations || '';
// Parse JSON
const arr = parseAiJson(rawText);
setRecommendations(arr);
setRecommendations(arr);
localStorage.setItem('aiRecommendations', JSON.stringify(arr));
} catch (err) {
console.error('Error fetching AI recommendations:', err);
console.error('Error fetching AI next steps =>', err);
} finally {
setAiLoading(false);
}
}
// Check/uncheck a recommendation
function handleToggle(recId) {
setSelectedIds((prev) => {
if (prev.includes(recId)) {
@ -759,17 +800,17 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
async function handleCreateSelectedMilestones() {
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;
// filter out those that are checked
const selectedRecs = recommendations.filter((r) => selectedIds.includes(r.id));
if (!selectedRecs.length) return;
const newMils = selectedRecs.map((rec) => ({
// Use the AI-suggested date:
const payload = selectedRecs.map((rec) => ({
title: rec.title,
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
}));
@ -777,20 +818,19 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
const r = await authFetch('/api/premium/milestone', {
method: 'POST',
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');
// re-run the projection to reflect newly inserted milestones
// re-run projection to see them in the chart
await buildProjection();
alert('Milestones successfully created! Check your timeline or projection.');
// optionally clear them
// optionally clear
alert('Milestones created successfully!');
setSelectedIds([]);
} catch (err) {
console.error('Error saving new milestones:', err);
alert('Error saving AI milestones.');
console.error('Error creating milestones =>', err);
alert('Error saving new AI milestones.');
}
}
@ -801,6 +841,8 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
if (!simulationYearsInput.trim()) setSimulationYearsInput('20');
}
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>
@ -981,7 +1023,7 @@ export default function MilestoneTracker({ selectedCareer: initialCareer }) {
{/* 7) AI Next Steps */}
<div className="bg-white p-4 rounded shadow mt-4">
<Button onClick={handleAiClick}>
What Should I Do Next?
{buttonLabel}
</Button>
{aiLoading && <p>Generating your next steps</p>}