Avoid duplicates in AI Suggestions, response parsing, checkboxes all fixed.
This commit is contained in:
parent
8604883bff
commit
c959367f38
@ -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 (1–3 years) milestones.
|
Please provide exactly 3 short-term (within 6 months) and 2 long-term (1–3 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:
|
||||||
|
|
||||||
[
|
[
|
||||||
|
@ -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>}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user