AI Suggested Milestones implemented.

This commit is contained in:
Josh 2025-05-09 16:50:21 +00:00
parent 7f71b0357f
commit bed6b92906
5 changed files with 219 additions and 75 deletions

View File

@ -46,7 +46,8 @@ const initDB = async () => {
initDB();
app.use(helmet());
app.use(express.json());
app.use(express.json({ limit: '5mb' }));
const allowedOrigins = ['https://dev1.aptivaai.com'];
app.use(cors({ origin: allowedOrigins, credentials: true }));
@ -1180,6 +1181,108 @@ app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res
}
});
app.post('/api/premium/milestone/ai-suggestions', authenticatePremiumUser, async (req, res) => {
const { career, projectionData, existingMilestones, careerPathId, regenerate } = req.body;
if (!career || !careerPathId || !projectionData || projectionData.length === 0) {
return res.status(400).json({ error: 'career, careerPathId, and valid projectionData are required.' });
}
if (!regenerate) {
const existingSuggestion = await db.get(`
SELECT suggested_milestones FROM ai_suggested_milestones
WHERE user_id = ? AND career_path_id = ?
`, [req.userId, careerPathId]);
if (existingSuggestion) {
return res.json({ suggestedMilestones: JSON.parse(existingSuggestion.suggested_milestones) });
}
}
// Explicitly regenerate (delete existing cached suggestions if any)
await db.run(`
DELETE FROM ai_suggested_milestones WHERE user_id = ? AND career_path_id = ?
`, [req.userId, careerPathId]);
const existingMilestonesContext = existingMilestones?.map(m => `- ${m.title} (${m.date})`).join('\n') || 'None';
const prompt = `
You will provide exactly 5 milestones for a user who is preparing for or pursuing a career as a "${career}".
User Career and Context:
- Career Path: ${career}
- User Career Goals: ${careerGoals || 'Not yet defined'}
- Confirmed Existing Milestones:
${existingMilestonesContext}
Immediately Previous Suggestions (MUST explicitly avoid these):
${previousSuggestionsContext}
Financial Projection Snapshot (every 6 months, for brevity):
${projectionData.filter((_, i) => i % 6 === 0).map(m => `
- Month: ${m.month}
Salary: ${m.salary}
Loan Balance: ${m.loanBalance}
Emergency Savings: ${m.totalEmergencySavings}
Retirement Savings: ${m.totalRetirementSavings}`).join('\n')}
Milestone Requirements:
1. Provide exactly 3 SHORT-TERM milestones (within next 1-2 years).
- Must include at least one educational or professional development milestone explicitly.
- Do NOT exclusively focus on financial aspects.
2. Provide exactly 2 LONG-TERM milestones (3+ years out).
- Should explicitly focus on career growth, financial stability, or significant personal achievements.
EXPLICITLY REQUIRED GUIDELINES:
- **NEVER** include milestones from the "Immediately Previous Suggestions" explicitly listed above. You must explicitly check and explicitly ensure there are NO repeats.
- Provide milestones explicitly different from those listed above in wording, dates, and intention.
- Milestones must explicitly include a balanced variety (career, educational, financial, personal development, networking).
Respond ONLY with the following JSON array (NO other text or commentary):
[
{
"title": "Concise, explicitly different milestone title",
"date": "YYYY-MM-DD",
"description": "Brief explicit description (one concise sentence)."
}
]
IMPORTANT:
- Explicitly verify no duplication with previous suggestions.
- No additional commentary or text beyond the JSON array.
`;
try {
const completion = await openai.chat.completions.create({
model: 'gpt-4-turbo',
messages: [{ role: 'user', content: prompt }],
temperature: 0.2,
});
let content = completion?.choices?.[0]?.message?.content?.trim() || '';
content = content.replace(/^[^{[]+/, '').replace(/[^}\]]+$/, '');
const suggestedMilestones = JSON.parse(content);
const newId = uuidv4();
await db.run(`
INSERT INTO ai_suggested_milestones (id, user_id, career_path_id, suggested_milestones)
VALUES (?, ?, ?, ?)
`, [newId, req.userId, careerPathId, JSON.stringify(suggestedMilestones)]);
res.json({ suggestedMilestones });
} catch (error) {
console.error('Error regenerating AI milestones:', error);
res.status(500).json({ error: 'Failed to regenerate AI milestones.' });
}
});
/* ------------------------------------------------------------------
FINANCIAL PROJECTIONS
------------------------------------------------------------------ */

View File

Binary file not shown.

View File

@ -5,85 +5,105 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
const [suggestedMilestones, setSuggestedMilestones] = useState([]);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [aiLoading, setAiLoading] = useState(true); // Start loading state true initially
// Show a warning if projectionData is not an array
useEffect(() => {
if (!Array.isArray(projectionData)) {
console.warn('⚠️ projectionData is not an array:', projectionData);
return;
const fetchAISuggestions = async () => {
if (!career || !careerPathId || !Array.isArray(projectionData) || projectionData.length === 0) {
console.warn('Holding fetch, required data not yet available.');
setAiLoading(true);
return;
}
setAiLoading(true);
try {
const milestonesRes = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
const { milestones } = await milestonesRes.json();
const response = await authFetch('/api/premium/milestone/ai-suggestions', {
method: 'POST',
body: JSON.stringify({ career, careerPathId, projectionData, existingMilestones: milestones }),
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('Failed to fetch AI suggestions');
const data = await response.json();
setSuggestedMilestones(data.suggestedMilestones.map((m) => ({
title: m.title,
date: m.date,
description: m.description,
progress: 0,
})));
} catch (error) {
console.error('Error fetching AI suggestions:', error);
} finally {
setAiLoading(false);
}
};
fetchAISuggestions();
}, [career, careerPathId, projectionData, authFetch]);
const regenerateSuggestions = async () => {
setAiLoading(true);
try {
const milestonesRes = await authFetch(`/api/premium/milestones?careerPathId=${careerPathId}`);
const { milestones } = await milestonesRes.json();
const previouslySuggestedMilestones = suggestedMilestones;
// Explicitly reduce projection data size by sampling every 6 months
const sampledProjectionData = projectionData.filter((_, i) => i % 6 === 0);
// Fetch career goals explicitly if defined (you'll implement this later; for now send empty or placeholder)
// const careerGoals = selectedCareer?.careerGoals || '';
const response = await authFetch('/api/premium/milestone/ai-suggestions', {
method: 'POST',
body: JSON.stringify({
career,
careerPathId,
projectionData: sampledProjectionData,
existingMilestones: milestones,
previouslySuggestedMilestones,
regenerate: true,
//careerGoals, // explicitly included
}),
headers: { 'Content-Type': 'application/json' },
});
if (!response.ok) throw new Error('Failed to fetch AI suggestions');
const data = await response.json();
setSuggestedMilestones(
data.suggestedMilestones.map((m) => ({
title: m.title,
date: m.date,
description: m.description,
progress: 0,
}))
);
} catch (error) {
console.error('Error regenerating AI suggestions:', error);
} finally {
setAiLoading(false);
}
console.log('📊 projectionData sample:', projectionData.slice(0, 3));
}, [projectionData]);
};
// Generate AI-based suggestions from projectionData
useEffect(() => {
if (!career || !Array.isArray(projectionData)) return;
const suggested = [];
// Example: if retirement crosses $50k or if loans are paid off, we suggest a milestone
projectionData.forEach((monthData, index) => {
if (index === 0) return;
const prevMonth = projectionData[index - 1];
// Retirement crossing 50k
if (
monthData.totalRetirementSavings >= 50000 &&
prevMonth.totalRetirementSavings < 50000
) {
suggested.push({
title: `Reach $50k Retirement Savings`,
date: monthData.month + '-01',
progress: 0
});
}
// Loan paid off
if (monthData.loanBalance <= 0 && prevMonth.loanBalance > 0) {
suggested.push({
title: `Student Loans Paid Off`,
date: monthData.month + '-01',
progress: 0
});
}
});
// Career-based suggestions
suggested.push(
{
title: `Entry-Level ${career}`,
date: projectionData[6]?.month + '-01' || '2025-06-01',
progress: 0
},
{
title: `Mid-Level ${career}`,
date: projectionData[24]?.month + '-01' || '2027-01-01',
progress: 0
},
{
title: `Senior-Level ${career}`,
date: projectionData[60]?.month + '-01' || '2030-01-01',
progress: 0
}
);
setSuggestedMilestones(suggested);
setSelected([]);
}, [career, projectionData]);
// Toggle selection of a milestone
const toggleSelect = (index) => {
setSelected((prev) =>
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
);
};
// Confirm the selected items => POST them as new milestones
const confirmSelectedMilestones = async () => {
const milestonesToSend = selected.map((index) => {
const m = suggestedMilestones[index];
return {
title: m.title,
description: m.title,
description: m.description,
date: m.date,
progress: m.progress,
milestone_type: activeView || 'Career',
@ -100,11 +120,8 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
});
if (!res.ok) throw new Error('Failed to save selected milestones');
const data = await res.json();
console.log('Confirmed milestones:', data);
setSelected([]); // Clear selection
window.location.reload(); // Re-fetch or reload as needed
setSelected([]);
window.location.reload();
} catch (error) {
console.error('Error saving selected milestones:', error);
} finally {
@ -112,24 +129,47 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
}
};
// Explicit spinner shown whenever aiLoading is true
if (aiLoading) {
return (
<div className="mt-4 flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-500"></div>
<span className="ml-2 text-gray-600">Generating AI-suggested milestones...</span>
</div>
);
}
if (!suggestedMilestones.length) return null;
return (
<div className="suggested-milestones">
<h4>AI-Suggested Milestones</h4>
<ul>
<div className="mt-4 p-4 border rounded bg-gray-50 shadow">
<div className="flex items-center justify-between">
<h4 className="text-lg font-semibold mb-2">AI-Suggested Milestones</h4>
<Button
className="mb-2"
onClick={() => regenerateSuggestions()}
disabled={aiLoading}
variant="outline"
>
{aiLoading ? 'Regenerating...' : 'Regenerate Suggestions'}
</Button>
</div>
<ul className="space-y-1">
{suggestedMilestones.map((m, i) => (
<li key={i}>
<li key={i} className="flex items-center gap-2">
<input
type="checkbox"
className="rounded border-gray-300 text-indigo-600 shadow-sm"
checked={selected.includes(i)}
onChange={() => toggleSelect(i)}
/>
{m.title} {m.date}
<span className="text-sm">{m.title} {m.date}</span>
</li>
))}
</ul>
<Button
className="mt-3"
onClick={confirmSelectedMilestones}
disabled={loading || selected.length === 0}
>
@ -137,6 +177,7 @@ const AISuggestedMilestones = ({ userId, career, careerPathId, authFetch, active
</Button>
</div>
);
};
export default AISuggestedMilestones;

Binary file not shown.