AI Suggested Milestones implemented.
This commit is contained in:
parent
7f71b0357f
commit
bed6b92906
@ -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
|
||||
------------------------------------------------------------------ */
|
||||
|
Binary file not shown.
@ -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;
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user