From c275804fbc545da87f209cb1685d2a17cd76c3f3 Mon Sep 17 00:00:00 2001 From: AG Date: Mon, 15 Dec 2025 22:46:04 +0200 Subject: [PATCH] AI Plan Generation. Clear button sets focus --- server/prisma/dev.db | Bin 475136 -> 475136 bytes server/test.db | Bin 90112 -> 90112 bytes specs/gymflow-test-plan.md | 32 +++ specs/requirements.md | 17 ++ src/components/FilledInput.tsx | 4 + src/components/Plans.tsx | 329 +++++++++++++++++++++++++++- src/components/Tracker/IdleView.tsx | 20 +- src/services/geminiService.ts | 105 +++++++++ src/services/i18n.ts | 44 ++++ 9 files changed, 542 insertions(+), 9 deletions(-) diff --git a/server/prisma/dev.db b/server/prisma/dev.db index 043723ef30d584d7aac751c40dcf1a0d7e6877fd..147078e4470a742d9ddb15574485c605db41b361 100644 GIT binary patch delta 2911 zcma)8ZLAd48J^jlJ-aizJLAWt2y*WQT?8=)&*z+Th$g@l*V;;yx|$Sz+&S|RHL*&f zsbBHl4c=h<;TOiS)mW4IQ7vcz_1fqUO8iq|{U8lB(G*C^pEg2LVobC|&+J;bnwv1Y z**~+-Jm-0z_j%u$>FLqw>Cwk8w7X9%nXtS4$(c!eX0kFfS$%8Lc`;adQ)QVI913;? z4+Wb895noY`fvJ=_>nJtzkRIzUVBI7Wc#+tU-Q-HI2TM+N1MNBu4>xer`{WtyS*82 zOSS7!&vTDfe&)`)kGiRQg?oPE-;KXimNoLmuNoT~OX?@<2kI}>e_g+>-T~i$gWy## z4Q>Xjt6M>>_IYh@?UBmwYEezp+Rne6cbzAlTb;Ge!s_wr`_*UdzYV|E@bknoZ|6s* z9cPMWOlU?aj1`X{BXJ6KLNpXiW)a0%f~d0gT9*AGN;E~B#E>d&kg<%RL<&KjWlE%o z2}-cF#~@#nDwR4mBnoi>S;7>QER#@49tuoJXjrV>26-RK|97slYKp~L<4l=DnS@M- z1V%D4^fW;-Hh+qQT6-_8_9`sS5K1BnBP24z95Se4Okt{7lprN!h*N7%fuxBJHP0|i zP;4{^si4-GHo9@j7~(i&)Ow>p3Oa>jp$Ufq8x)HOg)&7PYAj+wg^rct*6RfnD=oC* z1gcbrkf|_&l9=jJB(;dS%&^ebt^ra~NkT#(l^KOBl!Z^mU=>MBcpSy%vi0gfDAU&w z!DR>)RoqlB1e8e>K|!)ibCM=nQtJ-|l(86b9y1tHj7_g1X?&s~aVQewlZi!I1juAV zN*Y6ixgdh3m?heJvnW)lOoWjsR4QzcNuo_CO&}QOp)nwnLU3z$fs7)qQIr`bijnDv zHkPQ&*j}V4EK-Ow>rVwzPGy3nC?!zv&`{AVfjXt8mvPJ~4o&EJYV9nbnPO>7O;}=T zn!)1OkRo%25?WBIF~JeC{#YOrYILRyLmg|=J02^jLY+b$Db2WPYp99!+CXTwi5C-O z0-F+36Xi)UJ|Rp(PNmFJL8P!=87MW~M2I11LSe>%krXPODoAOZ7%$S0m_WQ#AoE+g zptJqJMUMH^W)EBhJ{;}H5@qHePk>H-WG$dgxd!t!M65PN1s9hnKL2O}EYBZY z_oLh2Gq-ojI5Iqb$$CJ#bB@0Zp*0Bg7xypqd%d1**=AGOuGPgQMrp|S=kuw z@(iP87)?0NI&r5pcJlitfIkoIcz?wiTH9UNjk2?MxS-uK57}t{l7h_rzUl?tGP7Qg zU6a3i6*zAm#zp<*1!JRdap0pY5vzi6d9F49JU1A#cW(fXEv{~{oxDB?o*lCt?_TeE zZ-slx-s%3`ea)SAZ*rmQG(KzWZakdtn*>*_8f2a^DwH;>Z!kxpFgq@jsadfxkqm{1 zhN=D3?CJ=7I%aRn>(gMVy=^Z(a&3LQe@TC_*XwnS_TIA5BozGx!wcOa7Nf)N9NZ4p zEhNZnZcHHNH{$=Ni4if|ei(dp^GL%xWd)JH-|w`yw~n>eH(&5hnG=qAVSl{Oe%QY< z`)P3SR8*p{AV@a-w0q5s^XW6g_I=vN`gE>{1zld^Sy*0#A0$8DaP?U1A5t4dygsCqH}wob!WC@bwAc zt*wqax94?KR~6@WGfbB`mv7xR(&+WnGP~iHnT;JcjCb;lA!z3Q9krj#9%c3M=6AV= B-0J`U delta 1876 zcma)6ZKxDg7@pagJu|brGiz%3b?^0~&Cg~y=ggUzbC!au-rIs<7_kc3kDZ+}1NtKr zlGMPvOJ1%>WOkvUgv!25(@y-EoxcvjodEt7xRocZf;NSpQX+pDL$suimQuq;ZosrVYKi>!OcBfC>WDQ z$9Ud|jR%Z5`mg%uda6H}TdEK1ecE;Hy!N{Gw6;;JfSb85!METzXn`%jQFelS{>S`8 z?t1=UKFkMsQ~g=}Onp(^rVi#_Q|BpHl`oVd(*HjbgBP%zUTLXndh;%I%?J&k>n5J< zA=tDD@?4t*e$(bG@M2DzP2`2r=~YTSM*>1{Ono~^sB4pu$9B^Vpbc5X5*BetU?iQ& zkO+D)ax-L1uuTYrc1UPqlO(~6Vwch=mQH3!N=9%LP#D{oi9-nts7;X@+BCvEiJ>@z zyV8ja>U$w|W1rj97hi;mYMb2kg?z$b7$Ct7rQ=h`I3b*bqNQe2XcCTXmL#qnKny7j zIf;27y_+E+bvL>&jEEftSco<`7f?cN?jyn&h8($(^iBqKSp?l6KsMz}92yIbsK>W? zQ#24$#CeRQw>vxMsN*HDIaiU&->23R^`6bvr1gn)2wb+3{5s`N>p^R7u44XXo>O<2 zFDo1JFPS^cnug4Q(%+@8OK+C;mL4swR38Gf!NZH6F-FF zopdZiCh4{sxOeRQGF3eJ&iQ5FoBm2KaYzr3fl7L15V*yl>XIsS9fbXKU@e$m>7~#v zf8W|EK4w)+oq)ROnT$`#oiobG zZ8tKLvkSMA+QyQ?G}sxXa#mbt>ncTZO*k`hI#Q{fghwe6{bD`ZO?|xkS@KW{CZ@#`>z4WJ_{qq~u`okNGR~g-rYwP+@l8txPcY0lG4mRYz z30d#n_58KB1~=CRx8vPD74&X@^`NtPo5}q8_QOvWUums15RJSe@jEUBmAfYWrG$s?B9hPuY9r=c+}uff`~Ge|81#pkf34ru zqussLz3c1uX2H}OFM%m4Y(gxA7c#~W#atk%ltBu|A(et-onU9`_L>+<)9v~@{X@}9 zMbFF+!+jp+MZub2hk&X-)ebc1-2bs}+YW+cfG)iUHoPk`dH9DOKHHQ*#7KfZl z#C0ej8;T=t4Ml`UCe|cTO2iIjTx4S-9f0xv&_4-iv)hNen?o}iIVkPki=pweejjeE zO;z5_PJxf6A-HXTZ761NY(EnpGd&!^E0F96D_L*a%i6NeI6qM%;f^}o~<4l|+qFJd_>g|sz-cygZKl9%69<@L6e(cS`Nc^EaUYhDu%M3%^9W2x8 zv+iJ-ww-YY$7Nct=1!JrmT7meOtq)nL07Qd=T+>&x28IcGF6y|G*=cX74Q2%hRh2d zT&*@covli>(z*4Lr38{}0>rdR5Vab{I7$ctR%Dbws~Z~?19rxFdm&z-Y$agJNyv{0 zaZ|=LGCTkafdn=PBgY27JVtRADa|Ao>eZhQy8Tgl*Z7-xw~N;L`W|%c(3s4Bvp;;h zpJzZ|H#SyxHe6#A9zS&##n+Rs;lFxIF61&EKgKn`*1lh9--lkx+W42_Dp1O;Uj>z| ztun3_5kKGGRi83+nD+9__}0ZFT};4mqqN`iZC~Xh$9!;YPGcWI#J&COz(0QHZXp3j z#|iM^PfagNMR&A3_J5G}(!=x@#s~h&d}*>HW1J~7R0WM59T?!U6aI9=dMMIf20uRh zGQCln-COoIog2=FLI21M!A)QFdkRp7sq&W`%RAQa2L}QiUp_1IcZxdG6&5YCnEvt=-9MlXyXn!JvRvi&W?+ngxgp z33`gh2@I8x5w)L7^Y}|Cyi9$kQ9eDv4Y-2xGMFH1okk!qz9x3PC3q-_W(w=i;hw;b z|7UfSsqfTwJ~hD&MGU~yK&SzH2+m=Q5(e8S;h>>y2sdAVdjdN?U0G%3_rjIzg2Qbz zs+59a18`8QNeK0I;?N3%cpM9c1C=P^>ifL%j#ZiZ-pQ&Cwhk;ADINw8?L7RHrfg$Ng!=`xHdxU%Xm4-MQRw NGOBr|DE{a#{0HTYMjZeE delta 149 zcmZoTz}j$tb%Hb_&qNt#MxKocZ{-*{C;Q3UnP#MyC%WhRC;OEo2kNC1y9b7c1RCjO z7zda57Nun7SR|U7R~AKCWQ3Imr8)cQryB>Cd*n_&D6h?D6k=duWoT?=VyS0tZn*iE pJeR;`mJN&wn^^zE diff --git a/specs/gymflow-test-plan.md b/specs/gymflow-test-plan.md index 661d0d7..2d176bc 100644 --- a/specs/gymflow-test-plan.md +++ b/specs/gymflow-test-plan.md @@ -333,6 +333,38 @@ Comprehensive test plan for the GymFlow web application, covering authentication **Expected Results:** - All exercises are created successfully with their respective types. +#### 2.14. A. Workout Plans - Create Plan with AI + +**File:** `tests/workout-management.spec.ts` + +**Steps:** + 1. Log in as a regular user. + 2. Navigate to the 'Plans' section. + 3. Click the '+' FAB button. + 4. Select 'With AI' option. + 5. In the AI Side Sheet, enter a prompt (e.g., 'Create a short leg workout with lunges'). + 6. Click 'Generate'. + 7. Wait for the AI response. + +**Expected Results:** + - A new plan is created and appears in the plans list. + - If 'Lunges' did not exist in the user's exercise library, it is created automatically. + - The plan contains the exercises described in the prompt. + +#### 2.15. B. Tracker - Empty State AI Prompt + +**File:** `tests/workout-management.spec.ts` + +**Steps:** + 1. Log in as a regular user with no existing plans. + 2. Navigate to the 'Tracker' section (Idle View). + 3. Verify the placeholder message "No workout plans yet." is displayed. + 4. Click the "Ask your AI coach to create one" link. + +**Expected Results:** + - User is navigated to the Plans view. + - The AI Side Sheet is automatically opened. + ### 3. III. Workout Tracking **Seed:** `tests/workout-tracking.spec.ts` diff --git a/specs/requirements.md b/specs/requirements.md index 1916058..663d853 100644 --- a/specs/requirements.md +++ b/specs/requirements.md @@ -64,6 +64,23 @@ Users can structure their training via Plans. * **Logic**: Supports reordering capabilities via drag-and-drop in UI. * **3.2.2 Plan Deletion** * Standard soft or hard delete (Cascades to PlanExercises). +* **3.2.3 AI Plan Creation** + * **Trigger**: "Create with AI" option in Plans FAB Menu, or "Ask your AI coach" link from Tracker (when no plans exist). + * **UI Flow**: + * Opens a dedicated Side Sheet in the Plans view. + * User enters a text prompt describing desired workout (e.g., "Create a 20-minute HIIT workout"). + * "Generate" button initiates AI call. + * **AI Logic**: + * System sends prompt to AI service (`geminiService`). + * AI returns a structured JSON object containing: `name`, `description`, and `exercises` array. + * Each exercise object contains: `name`, `isWeighted` (boolean), `restTimeSeconds` (number). + * For **new exercises** (not in user's library), AI also provides: `type` ('reps' or 'time'), `unilateral` (boolean). + * **Auto-Creation of Exercises**: + * System parses AI response. + * For each exercise in the response, checks if it exists in the user's exercise library by name. + * If not found, creates a new `Exercise` record with AI-provided attributes (type, unilateral flag) via `saveExercise`. + * Links the new/existing exercise ID to the plan step. + * **Result**: Saves the generated `WorkoutPlan` to DB and displays it in the Plans list. ### 3.3. Exercise Library * **3.3.1 Exercise Types** diff --git a/src/components/FilledInput.tsx b/src/components/FilledInput.tsx index 76485ba..dab88f1 100644 --- a/src/components/FilledInput.tsx +++ b/src/components/FilledInput.tsx @@ -26,6 +26,7 @@ const FilledInput: React.FC = ({ multiline = false, rows = 3 }) => { const id = useId(); + const inputRef = React.useRef(null); const handleClear = () => { const syntheticEvent = { @@ -33,6 +34,7 @@ const FilledInput: React.FC = ({ } as React.ChangeEvent; onChange(syntheticEvent); if (onClear) onClear(); + inputRef.current?.focus(); }; return ( @@ -43,6 +45,7 @@ const FilledInput: React.FC = ({ {!multiline ? ( } id={id} type={type} step={step} @@ -59,6 +62,7 @@ const FilledInput: React.FC = ({ /> ) : (