From 8288cb9f5ed87f0b41b9939bab795daf8fb141f2 Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 8 Sep 2025 16:10:03 -0500 Subject: [PATCH] fixed age to be a single number instead of range, fixed language for AI model to include thematic analysis, and added review/revert/save function to AI persona modification mechanism --- .DS_Store | Bin 14340 -> 14340 bytes .../__pycache__/personas.cpython-313.pyc | Bin 13306 -> 13703 bytes backend/app/routes/personas.py | 14 +- .../ai_persona_service.cpython-313.pyc | Bin 27751 -> 28803 bytes ...rsona_modification_service.cpython-313.pyc | Bin 10288 -> 10557 bytes backend/app/services/ai_persona_service.py | 28 ++++ .../services/persona_modification_service.py | 17 ++- backend/prompts/persona-basic-generation.md | 2 + .../prompts/persona-detailed-generation.md | 4 +- dist/index.html | 4 +- src/components/FocusGroupModerator.tsx | 2 +- .../persona/PersonaModificationModal.tsx | 27 ++-- src/components/persona/PersonaProfile.tsx | 135 +++++++++++++----- src/hooks/usePersonaDetails.ts | 53 ++++++- src/lib/api.ts | 3 +- src/pages/FocusGroupSession.tsx | 2 +- 16 files changed, 226 insertions(+), 65 deletions(-) diff --git a/.DS_Store b/.DS_Store index f32eb59739fc834528c266529377b51692cfe112..ec5e5dcc84d5e2e9cd0df31e4f78b2f406901a1c 100644 GIT binary patch delta 63 xcmZoEXerq6UqHmfNJqig)UsAbq1w>M49GDvHk`a$(Rj0)U?O(e&ASyn#Q;-Z5o-Vd delta 63 zcmZoEXerq6UqHmvNJqig(zI4bq1w>M(nLqW%))Z=ZbjqGZi0!}WjF6u^b`XCR_zgy diff --git a/backend/app/routes/__pycache__/personas.cpython-313.pyc b/backend/app/routes/__pycache__/personas.cpython-313.pyc index c7bd2b98c552821394ebd25faecc467986d502e9..4e30391516fe06cbf53dff6845f351d043713459 100644 GIT binary patch delta 1333 zcmZ8gTTCNW7(S=d>1EmuT`Wv56b2|Qg|a|O-7I9;EPLd>uQ+Pa!*`-eSsi}uoHffUEZXpUiXsqAA|;wdTC@nw=Pj3k0Ou$G7T~+oB<%#M?U9Z-+_6`+ zr|6oI;1lhsGqp&x)i%R^{dR3ly!sI0RBX#gJ%Q#V43 z$e>`4-&CxLW_`@}Y-|v<;bbQ(M*L zp_>9OP;Eeg=O_h6dFkWLpZF)?3w``eOssE4-l<asgN^tqAF5Om(fti>PX31zd^FZ zR71SHWUrVnVOn7EsO7bf-s8ulnR!{w4D-u&^mE5Z(V`9Ely;SXNZ=ZQDFV|Nmc!4< znx0ps+8q3>tjd~X;Ewge%#5t-N+Fk9S`OCg9t>2Uz8W=aN#cpW70}3qthj3(sL)w} zPV{4^&sw65tdWzEIn)Cks6P~$sMJ)Zkkf|zicxB+nWt}_c9kZlR%)njmA--7k!EGB z7jK}GKY{0Yv=|C|GMKLtv4rpZ58!oB_V=KT(5=x!uJb!Ca*vCA!fw$A+~|t)`_RCx zr`P9K>_;}&JM8VHwc`85gSLVF*1>h(*6>#BOXcqL-KX~1sc&r4XsYY+=MQ~d$1ZDx z`3qRvnSERE&qjPx1$fUtwm-GF(s0z!e8>e3x$qI!y&oMt;4U7vbRXMkFLMHDx1-De zm%Hqs-R?4=?e5>s1Mk3b2s{1#dk6Zq>(yd13g7UK4}uTic+9-&!|XGNwa>$t-3bzU zr**6o2Rjgs_nUW2QNnvkVkZU(@2}t>lMGWkBV7|=YL|zY@3zypW;g62Jl>sbr}hFw z-)kZI9?oJOrl@3e^Xo80Lj~RMea1b9Pf^Pfc$vUFipSn|)QRY;m;qlz7yGU|u9Jew zol?-JeKM`%`2MEn?3A>n`TznHvjrn~z(kD~ z6JsXoK?$08HjpOjO-SKj;-OIwgcuLTix+KEjK+g;2COztGXH-6_kZ*KvomvZ^5>YL zsMl)%8A9;s__}k|@YSMKlk^&y#SaSnQWL~ojo*eM6;worsFQ#JhO14b+X5R2*(ism*8DA$m&2!8DaPA zN!u#1I#OB6BxT7|Geu|9K`o+3udj%(4>n7dY;>8 zEHNv)2*L;KZm7oR?M~}>6g{iN3CIRA=yMfaj_t#hq4XCRkw62EbD^cS>Br{+6QS~WSfWrqVCzs1*Y@g47Z zSFYvoTfV!XVoaJMU^s1F4fI^Tg5kJ4U@Evm8!-6`0$KTErvZ;TuPlZ=@Vcot2p+=T zHfE`gpl6V9&m9D}e-SwhHzJm~6F$Vm~cp+aSe(-5At_-6-2_Kk7ysHc0S> zoh6zLx1QpVD{M!bO_bjhD8EUn1iKLmdosf=#KI9A^9*pK^as8`-Us_&5dnpThA_-C Pl*o_b6TuL3nW%pQ0};@! diff --git a/backend/app/routes/personas.py b/backend/app/routes/personas.py index 4bbae4a7..8d70b67e 100644 --- a/backend/app/routes/personas.py +++ b/backend/app/routes/personas.py @@ -165,6 +165,7 @@ async def modify_persona_with_ai(persona_id): - llm_model: Model to use (defaults to 'gemini-2.5-pro') - reasoning_effort: For GPT-5 (minimal, low, medium, high) - verbosity: For GPT-5 (low, medium, high) + - preview_only: If true, returns modified data without saving to database (defaults to false) """ try: # Get request data @@ -179,8 +180,10 @@ async def modify_persona_with_ai(persona_id): llm_model = request_data.get('llm_model', 'gemini-2.5-pro') reasoning_effort = request_data.get('reasoning_effort', 'medium') verbosity = request_data.get('verbosity', 'medium') + preview_only = request_data.get('preview_only', False) - print(f"🤖 Backend: Modifying persona {persona_id} with {llm_model}") + mode_text = "previewing" if preview_only else "modifying" + print(f"🤖 Backend: {mode_text.title()} persona {persona_id} with {llm_model}") print(f"📝 Modification prompt: {modification_prompt[:100]}...") # Call the modification service @@ -189,13 +192,16 @@ async def modify_persona_with_ai(persona_id): modification_prompt=modification_prompt, llm_model=llm_model, reasoning_effort=reasoning_effort, - verbosity=verbosity + verbosity=verbosity, + preview_only=preview_only ) + success_message = "Persona preview generated successfully" if preview_only else "Persona modified successfully" return jsonify({ "success": True, - "message": "Persona modified successfully", - "persona": make_serializable(modified_persona_data) + "message": success_message, + "persona": make_serializable(modified_persona_data), + "preview_only": preview_only }), 200 except PersonaModificationError as e: diff --git a/backend/app/services/__pycache__/ai_persona_service.cpython-313.pyc b/backend/app/services/__pycache__/ai_persona_service.cpython-313.pyc index 3adfedcfd8c721dc0074b8bc227430654fbd379f..b377fea4660af24f8e97468081aaefe2c92fb80a 100644 GIT binary patch delta 2836 zcmbW2ZA@F&9menR^)-IMFWC4Mj4xm?7mP^^lt6$4SOO%x1@N|v7GkggC&AEr?Tyey z&bn-J_SURtTR`eoD*7SQq^gsp2x-%%Bq|{)5Ez%Y~=8C0|sU>J!;o)}kNk)kK~ zPnr&izZkRH4yjMyUxhC?40$C3s8#~=C@O3ma%2=m`HVwKyeG4&^C)Vd)Mq?NI{Zd5 z)#`Bhj2dd7teUAsnbAQsq&l33ld61t+O1$rK2!g3d?CxGDckS$lOO8-J(wss(WJbZ z!Nc0$p*3vNZ6h!KyRIEo;M-cStd1T>oI4TDSz}`SIWj+mD(CXpNx#7Q!)z%0P5;$U zfb~xVS>7KebbZm{@~|yLO+|$WI~rvDtPl!MTn)0qOmHkT9vWlA(W%iO&z1!}6CO9~ zs;+gp+3<7(RAr6V#myOy@^@8!QSo2a#45-WOR|h;@L5L=H<+EshDXeQu+bC2NGdBN z1VR&`NJ?>Lb}Tp(2~CGNGv+KSrj&v#e?)XhWq}kTykN$A77PBROOCZxYpy^x-bDU{ zSt&(*LfxR!ULnzFy(ytT(Ax2PdG1(RNm{0fCU??QF#qym&Vn!>+$?ThzP5ZNO;P92c?tP4c1b>^C1!n^lH}+=Q&MKz zJcq+Ort&RQ`L@X&Z%-O*cTWBE)Q-WqZE!wo#(~?3W$UtPvn{-tJAG)gS4`#Aq{}H? z!KU2)ug{_wYlAjLEYD@y1dkVySK+eJe@ z6%D;!Ww(W1YczKo=yk&dhBVeKQsTZ|klSOV*S!TjI(kFL5WQifNqxhj>}jSqip)Lr z^hP}cx>-()p2(Da68ed*vbTwT(!>zGDWUrq{IR`JB4rXK1!pA^UpDS_81wAU|MvsHX;BEF3YE9xN)0@_We( zLOx{2?-$mh5{!!KP-&vR=!|qij3?hqhR(U$gW(|Wj|2nkOpq6*!~UZZwVznN52lp< zPg7j{1^i0sF>fxfCX@JnfCfOeO7S^>0l+s1QcNH^H6!qTC=BAil)62aK(c_Z0Y(W@ z(r7dkNHM?~evDK&Js*MID6Vu_Q~{7dz&P%6xouZLP6K8DF9Uc2K~EBE#BaJR=I@i{ z8kCguep)?)i0Q(otmhJzvNI@VEZoU;Zsj@`eRo^8bF1QtU6t{6=xxJ}s$@%5a<}+h z>vnm=uD&d(e=eXoVn7WV5>T~<1k_-<9m1X+ed(6IbX#8*&q``duf=Y~cC_r4mK7bo z45bc2DNdEDHEAV&k#nKz_$D`q{(+xzb>yv^Q+_&jjv{I7MASXa*=Fj#^dylFkgDB9 zJ;-j+br`6J`KUuDeQ1}0E|in*hn%9LQTp%&8L+Vpkl`}-Is=;p6pzLth)2nuKM-9DN!*mc`55MpjDLb3!$EwQC z6ZGQ~4Cp4B`~W`IZ%XJZp2}t4f>W5qd(}1Q6J#CQtey}E(m`E@Z8PG+!pA7|7E-nAYPWQg8Dk(B47x>0*U}H0xkiZfR_N- z0P#lY_)(H3>U;;1+2mvk=q_TzR)RdpUHWKhh{QOm@e|Z4~ z9l3S}a_wZ;P-kOaBb^NiZ`}#y!84g@X(G?$otAR)OrEw}Z2CVpp4khYZnYVI3=4RE z?f6%%W;BWKwVq^NC;dx_f42JMXfDxpen~3EE+zBN%ml*$PQ_y|I1l(4;OBr}0PXyn@Wl*~1!X;FP>VHXVx>NF#^(lRPro6gE;Ii)wJ6&bCh z9Ii!SVJfZ4w6iIlWuCruJ+02PHIz&lcgBU=ZSmP#o-M}uxi-^rNlH~zB|YAxH6Q4R z74NC8Ak>ADwxY%Ag~~K7wmvlE&3|RlusF78S?Jj^RHPZPOx8ozkd@K$gEc5JZRNol qGKT4FSVM7Kh?jk5_;Yn;%q5z#*^`dqy{vJB22oO<`19r0(Z2z_9l{*| delta 2041 zcmbW1ZA?>F7{|})>21qxX$uAWA}v*3Tq$ic#wPC@N((}%BCyS&LMc~4Xp8rj4ag?G zi<#ThlPnUmgw5=WTb!FLU&YsSum!UDGX1hIce1495nd5l1eaHZO2eE?WjYW+~4ZVxpG4(jNq& zztB;XEED4QEGK2$L!EW<2;F5Z&3d9l8_Sbgq_^EyS=XNKkaXuXEvLusz{IX>4n0wv|sjkTaj0OvBXuDEoQRG;!c1T zpaWz9b^+dFNN{~4;bAF34hk``jdqumHtvJ8d}L&xFTnxJ#r-hb&fw6CVbBpkDZmAA z1Ihv2fF3}Q{$5h5J_zyqfJh zdcDt#=JK%5D4(;+L2pvA;WcmI7E)52<-MW_9K{-s>duI-|Fk%olT@odibR zyf25$R~UUdvY_LbUdVxR3ufNue48v37=2Y_p^Eb{{Wg@lqvV?ixs&B;tRr{oIHvCs z(xiwRJxP^f7&hTB&8#TLF}kgy6pzqo#bbPuwpDIXoMMCH#k6f}E~S;(iqjAlQGI0& zyw<7U zy)NWhdO)UD|Efjwpv0I*wx-4$VX#LIMV*owcUH)*!v<5;^-x zGBX`(k%FHroRrRQpBz2we!!WZ>CyuYa^&=^y31D0dBFT=!4vg9jQx1kvU&QADOZxD zqDO{}ml~!G)1hh8RN#TZndDMw)(`7JnbxZH9x~D~zurT-U{V7;6iiB!uN~X}EHgc) KA^te>4gMF+EcC?y diff --git a/backend/app/services/__pycache__/persona_modification_service.cpython-313.pyc b/backend/app/services/__pycache__/persona_modification_service.cpython-313.pyc index 03bad89375bd1aa2e19570507f94ecf6d1a06841..662000dbc63d75bb4262b226eb33d6dba73ca72d 100644 GIT binary patch delta 1581 zcmaJ=TWlLe6usm1dYxT+?IeDzli0g<;>4EHhBPsdK2R-Hb<(6so9#+X6A8JEoj8JZ z*gUGHA3=an`-d1IfmDiw5Jml=G+zh-B_++Pts)9)RaF%d{PYLhCPEco+_mEns9>bo zIddOp&b>2x<;d6l;=0qx1CEFLza4!({)HH`@*Z=KWBn=JZ=LH2f$CBDAx;%=I&eC1 zia1?EPSrgmsMaCd5I1CRx3HMh?p7;nV9Yv35=9aKLsY8_*GP5Td%uQ0cGk?P>^EZc zzGK|TOy@GEU(V~VoH@m92Ahki=S`gJ2WZT5kl{3rzUD~~j$ZPFU0i~gF#vr;<8(W{ z>yZU}*``-e;%y-nwAJgC`N3c*5kC{=SW3s}qGpS2o;chEK)wEO zP%L}9KqaFLz%+S}>17?DyEGS7{r#4{03GqldulYKS8LmtzYoyF35x;^AbHp-Hw$A^p-zN1p2!_xuZB=g{jHyhsVq_+#41Gd*H9wro<}$;|aC$PWoX$;- z=BFl=iS%?%A5kXrMf1t@L}u>K+Al2~p;SiC80krj-{e)Yc|#e`7!!Ftt;|SDOwSk7 zF=AVGeC}#!kPtT&6`2gsxN??tW2+SUnQ~0%!l(8KUWdYq-INPoAg%P1@WzQhc&Lj# zh_!q!UKX#++=&g`jSRfcJ(L?CM0T(3N#BW_T&p|zKni^%d?2hf9=LYmTF+X;(VwLL zU;k-4GP>4~`%xMz*r2ZU2hVd)2I#%WReKETPmre@9{O(m#o5QGi?Zoh44VfmMvAzhh@-@`$N1UKMrLtWOJ|r_3bTlp8X0sf#R(=aHG2*b zb{P^~d)eiLdFXP7>0RE-qJ4#!mK7V{UB|AtlhEyFSKS=CSN$xSR_n~rRhjPzGOO{H z9*Marv50R58PsA_V%fcMc82bVO8i;eJi?oRgS0oQk~8$K+zNGrl}AMx`TiP3lLCJ+SsWC5sgIEBE*J8U}F+5*uk4IuF@iLr91Du`#JZ# z_vWj~<%g7QSr&oV7o8u?|2FW65@p0Lb5Cr03y(3?gL=0vmUvwvZX+%eSBTq7vd)ya z5?>OAD3+8DDSD?5jxtA4(rO2&D7vF!pX-XLH+^_H?5jS`eyzBVaQ~*w#hiVL_%?+{ z)Ppo&H74coaSGHU=1BZDjR_zyKC@$Fbyz)okvE$E!i-$xlp7vfZsYSouJ zaM9^yBLHOyzvS#i5?*p1KpprN`P#78)l1tZ2k=vz4;KWXrz{t(J20Ck2U`?7zlMxY zxsq;4x0y)=_EBvS8a(zH{=s!vdh7OR8K>OgfZX2aKu7a5!1L%my1?3?tQZPD?w*Vq z@^h31eIS7bb4Y*;DqIXhQEdiCE??v0m)*dhR7088op1L|*wWzQP94v3G}#Lv0-)pYi|f zOI~lc-(VHccNP6@2BN+0^$(g8sfoaUCz5I8q%GTd>R75rw6bnsHRbTggkMQT6O7-JiA5ztKLQf!M$G>YKtb|`+?1T%y8Gb}ckQds7m!UC~ z#hc;tD2`w2X^u5Ti0r;t_@JdEWDsddj(lLM#!T>jns zde7J;-zD3+HnE`=Zus~7sP5U)@kZ~bLO+BaMSnFr@!NeDPBzHES5W=|^s0A!6qZni zVJ@U)0>9neli^`Su@k%!OpCb)bvcT%0(Uvam^j{-m8kE!(l)ZW5~N7g6)jHiRR!gG zxvTbsiAT-&DwzDM$ilRR8Dz$JF?*7wR~?Bg&^2JQEL~%1lBfxYG}k0Gr?ItAGUsRO zsyVanF|&0)OX79SibTZm{cJsvhVfx`Ey0uY+AvE(Yx~X8wNWuYLSGXS`4oLE#hQ48 zCR!>Ea_T~iIiYBCGm8u5g~7e?h|QnWq Dict[str, Any]: """ Modify a persona using AI based on natural language instructions. @@ -149,6 +150,7 @@ class PersonaModificationService: reasoning_effort: Reasoning effort for GPT-5 (minimal, low, medium, high) verbosity: Response verbosity for GPT-5 (low, medium, high) max_retries: Maximum number of retries for invalid responses + preview_only: If True, returns modified data without saving to database Returns: Dictionary containing the modified persona data @@ -211,13 +213,16 @@ class PersonaModificationService: sanitized_persona, modified_persona_data ) - # Update the persona in the database - success = await Persona.update(persona_id, modified_persona_data) - if not success: - raise PersonaModificationError("Failed to update persona in database") + # Update the persona in the database (only if not preview mode) + if not preview_only: + success = await Persona.update(persona_id, modified_persona_data) + if not success: + raise PersonaModificationError("Failed to update persona in database") + logger.info(f"Successfully modified persona {persona_id}") + else: + logger.info(f"Generated preview for persona {persona_id} (not saved to database)") # Return the modified persona data - logger.info(f"Successfully modified persona {persona_id}") return modified_persona_data except LLMServiceError as e: diff --git a/backend/prompts/persona-basic-generation.md b/backend/prompts/persona-basic-generation.md index 3aabd1bd..0a30d543 100644 --- a/backend/prompts/persona-basic-generation.md +++ b/backend/prompts/persona-basic-generation.md @@ -51,6 +51,8 @@ EXAMPLE_JSON_START ] EXAMPLE_JSON_END +CRITICAL AGE REQUIREMENT: The "age" field MUST contain a single, specific number (e.g., "35", "42") representing the persona's exact age. DO NOT use age ranges (e.g., "35-42", "30-35"). These are individual personas and each person has one specific age, not a range. + IMPORTANT: - Return EXACTLY {count} personas in a JSON array format - Do not include any comments (like "// Second persona") in the JSON diff --git a/backend/prompts/persona-detailed-generation.md b/backend/prompts/persona-detailed-generation.md index 6fc86608..184c38fc 100644 --- a/backend/prompts/persona-detailed-generation.md +++ b/backend/prompts/persona-detailed-generation.md @@ -35,7 +35,7 @@ EXAMPLE_JSON_START { "id": "generated-[unique-id]", "name": "[Full Name]", - "age": "[Age Range]", + "age": "[Specific Age]", "gender": "[Gender]", "occupation": "[Job Title]", "education": "[Education Level]", @@ -106,4 +106,6 @@ EXAMPLE_JSON_START } EXAMPLE_JSON_END +CRITICAL AGE REQUIREMENT: The "age" field MUST contain a single, specific number (e.g., "35", "42") as a string, representing the persona's exact age. DO NOT use age ranges (e.g., "35-42", "30-35"). These are individual personas and each person has one specific age, not a range. + IMPORTANT: Return ONLY the JSON object with no additional text, explanations, or formatting. \ No newline at end of file diff --git a/dist/index.html b/dist/index.html index 081a98f6..e505d8ce 100644 --- a/dist/index.html +++ b/dist/index.html @@ -7,8 +7,8 @@ - - + + diff --git a/src/components/FocusGroupModerator.tsx b/src/components/FocusGroupModerator.tsx index 075a1047..b7ee3682 100644 --- a/src/components/FocusGroupModerator.tsx +++ b/src/components/FocusGroupModerator.tsx @@ -1556,7 +1556,7 @@ true; - Choose which AI model to use for generating responses and discussion guides + Choose which AI model to use for generating responses, discussion guides, and thematic analysis diff --git a/src/components/persona/PersonaModificationModal.tsx b/src/components/persona/PersonaModificationModal.tsx index a61ef578..37065721 100644 --- a/src/components/persona/PersonaModificationModal.tsx +++ b/src/components/persona/PersonaModificationModal.tsx @@ -50,14 +50,14 @@ interface PersonaModificationModalProps { persona: Persona; isOpen: boolean; onClose: () => void; - onPersonaModified: (modifiedPersona: Persona) => void; + onPersonaPreview: (modifiedPersona: Persona) => void; } export default function PersonaModificationModal({ persona, isOpen, onClose, - onPersonaModified + onPersonaPreview }: PersonaModificationModalProps) { const [isProcessing, setIsProcessing] = useState(false); @@ -81,8 +81,8 @@ export default function PersonaModificationModal({ setIsProcessing(true); try { - toastService.info("Modifying persona with AI...", { - description: `Using ${values.llm_model} to process your modification request` + toastService.info("Generating persona preview...", { + description: `Using ${values.llm_model} to create a preview of your modifications` }); // Use the persona's MongoDB _id or fallback to id @@ -92,26 +92,27 @@ export default function PersonaModificationModal({ modification_prompt: values.modificationPrompt, llm_model: values.llm_model, reasoning_effort: values.reasoning_effort || 'medium', - verbosity: values.verbosity || 'medium' + verbosity: values.verbosity || 'medium', + preview_only: true }); if (response.data && response.data.persona) { - toastService.success("Persona modified successfully!", { - description: `${persona.name} has been updated with AI modifications` + toastService.success("Preview generated successfully!", { + description: `Ready to review proposed changes to ${persona.name}` }); - onPersonaModified(response.data.persona); + onPersonaPreview(response.data.persona); handleClose(); } else { throw new Error("Invalid response from server"); } } catch (error: any) { - console.error("Error modifying persona:", error); + console.error("Error generating persona preview:", error); if (error.response) { const errorMessage = error.response.data?.error || "Server error occurred"; - toastService.error("Failed to modify persona", { + toastService.error("Failed to generate preview", { description: errorMessage }); } else if (error.request) { @@ -119,7 +120,7 @@ export default function PersonaModificationModal({ description: "Unable to connect to the server" }); } else { - toastService.error("Modification failed", { + toastService.error("Preview generation failed", { description: error.message || "An unexpected error occurred" }); } @@ -283,12 +284,12 @@ export default function PersonaModificationModal({ {isProcessing ? ( <> - Processing... + Generating Preview... ) : ( <> - Process Persona Modification + Generate Preview )} diff --git a/src/components/persona/PersonaProfile.tsx b/src/components/persona/PersonaProfile.tsx index e195cc9f..32ecbae8 100644 --- a/src/components/persona/PersonaProfile.tsx +++ b/src/components/persona/PersonaProfile.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import Navigation from '@/components/Navigation'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -30,12 +30,17 @@ import { usePersonaDetails } from '@/hooks/usePersonaDetails'; export default function PersonaProfile() { const { currentPersona, + displayPersona, isEditing, isFromReview, isLoading, + isReviewMode, setIsEditing, handleGoBack, - handleSaveEdit + handleSaveEdit, + enterReviewMode, + exitReviewMode, + saveReviewedPersona } = usePersonaDetails(); const { navigationState } = useNavigation(); @@ -43,6 +48,25 @@ export default function PersonaProfile() { const [isExporting, setIsExporting] = useState(false); const [isModificationModalOpen, setIsModificationModalOpen] = useState(false); + // Navigation blocking during review mode + useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (isReviewMode) { + e.preventDefault(); + e.returnValue = "You have unsaved changes. Your modifications will be lost if you leave."; + return e.returnValue; + } + }; + + if (isReviewMode) { + window.addEventListener('beforeunload', handleBeforeUnload); + } + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [isReviewMode]); + // Fetch focus group name if coming from focus group session useEffect(() => { if (navigationState.focusGroupId && navigationState.previousRoute?.startsWith('/focus-groups/')) { @@ -196,43 +220,85 @@ export default function PersonaProfile() { )} + {/* Review Mode Banner */} + {isReviewMode && ( +
+
+
+

+ 📝 Reviewing proposed changes to {displayPersona?.name} - Save or cancel to continue +

+
+
+
+ )} +

Persona Profile

- - - + {isReviewMode ? ( + <> + + + + ) : ( + <> + + + + + )}
-
+
- +
@@ -241,23 +307,23 @@ export default function PersonaProfile() { Cooper Profile Personality Scenarios - Generation Prompts + Persona Inputs - + - + - + - +
@@ -271,10 +337,9 @@ export default function PersonaProfile() { persona={currentPersona} isOpen={isModificationModalOpen} onClose={() => setIsModificationModalOpen(false)} - onPersonaModified={(modifiedPersona) => { - // Update the current persona with the modified data - // This will refresh the persona detail view - window.location.reload(); + onPersonaPreview={(modifiedPersona) => { + // Enter review mode with the modified persona data + enterReviewMode(modifiedPersona); }} /> )} diff --git a/src/hooks/usePersonaDetails.ts b/src/hooks/usePersonaDetails.ts index 3ff1e2c4..d672bc2c 100644 --- a/src/hooks/usePersonaDetails.ts +++ b/src/hooks/usePersonaDetails.ts @@ -552,8 +552,10 @@ export function usePersonaDetails() { const navigate = useNavigate(); const { navigationState, clearNavigationState } = useNavigation(); const [currentPersona, setCurrentPersona] = useState(undefined); + const [reviewPersona, setReviewPersona] = useState(undefined); const [isFromReview, setIsFromReview] = useState(false); const [isEditing, setIsEditing] = useState(false); + const [isReviewMode, setIsReviewMode] = useState(false); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -729,14 +731,63 @@ export function usePersonaDetails() { // If we got here, the save was successful return true; }; + + const enterReviewMode = (modifiedPersona: Persona) => { + setReviewPersona(modifiedPersona); + setIsReviewMode(true); + }; + + const exitReviewMode = () => { + setReviewPersona(undefined); + setIsReviewMode(false); + }; + + const saveReviewedPersona = async () => { + if (!reviewPersona || !currentPersona) return false; + + try { + // Use the persona's MongoDB _id or fallback to id + const personaId = currentPersona._id || currentPersona.id; + + // Use the update API since we already have the modified data + const updateResponse = await personasApi.update(personaId, reviewPersona); + + if (updateResponse) { + toast.success("Persona saved successfully!"); + setCurrentPersona(reviewPersona); + exitReviewMode(); + // Give time for state update to complete before reloading + setTimeout(() => { + window.location.reload(); + }, 100); + return true; + } else { + toast.error("Failed to save persona changes"); + return false; + } + } catch (error) { + console.error("Error saving reviewed persona:", error); + toast.error("Failed to save persona changes: " + (error.message || "Unknown error")); + return false; + } + }; + + // Get the persona to display (review persona if in review mode, otherwise current persona) + const displayPersona = isReviewMode ? reviewPersona : currentPersona; return { currentPersona, + displayPersona, isEditing, isFromReview, isLoading, + isReviewMode, + reviewPersona, setIsEditing, handleGoBack, - handleSaveEdit + handleSaveEdit, + enterReviewMode, + exitReviewMode, + saveReviewedPersona }; } diff --git a/src/lib/api.ts b/src/lib/api.ts index 410e4bc5..e01fb90f 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -172,7 +172,8 @@ export const personasApi = { modifyWithAI: (id: string, modificationData: any) => { const personaId = typeof id === 'object' && id !== null ? (id as any)._id || id : id; - console.log(`Modifying persona with AI, ID: ${personaId}`); + const mode = modificationData.preview_only ? 'Previewing' : 'Modifying'; + console.log(`${mode} persona with AI, ID: ${personaId}`); return api.post(`/personas/${personaId}/modify-with-ai`, modificationData); }, diff --git a/src/pages/FocusGroupSession.tsx b/src/pages/FocusGroupSession.tsx index 9a3eb64a..5b67b923 100644 --- a/src/pages/FocusGroupSession.tsx +++ b/src/pages/FocusGroupSession.tsx @@ -2283,7 +2283,7 @@ const FocusGroupSession = () => { AI Model Settings - Choose which AI model to use for generating responses and discussion guides in this focus group. + Choose which AI model to use for generating responses, discussion guides, and thematic analysis in this focus group.