diff --git a/backend/server3.js b/backend/server3.js index 84732d8..b664559 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -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 ------------------------------------------------------------------ */ diff --git a/backend/user_profile b/backend/user_profile deleted file mode 100644 index e69de29..0000000 diff --git a/backend/user_profile.db b/backend/user_profile.db deleted file mode 100644 index 0f2a2f2..0000000 Binary files a/backend/user_profile.db and /dev/null differ diff --git a/src/components/AISuggestedMilestones.js b/src/components/AISuggestedMilestones.js index 1609387..ec2045f 100644 --- a/src/components/AISuggestedMilestones.js +++ b/src/components/AISuggestedMilestones.js @@ -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 ( +