From c87d723df7cc23b92b289640131f59d3573faa7a Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 9 May 2025 16:50:21 +0000 Subject: [PATCH] AI Suggested Milestones implemented. --- backend/server3.js | 105 ++++++++++++- backend/user_profile | 0 backend/user_profile.db | Bin 24576 -> 0 bytes src/components/AISuggestedMilestones.js | 189 ++++++++++++++---------- user_profile.db | Bin 106496 -> 110592 bytes 5 files changed, 219 insertions(+), 75 deletions(-) delete mode 100644 backend/user_profile delete mode 100644 backend/user_profile.db 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 0f2a2f254cc6ad33b19d3e7f1813baa435226d7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI(Pfyce90%~H>lkbS3Nf+av?qiqivu^dVDv;sy4h@O&~;I`EM+ZF?f$TKqQ;n@ zyd2_P5rv-7H3RR^nm7LBiVo6DOEw_x0WjED^P8IQ`LQ7>QSBu5o zPE);lpWaXn)8C0!OVV0Z?Dc(bwCgSH(qS!Ib?$R6IiA|lRsZ%#q8FYMP*%!2F~#>k z28a1*l=)mZCsMoij;G)jhJMF!H(y)E8}I_8pT^yL&ejWjKyOF2c66=QRxN9<)zSQc z^^kI9S(Nf6Z*pe)2aHo$%!#sCS{5q9RKjHaf)wRTPCp0VZx*z)SN zGlp8~tQcBlL;Ryh%(^Gm?_%k@UA1j9 zU+rHi2iNH&Ghs3{mF6eQ0b951TI&4QTNqo~c9+NI>#asQnMy9?1;IAermfCZ)TZ{1 zR@xn-xoyqKEo)CV%>>O0;pvET0t|OdxMS`c_vJcggSHTW00bZa0SG_<0uX=z1Rwwb z2;7(e{p-ldOIzG$(_weLU|^-def}Tdz7y_;^MeEd2tWV=5P$##AOHafKmY;|fWWO3 z7!9z|@YzQh6AT+;yjuS|fMDvNreIZ{Ya6nz<;s=J3q>htyKl^{SyrV? z)>zCwwO>EZ*E>e<{r?lfo!nh6L { - 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 ( +
+
+ Generating AI-suggested milestones... +
+ ); + } + if (!suggestedMilestones.length) return null; return ( -
-

AI-Suggested Milestones

-
    +
    +
    +

    AI-Suggested Milestones

    + +
    + +
      {suggestedMilestones.map((m, i) => ( -
    • +
    • toggleSelect(i)} /> - {m.title} – {m.date} + {m.title} – {m.date}
    • ))}
    ); + }; export default AISuggestedMilestones; diff --git a/user_profile.db b/user_profile.db index 9aa03023e7c6cf8d1f06be7ca384baac3c9acf01..a694451c0d7e3f6138e5ae6313a5441f144b7971 100644 GIT binary patch delta 3544 zcma)8O>7%Q6!yf96T3~fp-q!E2Zm5+19tIWoSzDb{J3ePw2ks3{)EhUcWf_N?^-jv zP7OlaAfyTeQY#Qf>WNAf5|v(O~E{zh|nVo&} zz4yNFdvoR6;FaHk*G30dd_LbO{O$iJ*CR<-Ji~7G!cvd)mv}I`TmC@md*Vs>Iyiju z@U=r>=h-cv^pkW;y0yElN}*_AXagmZ$*E7szbC_f@86Zu$#72uo`%HJ+Dj31@>3MJ z9r*hGr9j|2U+9}&bSDsifb9CVzBkc}eh5Dm9QOqS3%>rLzPsW3(q8|?z(PRsFQOfP z4|)>~qQUFinOfm6`YI3{_a}F^U-0iO97VsQfOHSt-re3oJ(I_44+3bs_GbW{-@7z| zc3(a;AVBxu@Eu(2ztKCgyB+RLUiJC=H-!JR85VJ&5UKZNphOXi#SuFtUz_SiqBcPG8P$Ah$(g?Vu^`p z41VKcjdGc(Wm7eDc(kOerV6_-(J^hj&Phep3moSm4Bk@BA|}`*5ZlB?9-BpGQ;JK5 zZWj3|tf^u%rkBf9=aiordpF{No1+C)$E(y-8HCqOJP&U%HD`(Qc{{{(Cx~lsvgw|t zRk7^6Z`T80(*S793%`%NtU*HDk^wmvxh<{EVuRsSEPi@(ycOcCX;MgY-YAtdmFP0X z>x`&6?7-53N@0~l5>y6ogjJznZ#3vVjpM1A zRBR?5>#%fnyvI@6CMb>2e2U1E(R4a3M^kiKiIRMrMk!I0m_p6 zC{&I@Cr!#IltdJytuqU>LN%lOKSt0|Wep>6WDP>AE6El|!*O+s0nDCFIQAeL70Lvi zJiZlK!yZIiJ3E6@%vD+@RmT_B+y+qtYRM{z+D+Kch>v}=kbqI5tuEoo@H{0Nc=ddd%9~NE+y*wD=mgfK zag+2MCjxOr059R(hOgL>BY*^!*7fK~RTLgX35hTTQqVd`GC4gnk!qEsvvf6$gd{)i zO~S~_$x1AhismNdcr=wt&?uQs#-fTOlNhBF)A@8h(HP&&qMYS0bU1FKQ>SKi9Dw!w zMQUXO1_4Y%4cIRf#=xj75MlMQX79_9@(46 z-yc-RGT{u@ZN{JOlwhBgzo@%UZsRZGmx`a+%>CIg2~lZ-OlP1mlCiy zXtKChFj|Bw7Ldz^tBn&xc!NXFCOwLqgiwU2G;k$H-rO`QeJ*%?O%&z(KXwdky_9 zoe+2GP-w{~{XB4Y;LC%*9{A|MQs|G+=bRT`F{gR2BD1wo&4+|0R}QVF2J~I;)HqIdsZ_B zGfwwh&FBoYbo%`@jQ_T~iZDK5+*<4;C*22BMvaa-}}{wo