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 = {},
|
||||
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 (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:
|
||||
- "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:
|
||||
|
||||
[
|
||||
|
@ -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>}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user