From c0abe55ccf819b41c9832eaf46f29764f18582dd Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:59:24 -0400 Subject: [PATCH 001/820] SFC-32 logo and title done (#1) --- frontend/index.html | 2 +- frontend/src/img/logo.png | Bin 0 -> 42129 bytes frontend/src/pages/layout/Layout.tsx | 5 +++-- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 frontend/src/img/logo.png diff --git a/frontend/index.html b/frontend/index.html index f158d5a2..c2bc9a4b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Chat On Your Data | Demo + Clew
diff --git a/frontend/src/img/logo.png b/frontend/src/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7ff986170b3fe152c38917b4d64a6f6bcbd42bc8 GIT binary patch literal 42129 zcmYg2cOcc@+sEGAkR5K4b!BfESJ_+HTlQ#KW!-F{lsz)CQf5YW*RBvDWRECGsBG#z zw{Q8q{yTi0bDrlp&widGMqgKrikyWU06?Xoe!&m`(kcM(Uy>5T-(-dKe*l0JXk1Xf zT;U- zdYbtlj`olR8!Np3emN&HYk>qsBT*YqR~u=p#vYlQ{`Uu4tCnPZTk;@M9;-1cA1gn5 z<^LcsII#iF8j*~OR_3(sg!13ytaa^>Fs|zDZOp=^8sJzCQTswo5}-(uz!(F zQi6Lr9`DIL)QPW8c8tzzh|VRYf9UATLUf3ZDH%aS(7a<%iFXi`=@>K~g3@0&1{J`8 z4*p}ph%N+OJ_Z$pOi1862GxR~SB{x5$O1u`kC{-T2|>^P4;0>vu-HFP058?4@E_;r zuHu%t$4f1^q=t+sU2W8w|0@=l-?a94(h1|O4^>GXnce)4TNoi`9>Qy&BxorgQ2h5R z>=8uj>w|ytg4u!sGER4V?kWxqk>h11ZizfzPD0iQ;~g)X;IfkIe@pmrCHL`i824AA z|1IH-RQ?b4ATNf4H%sX;eS$;dmh4m zh@UVa{Af;N;rk!P&%RQF$YaBW#Y}+x?a~AO9fMZ5tdLWwH zn*THotU{&w8gaa{Bov&qtp6=>=f)i`>u}5PGv2KoMl+|12pef|oL$cKmS> zb)}B|F{GWezv?PGVa<=RtT4t{oL|T2mjdJ-D5!l%Mh(y?PG#(LQOelj;(KoX|CPz zjlPOF%ixl9jPeag0n4#4VAx#k??fJha+*L;y#IkdI?M3ne~ii_1LmQN{T=e- zOV1UDpr?;Pd6OaN|G26N8AblT`(lBhq5lJAK%?|?p&e=$a4zTg%LoZ!T~hMlC`6b5 z2hl%?c@lG$f%ku#y5TlGwsDvVNJ`xQHa(eP%4u|rj|&8S^}kCuhk7ac_@7E(j3FrH zF&A=uGE8lqjzOWD=u@%s+;)0Y2(4 zuPRUJpen;gT5r7fnJ)49t8kb#c$nb-S2$ZWNS6vk&Q$G>>*QJCIIqj0_4`}nx6j!j z{ja5d{R;AG-Q4+Yy7xoz_mn+T>(*~5thpjVq>BNAo4yj=2k-+fokrm7emKJr>uS^y_-C{=g*^nn!MD5 z^_Zoz^EZA7|JsuL?Ym=>UBZFi-&4Y%A(h_PY^Ld#w;roY zyw`BhYEJ&O*lh!VAt$ZExeDyta8q7Y7Ci3^G{54>p{F<0d%II6YYhCKH9%`VcWsC? zck{gaS`$iD*{KQiaMDg#Z+0BCQ~zOGj!v*-m_%`$?p=M+yXyI=lW@*LHEd`pvMC+Y4b$Q447< zb@m=#aL!9^Y1G*%3n^$3@Dv0f9btdx#F{`QBO2AmA5SW)#fot*??!zymL!QVPt)FK z|J{4nYb~`)*phL7;)4=L`!o6xXRTPf%BZ1wh49}st;FK%TlP%WwXLKF2@}TI_qr!< z`1k7fu4kA7u>9U4WH)pBB<6NpsK9_Ha6(327#LP-R*)n~A8L3%r4eX)x)UEMS85>Fnn`)uDC{Kw~5@j?&nnd+DB9? zN1X@_s0?xC_%0t|LTK{cH$~x`8i`1-1!aflDg^+k_57?S5@l2 zsO$M1tvr^e_Q*wDEj}>49AwLNrKOGx+niuX!@CYnM z5@t*|CJolS4OGMf9$xg;x2~=~I1`yRQ+!>8+718@_2`JF(3mFVaQerSVv zhh4lAjET1Ad0V7}ot~USGaUGw+a&@UQc>rmfgJPn=TyrV zQE5jJ#VX1TofSG*Iv@lt)*S8WDvWG{qJ7`bP4568t`!|&k7Jz?`H8>{&J1R@V&Pai zucg~F`}l0pKExnH0-CK)gG6G|TAq(nS!nSv#$;9cXW;!TF?$kPe;~c$)Jx}OqFaAb zP34tcXB}$28V|Evu%pCLQ;~2d7P96Vl#WrzI(polK0l#er0@d&13gN`?K&++jtz=| zlF?95Thz!LsZ8{aV%3ACFi8WD%MFQ9fyAT_g?-wQ#D6_-|1xgD~>j!C&s`6D8&zrPtusH@OAb>Yj24Hx$qjUs6osd zhC1Hf4~{fKWZp?%Rf&H8t*5M_Bd!J})|WN#QUpwrLtSWyK*s4#v?NYNJTG;9J;Taf z#hj)ni25O`MFp*87c>O)wt2$Pr8N1z1DF)jssvmf;7Dhir!Pq$HWr0oHGOMt6P13d zffy)J?IY~{m)g4ugvVm-c0WGG1A-^)?Tm5S%Y#L?c@QtJH*XQW3YTO9xh{Ozg%)n^ zA$D>gt`;5f0ViQOfFc5NL2*7tKC8%>4PX`uPN4BoNuRw$D5c8Tz#t2ffm#=mc8buH zH>)4pw}tFHAsI@y=MSX?fTa}`X(@DcsEj`z5Z8!~2!m_r)b*ow#adPy1@IWieb*Ko z*FTLia*F8+5rB(-*bpDu;uK#QsCb{kKsk+wE}f?>}p{ zp!fPN#S%opEv88dTv8Ae0b*`8B7O|Hw63VEsNN)93aww+FIqvm?3xNfn*=;}{3)2c zdxh{E7!*Y^oP~i|t}b*^-R%ZsnRl4AlwH=5Ke%lP!N;$fH+fIiTT^!MFwz2-WCTSb zj?iN7?=$k(-8OIt#Ff6DG+TwK;>Qx2`SyU5{B}tQFenapg~^IPCaTNFzxZCd=HWN= zvcUV7tY#@M7DxJsKtjAi_;~A$mAm?Pa1vaf0F-*eO~=!>PtKhPG0%Ozg={wL(dJ%o z$~8dWqPxc%R>uZ7S@}qh-k!{(H8%yAWwNSDi8TRihIkb{>XB2xhkaRnD!i1#^H+oL z$gih#_u+w$bA>G${NG2r2!T0IXS*?!KGynL(~C`RL3mHzRGf2eHFVKkHIj)7!Us;N z+q`RfhOwCtnT8CYv1}X*bYGT!xt^v?f&eUg&nKKZxrgI9b~fD9 zA4cT1BvK3&rn=PzteJOi#o9&Zk22;dDZ*<4Sb4VQT=W=qC>}V}iH;zFhRgS76cI{K z)+}TK5}N!%lAzi-Nk9FqQ7kpU=E?Xgd&be?1NCsz`c$|vehx!N?q05TYMBstzchM54IL8AgM9*_&4z)Qi0tf#du5cR|N z`4BL%k58`)kGSSyZu%rXz!+doAQ^B4p0u>(u=aun2zFINi2dI1M}xXW`cM)u-lPzI z^C0EYXA{PgVD4qODK1M97U?H(y!(>ip1BR-QwK3&hZSUaj@;645dmO(qa~o@=*0-d z{4qW_Ohstn3Pb@5dDrb~D)sr`)^x#7I&STshYmst;*fM+{Eq{mT2+x=ZTtp{Ubgno zY6Q3&#O6mtuzaBGGvz=B0y|2b^KlXQU|WZRLSpC&CCDKtQC)YyhzqAqZEouttBdor zKv>3sx8#yMY0-IlFvpHYQNaL_?G?nt3*~DzRB|ix&d73NKlUrx!21T?iU zg^-OT+r2m{f1chuuN)tI^K!K>v|pEWrD;K$Hb;9y$E{1BeMKn!&Y=MIDBP4EW`${O z)ldPk)Mdt?r?N_lu)rd3eDuVayX7we|I;VTn5<1B?{l_At>&dp!xcSh?%?oLRk#aP|Ay zsX=0{e1PGrcP>=D8vl_P_@^PX<{|20F;Y_lsNi?HP+FgQdx{C$1d;a4R7-M2ekC#k z7%OJ(3-Q5zmtp*Jw$qRo+JQK0ipooEPMi{=n!H!>QJ(J_34n)A0B>@m1!b548i4x< zd%grHE@!Kz=RUv)xdB|>kPDv(e9oNK@~KQn$wAlX0P2!tO_U+mPALHH@OzvEP<>%F zcHWG#!-6p{5Y}FU@9QHKb3{j@5FBVy@fx*jWMDjHTi@;HvuE&*q>(9T=wY1bdq|Gz znIa(!OK_?_SAO_+ryA|dKdNWzo?-f=e+}nEF9S>uS=ANjX@v8c%=g~y=JFqn?BnY{ zvQTac?Aobrr|iGGBU*kQR6Y=Ys2|Hw`>q|TLD39)NuajEk_>XmU$EIwf@fmcFVR|= z()w`rb4ZB!kK{71FTZ}qznmo(MBSpZcbOdM4#EShHwI*1>g5NFC=Nd}`_Lco?d~N&8bHp8LC8)784QJU{1N7Sq@< zJIS1iuymuUZ?>ZBXpAxjAcLp#VY)(iZ279S8)VnUS&7Q}lJZPC&jY_yUDG4UvFOa7 zmAhU(!@@+4dP8GvFNbZkLI$p{eKKwx>aOAUw`aW{DFtk;zFF)D%~bzlUuXVA&U(LH z^w~}s&r0joZ2rO6(2;f9CjhPqqfzPVJVg~#1$bhEtwRhv6WgCW$;+=Cv?R5&OD*cO z|78CtUR#^QBrOajM-R%ILI6L^f;^PMJ_k<~9u!N-a1g3TDYr@^9=31t^-_89pjI*o@1N7@^n1};-<5yL7^;X?01!omYd)J?7wD2|rIa*ZV zU88qN4_H5fhD7o0AqxO^f3#@%nr`CRbF?48Iv9~DhM-A{X4;0dQoNTp&sCh+c4f%p z`%kftQB0f~dG!sW^Trr%8@`P**mnNKLZ}gkS0c`CmUaQHot>anXdJVH~z`8!V~4d38*rxE`BZBX`jY?}t|!qxfQ z`M+K4lFgT=n?+ie?F3lE^tPGAV>0hVDJb{s-$*;J*P7iZ39vziOc~Si)xk7cWsiHW zs~pIO=RvPvtA$1dz`zjR(#i4rHf5{(_Ly_E)rSdaAERIL`$4l6Igd#BHWF^qJOP-i zgYC~*?S500vb9I=lpC?*$Nu(O%LBlI*8T&*e^R-DU=5wDX09)+_Y%1zHP81#Z@KA{ z0C7VLi4)Z)1~TYgG8F5QR6ibNHQrP0{*uc1Pn1poEOof)*?$DQv~ZVyG+yu= z!kHE~yN=tQ5;u|WnUZDEUgf-Hv1`5Vk^=L! zykfW7qu+@EX9P8E>70W|(Ru(}xeCv-FoCR=LZd981MY+EmOiD#e!Ef;r_YVu15Ly$ z0T(JWbvh58hzY=CgU`4fwG(lC#o>dAVqNb%BR&^ zs}xw{z-a&+6wTtx8l2DP(mUPq)l2K*MFI>NX;rBPwDYj{A*?w6p1E{HVv@2iQjRnjRj?xO5S=%ZH>#B(lCf#mjA8-|Caw3***ueF^@m4&{;S5IRUP+ zFf9Gz-jC=5Uzi6Kb`6{YwjAN6>!4l&5(?TE&*`WgT5-^!`FTgtE-LopFzjwQj z>Y-?1bjm}v6R++`1XX4ik3wA$S~%Fb&(~*v1`=xK&i@amC$p~P)t&Ke6vb0drfxFs z8oYp>6Ib%O&4FFiJ%0%gQ#4)I3Icq^$*Y@T*!pb0nh4~Y_J{w)5q(bn!uhyeu>E|s z@udf)Vg88p8JCUvS=Ha+574W03HTUanV&bMHx;(5WW!JNe4G#?fvM2@sDI#I;ilAA z8${|UYqH#3^6*t#OP$#)_dHWW3>*kyQI?t~a#(0#e!w8NE6oxBr!A}g-kb@Oa1)G7 z=XUZ~yTHc+*n(!c)p2OAv%S8F^kb30zo?EZeQ8;y)@6WJy!ShAS6?Zted@XbjA8Tr?tg{Qb^nT8Az1!+t1qywaB?t8lSUK~O~2 z(fpSyi)UK&_JrMB8}#S3@!nb$)J}~xKaO_{nh1uKNuQ*d0~;T!o$Dh*H;Sz20Ve3# zH~R2lN1SU-sAYQix$)c3&ct}j#fr^>?3zn0*EYZdmHrXE;4Xxm|Jp^vd5rH)~HMH}NNxO0!zT-4CT zp(_4jG!d9sXf3qvSy{C36r^?lV8iS>64%yB>Q;O^w*)-jwW6uKB1|)`H)rY19Qmp4 z`QIEI5S6q`8obTU!d>(8*>IP;T&$oHyAx27v)@&SV0G#EF5G=_XUQ&ZDd^dW3vm2m zzcoou#>;veY3XaVV~rt7Q}(dgd#7TT^34HPh7WyvNxwxwF6p-QM6G<>36)un1H?iP%s*qjZSa8-!Ce>ozow|x zpucbY`^aRkL2`8wgR!ff*?{?c(l|e~ye*{$p9}_!(wKlt6tO{yu^J*sbG=Cd8p%tj z)w2xHi}ux2=sz;;XPd;gG;6h+8u>okQ`+Eh|+p1 z#Wp=Wa^pkP#x2Uiiv-|_N*yw##$eA*hK6nqv%q(c2SyJk^t3Ge`Ol(I zO&*bMZB_iz^4#;nC4OSIEZ=9@8`#^DiASXXx+5h1k>Jp(9B)xdt*Eg{a$5Jljn>|!KO-R_3AJ=A@JOr-f}|Y!+r))`uTr$ z(!3}>A}9tg#V`MXiX?Gw;hz2ZrbGFyYX2af;A|);hU(+iR0qP;HJ$e<${qu7D_!sp zD69#YJI4_n)UnCb$gem#RB~RIztk8h@0FTUtjly&RSp4aF3zO}#UfW=EQN#P{&(c= z!KKNv2WYmzO&14!IW6X3uOPfp@6_OIuP&%HkH|Xkl#B?2WeOahJyBr-Kupf%k7I&E z6&u{1s6M+7Z@(+l1|`5uWwGW??_!bksjuOi`}YDp@gcf%WAhZ$p0`LB9c07jWrzXx z&MDNtgX?E4R@>ti0y>^~ztKtK7Ao)A5AOds`;L|?0^|zfzwC3AIlbe+BH{f-69BgE znLnNf-szpIrj#m_{(CdFUOh1I5lHSg^M^H<(iO=gw&|EnFIG8`hm^3Ex}imF&pLN7 z@?YiLIe_Y~r6n45P)V@t6z=VX=hD5>-bcJ892k>aQkUe3lgf`L0xS>E9Ww$CVvTTx z^rK0wfAm(K73+RI_&`LZdZ_(*D<4OZHgmLV+n>T-{Po??DO3mQdSAjP4%jHkl>bNN z_*?f?)yNcw$gFE}2{n8CY?av>s$_!9d^J^ENGaf=CR^~ag;quuS5a48#0T2W&k?cE zl)>JrZ9tWd(nhSMpkBSz2l^?wJj~WwzRX3|UZ)yjJS7XZR!(Qe|3Du80fE-fR&6Cf z?YvlnoL*+=%++uA^|AXi4ByvFKa_m&1ui|%F1R@$Z>r@>8mt@P0pCiaXztpIW>_)l zgR?l=pW>;M$q`aSLZ0NhLJ>n{E4RycPGspU#(3@g%y8d=^qr%BPfA3VGGauO=)E;DYL%o9Z|YH&9- zh?Ij33&8l4F)}6T^e8-h-u!_4tJ(x&1Ye0S<#PYJyS z98qw|q(mY?VhfqYa-y+LFMyr9=1fhK3?8IFeS+)ua)wdvl7F5NBMmJ1uv1mRHlG-@ z!D3ZN_4&64h?x>s>&aoXwKMg$tfmX!kyP{V4{;R^z(spP=zDey?Wsal24xSJ*!T-~ zp2q%(UaxR^lD1wKeQ z@pA+PDcGp($uKP^go!6CR^Pfc?K55Gj?U91G-LNk(Z%~-uSXIbvd}WP3|kaR4ny2N{x=AeGe1GI$<4C2mh z_z(a&Q_2nLZW*YoREcVW^W$ zGVOIMFIUbFIl)FB#`nA!b!t1=a{$ilNK|zaGf4Dd72qAU7bJRKwUQz~`|4qVgs`M{ z(9KqxO>0-No|4P-kk7K5Td@e?h*)KYwNRk+qm{I(zJvV zBDr_cyJnP?;vCB#uvrd4%S;ZJWMR57_ykr5VU%_uA)?(1cX*kFr+tG*kTRwPUbD~c z#fxGQa;qvXN9QXH(5tce;Z6tX&INxkAk5p#hfA{6Jq#!CY{bthOPz7^y%+Wx4ymWc;hc#JoWrn&Ebm*9B)@zVnY)?C3iM zACI5jf0IlA%r{&5J#2BQa0n`L-gAQfXKwNkn^YyzCGfM;4G)EP-Opugb^um!NPWtG zcOl|(o|4e;JR!h*=|c4CofE?i;9*S+EwJ~~^65^jeCk6Su8V_@>RDD zCt``gwmTHhAv{Ww=e+S%XxNu*mC_^~hXvL-qv5hcybJr@K4E{No9GZ2x z5xFZKHm3h@RxG$omE7}Ounom^JYd6o3hTcNh>1$RZx8|O_>Qh4zB^2LJ-8)FAl+(Z zDEg0%I*bDG>ax9ag-K^VRhDyZF5e4;g&G=YP$#{?VmI0VBP}{yZwhkVW}BM-3=_(+ z);=GnKjT;XUTkt|s?X=`9V@PgI;R2z*vN$-8uFb>@XRM)aA)$v!7dAkYohrF`sV7! zE2TwL%K71vi9dMr3T=M)GhdX^D1)Icjb>DRFw9@#lTSPJOu*Jv2Aopef%@`QXa57O zg_*V7ylRm>Yp=!%V=)c#p8n=sa8M38^Vs6i2VYzl)jcMv0S?lfjxeisLm20w7m)b_;pflT75a!53kj@(m6R%_b|(#{UYm_!4%(HmGD%BQ3(R6QT8> zdgB0b4mN=M;B}pj^!$DKO{@;G@TI6fUtxnraY-huO!6PybA-b#yKg6I``KAe-g=ZF z@&U~<$(+erYnu=j@2bJiLkhk^`@0!5?&Icki?6S} z?`@nbF>vGR#7}bb37c!93QGokCW1l&4hgMHiAin_cL+3Tm~3G0j5)&WlH^Sn9%$|h z<2$r6S6Xc(jfS8|zHJo1WC4?L?|zw-}t6d|NIX1?4^;X%m=K7X z+di>&bKaAwt!8Uj_^KgNJOQVw|GPA$x8;l-8(QuruoCBoMvbfFpGKod9-^0>OCBv1 z#OP(nOhLtrNhSeqNI3a@PsrTbxSKP+=gf2oXQ6n0d|OUDt3AzKnfFlU3{*~(7B;-` zL8eTTRoT+S-zBZ;z11|6>Z<qtM34sj#u+jFpa%a!Y}xOl$`d9F_J9?K0oRz))jh$Os`QUQg|mb>}<;by#Dyo298 zL?}fDzn^&&SS|BPbsu3oS^8YmjT3+L=2X;N05M3j@^Fb+)H2F-yoFetP^Oej+Bz=) zuxG>J$o|N6SU}SIk(I{;EvK2xn$yh$pQTnL6Gu0P zhvtdlY(vmk)CNcD35;MD$!`(F#wfmFqj=&W>hgwcTj$eN@yGmHFlg-?iYhx4P(Zoa zt-zu`^f#hmsfML+ux@OhrRFXBgswFV7v1SQj-<+j(5{9`^WxLh*@Y^q!rPrYl0csT z$pDHrZi`S>l%A2$Sai^hFju$l+M$s-r%G-c^lGk9DaM{@M~A`$7Eu4l#$@(7N1C-; z&#BpJ@><&vYVaf!K>>_nd|nKco~@;ILymPvDuly(cq``5r~lZ4z|Dxx{==iFjd2wm z==e)|HyP&XYBx6zy-_Nd^sT~Kc3aJXIEKe3OOEW>+(ZHzgUyV^c94flt2Dkt7uFgP zz2D@+d)Ozo1Ak-ZTp*+(!TJ` zUT4V*{x0{7_X96rUmK5M{KN26>kF7&B%7b9818Y-k9-#*7iYlD1sfOqa0uDf?Rw(J zC^j`OEK+96xDeMXcreTZ1$t(TLHl&?bvvV>&gXJEwEt1@%E*lg#{lJ|^&aofuyBc+ zQcp7vycWW&Mqy@VzC2=){M>Dfx%M|`lCzO?#{)I?Bf|T22e6o;m#@N{)TKZG^yMM% zx96K_=3%eoTj{w&6PDVofme6Ezj!t)yHxXCZLrGu#N15$r0XX9FxTfx|K56zF>7;{&egC8)+`E}Ag1U{7T?BKI#@Erdm=L|i3rldfLRz(ns z-aUuQj2(z|lf|G_p^}EKHf{j6(a}VhAQ`>FUp z*ozQsU<{rt!PYBuhUbEtneM-H^i^yi1WGHdu(fpm8ELgecB7)X%+itK=B3Y{2YLiT zBB#0%WuNZZ;g=xiT*rKzgBkn2^oUCZ8^a+%BOP(!M@&qI$oHR3ifQJPi<+mQd1-oR zqu5j(Rd;??QU9g*ZQhxRUs=E2$a@X7F#(J{SrxW*x@?9rHk2yz3+QuYzQu)TBSlP7K?e{<sn#UOX7 zWaFp}I%Cj9XMbm}`5qV7@_t2e=IiTE9BY@BX73YTu@#gQunSRT4HJSEQ=1yeK=arx zyOCc^TE_1YJ(mS0fhrNGmH0m1D<(dj@M>soRH8vgp%O{i2*0_hW?AKlh zJK3vi{-Ye!E+Uj;FglUDZc|nahfvtC=}^6~!Ex=Vs`>A4<|P1t<8|0ymxo&)hI!{V zvD|L3-YMKdmPVRG>W18Wrl8kS`4szE?aQ68qQA4PxV#2bNGSi=G&^cMG_na#cCD#X)zUqKJ~+nL(-Bljt6y;d1) zT5!Ty?Z-id=$$y_{h|@wDwZ2}o)+|D9Zdy(t?#%RZCuQqEy`N;q9T(`2BrygKIc6NPZ2mnL&_-ww``fF#G-eqqVa zu#6h0zFzhE0?X3w#8z5(fEpZpL!*Qr`QKesTMV0kG586Nnc}G3!O*Mmk};W~LI((i zBIDDd3BigV5U5lJ25EV&W{_(O>%{^v0Ma$g{MAG!1`4l0C2 ze;C?a*X~&&$mG&bR+A)1T~M`!-Jtbw3wA@tx_3+wF_#C~i#kyEhSudkuJ$Tn+hSX+ zKUlncd$#}3x*k5Y^Mu+m?PB9<$&yYxX(FFnERk7*&MNvdUgSi#W1ZMI-J`eonA4s=*!OwYegqD7OE#@`D~13}5fr`UnF0{IolM*{EWL>r=#i|R zZ5snbBKx$Ua>MEGp26K0>wa_3XE1EN1=m_sh%sb7EZh{2nUADtDVk!I)MjJtpxgJ^NkNx++pppe%-&|F;5$dsmNSe2z2oxge@ zAJ5K#XuY2&lH4CqyjMiYo}r?Et9#%X_I_%eQR~@w(+gvrA>Ky6Q1oPV6cwP?izaH- zIneBY!%h1Lg@Qy$udMl(uzgMH{pcQlrp*VYtdy;({yVdb@``gI6#nDsIXOT-T;)`> zspMVR@G1@-iC{xG+^z<O zb@)jV^Vpr~-lmqZu%w{)*fXF9UYW}73Xioc>>?t4Q%Tjy? zyE19Zx=a;zIF&a}fn z{+30Vcb>f+Qj=fug+;4%Xg}S@_AC7HD_3P%i<M7HoH`!mO%zOM>?m z-Om1C)>IhHX6ydx;Gf9O^RNA9_hB`(T+H9oOdT)B4^HEjAO0c$Zn-fL#;!K5rtr}~ z`6?X2$4+R<5&1~Ex$GfcezH(lOHnPdlIfR>X_2ZbsJLv$^(r%`7$oF8oES>Elj7(4 zQU6E(;}wO^Evj!GO`rU=j|ZQ2cB0*J+`atJ`nEJFtH3Sqc2UH}bDH_#?===X^5?u3`0_~>ci$R_i9SUs5SKY9%diG{Z6G53r!E-s!|n|g8Bl6=&w z5KfVQl?SF;;F5B5L`rA;^4QatlZm>|o3)EAF;w)e%=KrpuSmH0AZg83M;}4%myNkU4>c z*xa?P11mSTn2=xUvt~S7x6ZV{j$Cz8ZR4TSO=%MFBoje#6WU`#SFs)YWeVoOXXFe7 z>yrVtcdjveKp;!97xdR{G!{b__*!zBXSckf={!}*+Pv9bAhY(e@j(8wvez57K9(MC z?hP7IyT8;!LQFL4xyuGl6|I*>sZMX}zG4AsL`a5Z%>&J@A=I>qZgt1oIEh=F<3oXF zyp}T#Df5vp87MLbZq8RvhZyP?Y7~zwzmaHa0-dCtjpXQp6oeBGlppg!z>> zgxXSG(nuDpm--3dr7D{ITK_C*^23GvDe7fT_qzCR^IO`Z%aJz)Y^gvWoLRqF0BQ3i zC2{pm&{2BTVLvyVvMw>({HcJbqp*oeAZX`3f2uRa_G>SnHRF>hEoh^e^jMHb~j!l$I1x4(p z5V|50jr?+P%#-#KA^3iM<1~j(&ahM`sU^TD9}YV@owdKVln%6l7SiG+!}ah{36Hvz zb{~&*Vd+_J-aS#I5lai=IO7-PZ>-JZ-riz9AnfhFK;IC1ajZpH;mNt{GhVD{wo^Zq zEeJn4iXd9w?bzoIGHH()99n+aBEYn{iiQh$FzO)u!ZG!)e6_@zEe%2y8my>t8&nZ=) zh}#*1q8uZ-ZTE2nW4yu+9rIiBu{Kt)wV@ksn#QswknE-sT_(VhStL_>WUiD0A*6y= z_a4Fnex52Y<_5M*3Mw6pRp)H80Ern$&)m|leEI<;EnRy7(t}%AIhC+@EuempwKi-aFPV#4}z5Mh3iL5g3flHq}yXFAa( zkYrwH*so?Rhi#GzljB(AIgq15XlffgrG}TQwlbKwb$WNf1U}$R<3ciM=0lNM`S^t= z^R_TG{5Zaqk-(<$qp)05i6a^K9EIf#TpPks3ZL0Lbg?PXxBPrnCAwebyZD)Zvj75& zRgo>*t9V?`nMiF`Jw^0gD4leasK9N+WI#MNqQX+J@!Kdj-k&XK;TD%z+gFQR6}pX{ z)!I-nB|?$ub{vSkgLV+M`Bn@DX?9F^C*;NmK$-xOp&PnO^d{kLRy1WrGpre3to(CE zQo7s#&9e%L8Ia>9%x1^bX&2uXBN3G?B<3|RFKej+H(e8 zpZ;N=3n=<>>bSwHSJJw>-3?m!pwDdey(3|j&(a0|!gDJZzOcqp>off2L_~KTqni99 z?X5Pwz5vf#ax!iz5N8EuE58wanw)MSu(GkVEh54Q=b>^ zaj`-W1@#r}AlG1+em+hzT%b%@E_Q5vt22iH#Jzz9VGk>|ncLosIiTK(Jqy;XJ{CGytzU{ETRy$L>xwktL zmpw^2hITnG|I|N+J9}0T6@Y#u7ZY16MHhUS_%QU7>ikzH#rR=D;LAlyZR28x2cB|v zqH`8h2k*z02dwgsN{CriKC; zzZm{<8n*>3T%kFPCrz^EN-tQNx~N3whgrW57PKXYdSG(yz{5@>yxW&cUqpT})7Nh# za?Y9pOwy4?2)(tW_)`q}2(MW5x;z?M0@j~xkcB*jAAQ%g9mkNE? zp|zN4xiifSpBh-GFG@qX8Ym~0hL273DUl2}-nAO$iGNb8tH>IKCu`zPo}t?KaFL?d zOFT4Ajo|G!=c=v64m{bwiiiPHGd}vU~2TGpsB}Bw_GC8y?qsjMmhE3z5R^WBz zJfggI>K>tjpMBC`^pJX9rkjwGg6b|Fme>%CymH$;40_cDpEt>(Q65?cnvRf*6Szk^ z@%_?ixEp)%qegjFDs!yqmedl7BHe_kuV0pVI>2o0Qc~qmUEjb5xzZ36`X0xH&;Y1K z@O7*H!4`%YDg{B@3Adr$^Zkx;Vwx_2)?xB--w7}h1#`s*XW?q>E-APKU*+gP_0x9q zh@MY2fx4GQ01r)V!qcl}p03S&c5!T4XOFB;nY`RWe)iE00WiJ4UOFf~HadrgE6-O_ zLxyRr39Li$wEarj2d{}~TRd~NVfs)KqAR9KzUHNP?^D)(BEaC01{*fjGyHzM9SZ{q z_*!Ef-cUL;lJnaot{M3g6-0drT}p0#4WAo(k>$#bIp5v~xA|R((~)UrxK&R>fTc6! zs_eKl@-0GD6Xg1$QSz#P__eSyINp52ZkTGh$DZ?0f_ljV+q)5~)k>EiPX4f!#&Rt9 zl&G|3{t>`L?NREiE!=J%mX(Kyh&xDe_4{|(V4tUafMoSFA@KX$;z#Y3u{ zmPk`a)jD(fR1I?XkP6Jw{wy$-TA7Qu{uBWCx_82ae>n1568vuzP_lDRGZ2(qrmVE2 z-kTZ@@}1S2(hbhsaLAFX&|&}93o1_&$w$bSytCS$;fMdw1NXect2gNdl+%LkgB1+uD<)xdW? ziu(zcQIg@~jo)AU6FhkcZ_OLtTHB|X7s#(oW8bvZAKR*uJ63;c!3PRjZMr)b-MHY{ z8-&P|{Z5*0k>>A>+Y|<2_35LL8+oo7Qm$2OPgc+4m~a;_CGkP_PE}6*qa)eP9vgxR z19tOFFjvjxZhT1<1B)th;ig|;b>Z}XzS0i5KYk~Xgo!w2TUQUw^{+USPZvU6o!Wgq z{lm+qj*%HcP&>@7se}7(3ZtBzhET&#x~fD|EI=%tLW5>XS-|h?Kx_(?&ZRH%r&nA_ zI(5Zm_HV=BQW@2Ju4@ML6a?=Pws^oa5z%`HgN+MptV5BA9^#{^`a}cUs2EZrL_PLd=OP@O9UVa=erECvdc2<@%NScO7aMzq zfhfJo|KVEK@J1O?Pb~G<$1n?yi9G-24Hb9*f?P0YyF5Yhdnd)zDPA{(&8>&hv zLee`RUx>Q#5;Z5H{m1}4{G%L__u7uKXkZ~7n*9amgZdA%;{EB}QcepV?|gWywc%_A zSyf-ucCuy-lC1{Wx$y=z&9P^P9^0}BdE0j)=(g($5r5&aJe3gDe(5*wBSX+Yk3X)XB3Sa%!*B3~p6XakA)%Z=aHb#Nb z$qs+P{OE_Fxe9#AtR3x`0g*ZlADD>i{AhcEpG&jTj!HczN@RYHFh#oV0>;r#Dx$Tn zv`sy)6z3qh?4LylQxG-P8q>fB_=w)0kmnaz(NjzZSHdWn(6aNs&uN{m--xc#LDskr z-}1|lFVaiYWmoegMXy5Lv11_rDH(M25A*{>uQv|bve^s9T>@2i94la~MY(F7wq%(W zaiTM)p?XNA2?iND!)){$`bE0Het+hDFrFx_H3>$7v|1mtWPX$mAAo~BvMTMcKNr=R zFw|4zN)4kFZ2h-*W?8Xwp?53`Bd{;%>gR+g{3(t6;EU^@g=2#|N%A#v(0##i@9ss> zBn`Gk0McZUDbI1bA>Th831i+0*zM)kuuFLnTOJ!5DwK62pJ!8S;0Fxx)6h@ysE{+y z?vwlT8tquDG1{ueMZDpX6uo^FK8h(5zxzv0;x4)7a1>M0ZOI2JaKJ7RK3qBnwKh7_ z)|2!qoWXuTsPqnAe(jAnYh?y1R}hS;dPC0Hd_)EJ!gZ^KHC+^%Q}zz!Q+e=#iiPCe za<=vj<_Na(VdY`E{4f9_k)4Lwlisvu2XFccjZ#FKZQJl zrRu78M_~CzgIT$2ngl4YKkN$hjeAPgpb9C=hHsue)js&PU9x;w_UI;w4jzc)?leq! z0f$GN=HlQ#DuHITQ}F=C4z?5az^?F7S3BoyHD!go7S9XFv~`_>u-T zEwf2MoZzW;Q%~oH5&k!RrI9A3!!Qt3iY}z=-A)$cG5l0Ug{&(N)N~*PPKz_`bNBhu z_+SipXeOO`!{4z>s7wB#tI_}Q^pycoXkFJsgQSuI5+XfxhvXHM7L@Mp?nX)pLFw+2 z?(Sx2kZz=;yWuG@rTTrll$zm_gZW33kfS(erAZNp@nRVnw#)_2xgV@q}d@ssADnUhb-21xC@CMo$Y9 zt+5erAvNtRaSnBW%G-rExf2)8_eJ*zQ^99W?#erj^c?rNowh)rwjn|ppxOi83orgP z$RUBI>HKzZrqrXE897)kX3vj$t5A89oP)Rn4D=?UfFsRPY9*we2GNDcBdQlo56@RH zoh_HA(AL1Fyuo&qdfmkiTnhZJd9?FSUeMF?CjGi5V?#}y)t)6eBB^TS7Rm&_Gf5I_IyS5H;WuShPvSE_UORP|+P9TQGVVtet zEXm;^1YxCT^So~p#b9EZaEQUuglede=E6bfGn`p3b-8)Fn;*gK^t%%>|Q z6YwieM(r`5eY!;K`LInA!(ROAh&j?L_oU6C@v6SPhC=SBy;yE;8$XgVrnVF^i!))> zVOqHHZo0ICy|HsENz(D?_R0gSA-ol89`FcGHt@*smr-;Y1^>M0t;y+bH!9m`3o>)& zF{?bi#M3-h1CeQ6*-I<~Qd5haz)qm0#zEaRjt!RWeRT#QG2BXHfa>-lLVK*&;Rs2+ zTswXSJ_O=RmR;SoK^c}dzAS)Oo7#BRd>(O@^1DF*(iZlTHahJhCPA7c2L|+Fo<$xs zPG3q=XG?9}pGnqjRPN%{g@^j^5CDumaU)a@_Fr$x-L*0h;Mr60_&C=X- zJTC`m9th$r7>XcXOm5t6%Ferx~Bc}Yi&^p5i0i8(Y7_OA8n`p<}wZFdKNXo~QW z2}F*0ss&pru5Y(0x0DWV==R9o9NgRvxXh3Q12fMmM+XMR5hwSq$t-cR4mb=Nta4xb z)zIoW87pCrPB!JXMG2d*z?O~WN0z|PYiRCc9PZi4GE4-)#a8WQt9oEkRuv-;qa5&i3Bv4^5HS8|1v|C5-F z&a9)XyOHCV_>>j3*WNOHJUm2%J5jYJ&&z0Yoc=EKx%^zGCdjCQKLwu_XGgqYV`Ig=wtp2J9a21p{z79 zm(K~1RsQ|xx{5)UU1Hz?n%ejm=4w80*kmX6ro91Z`M>`Mkmrbr7IudMFY51D{7<*) zf1kFQhaG%Z1&k6wT-}7yr<1oG^?y1|(W1Dp>8jE}O_C08bDgKS%KyG4@`1D5GXo56 z4o4fn`QiR2)d!&qqlq1{PRzZ|GE1D`4!9!MEWLZQCe(5fSTisHmt=5(m(g-H;(x99 zvZIxII!8W0o7L}qa&@JndN3D@?poKtBHiCx-JHg~n=VGyBN8R)&~|5%*Yf$_Z__2= zz>F^r!ZY0*p+izfIrW6L_0q*G(mXrD7F8{Oo}BJEm5a@jF3`ydD)+I89vU-v zobp(WEA1iXl07xKxwRqI16R~EPN`1QuuW=1&;K8sTY&1oh3N#$OFj&+)HV)V%>$nN ze*ed^Z{{F&zwQq%&i_7bT~+jFx)VM^HLu*N_@}lX`m#!i^`uDLCWCAvB~V`B9od3E zj0X>>W~LSg;+GhZgdCcKd)bGA(2_>&^;W>hqI?(&I?{{k!B#yUQq9UWdx(2@D16i* z*TEZqy;k9cL^pk%xvTBr1vstU1IROfC1&Iqu@BSp5QGa+-Yd6?v$S%M5MqdY9*660 zrakEQ+S4lHjxGIu>>`W|p%cL zLwwtdtMa;pA3yjkUY%XMk4-XdWoZ=`c!$;YsW?D#J@KGb`Cuf6r;BC!*J1=51o*xc1JZ^i;N39&(RqoL#-O6V{R9);%F+KS0F?x$R zx5omQOcQGkaPoEFraL@)R5X=sGm`bLb=P}k_D1TT8s~99Yf8;VG76OdW|`dE?z789 zg;`?dH(?ZpI62o=P6kuQ;~Vm7${7)a{7=uK9$we6C)N=yS0!RAUk3D!K;H-uFlU3P{P zb8vtpme$RQ^WMP0AeUjq9M1QN{sz@N4~90yu#MZ(-kI?vZCaFMh%rOZhxPuIPd1O* zzBxpUd#52A&EP5%D5W9*mYg*|{r}^yjoCn1y z`~0QvyXkJPomwQpXufaDv&Gcs@~Wl+I^oYf*t;t5lvsePSPW(1tORy6=_~qnvu<(yFUV{fOJJkf-#TsQZzrdZZ^FR#@`x?f*kBvx`-W& zsFfn;7ZbOSdG*zmUEe5y+r_w&G1%Y29jlFoX0?@ctoPV=E-#nIc*vq!)bjCRYiU82 zJQwoUwq?6vT6@>bO#G^Kt`zR~=~4_Zy1ghZjFG)3>2Mg=0?nFls*k$8;&#aRVLvA6 ztBl^FBbRMASnf^%PDFI>_T@DH};6_pF2Rj6SW02z+gSW>8ZpC<%_1Vx(`19_`3R za#cs53LF7duC8;OL(=G91y`Aw;ztgI%>Ok7CskD+P9zkP4_%X5=I65a{6>&ux&ZMr z-Z7p!kedZO1|9VGIXHr^YV6~txLU7^*mqbEE*Z*|{aGIyF2&dU5!+lni4yCx67T%|%6I6wnq0M;Qbr>CmdG_PN^3_a`Em zrf%Q|6R<@f=QrIJ!tpv&nA@F~AQp&DE8S05;?ZOyUMEErWSItX~qeqiG_d58h6s@)QDZx zQiQB`5FvQ7iG|T$q~*)seAa+{z{~VcXV19Hz-0>yyYIc@UnO0<6*z8jA_WW<@?2$t zT^EJY*2Fh+INow3u$)%%JumA|l}$gRO? zeC5d^*Q(nAVw8!h&cHD1q>-Z#7V@TjzDVv*$6RGaQoS2yuc{My&}YbdNWKYJot){? zPbQ}dUH=Dhjc3IHjzjq_QI(|Q9es-uL*=GS23Dvngruqe%>U8Q_(3@Z9 z)H!|3P-A!vb_vvUkQeL~68^3)tURLOsEbRWMWiOkdRmUT8u&b0WC+|;MeXbrJOzQP zfyZJB&Gc0j$>`;Vgwx2WBYS-3G=UMtIhF_Ri-!-7&^% z=bKrW5HVM75jC0W`#ede2G*KpPVK&t*9tj!bicn!py?O(L>(FxWDD^eart3^qeb93qhFj*(JH9f2gqSbQ|BV9iZEDtJE^0(b z_}iVfFGvhhFXVl7T(J?%f57LEBT3PR+0vJtO=kyv5zguzvK{*9hGtwuSzKTWKezCk z2D1n#E#+XYR(H!#MCoX~_hq<#!Bg^<@^$J@xp6N?T|co`dEp$wX>CQth*H>1<9Jea zc&h5&5-fjHW9C$s@Q)NW|FDMh{xP%BOe;>dIuU-Mzsgennt}Q;XU|u2@)~Y(hp_Z^ zI}C44)Q;+PYPN$D-GV5wk*)z@Jhh)9%|B1AeJ5nthumpcL1)oS1u5^b=)TM>7(GiZ z269LC1vTs~lwOf{DD3WyJD6y`JJNYv7tPUG0%g~*UuVsS&AD%A3`$^=OhPhyZ=0Mx2Ay-j@6BT16Hxc;j^gd zyW#JTJ{Ta&q5D&Dbe_m%^Uv$>ZuqaFvDnn4>EL0L!yUV)%};v%?ncPm%gEud=It4C zm*TfN-lJE4Ve$#Hgc{T^;l$qgFtoGM|UP6ksaygCiQz zBj!==a2bh$C+ke!d+C3rc5}_GZ|0y;TgE8{b$TBoPd>B*?~Z39F%te9K3|EXBVz*f z9luPTPr>`dF=$(dCf_}#%oh=Qd^)RWRMu4g7hj;jHTy1m?2T11-g7cr;51U4d{|*Z z*TrsN#{ukKlp_(VZxl#=)r~@ak6(Q(iL2J}P0WrBx#S*C z0(`H=p>i8AE-JHQpv}wrB$g~m6#zLhvVIZfkysvs$&vp9G=K<4fMW>v;GCTME=+&S zsaAG^*EMO{F2@R5>RB@{6-$wT1CuHGBzbEDe{;djRkqMD4so&&=fz7;4d@&w)G>ht zy}a9yz0raY1>UmV$!2z(761qL-x>jtNUHp=_$*sa>2O;<{=W54mvqq8cbGN<;XfeX z)3)cJOHoVTWM8x0AVByEf=M2=7J50Djf78Pe01%y;!lCjihOBs6$B>Rk!{N4%n_mz zNnP6M7+*|~RgEUAS+gua8T$=a7Y_M^I~;r9XF8(p%^c2}*!_<NP|F{7LpoG z@R~_NZe;8$SBoKdnek6mHpU)h8_>4;3FUHgZY`452UZ9FZt;pw>+tnJcTbnetX(wSy&()QF}>>Lp$CQK zW-}%S1?{E5jQZW8+u@1m`Gtfh%V^I}6-$EzgZ%0CjG^->=QmyKZ1hOEK;8|gzCefuCLW0JTP#_uI&`j@{loX?Es^a%T&`m?(^>AbhOfc@b>@}0qO2~TdFmyYmAc0i^!n+qglnxg`z2Byb%vh6!q%IVW(xv>h#>M|TX%A^r7x z-ibY@dNP*;``UeWYWnEq;Ib3x$pSQavN2Z`o?-Mol_emt0A@2m2)$RrQbv5vvvw4_ z(Je~C7h*|WElJwo9@+iwMjLI?!X~VztBO!x_)TdXhnfyQzVgz5TN=urK-JvtJ9k|b zptd;^NDzC?TXGK{&1}_Bs2MD{gnkG&|GxM+aSBCR^Q0x>B#;JLAfHp)ClspeuPx6$ z{5B=PMK_AmUKcWQznKtJEpPaG7V=L{iVV7kf=E? z{9kSf5}51+Cooo5gW9r9db=?MvBhUXsnTDKUXRlYw*CWD+rV;Hf@pl2q=}JFwvv3& zR?L`ItDor@_aFk9 zFMcR1!aN`miZDxP3`a~?iO_2rK1VvK|B&JB=dEi6;>c4587_o9_oC<8SocEU`Jn-K zx%ZuRu7|z#}!>B;|wsS)H zG+N5yHaceD`#{QgCRWyoX!xVK5d2>!e#cPBo9{=%Bbv&RxFi}(WlbsVJ$8VXFerHk zgbu+=6FLbWnULP;`I7$A*QyzZPq$9KGJqgm3}6Dz@i@qr=9va`zMg>p?p6p6!-ue~ z54$YHc|1I-=+YqGLGPX5n+Kg&Dbb{c*n@c9I0l;qN`Y9+^BguYIWKWCcxapQxJl** z%a*pmJ-+AIzXiw&!i~z5ze$nGWruffHSZ^5T(f|h9q~_PM$TlFdhcEma<$a?uAR~E z$$_y1$~qE_eB>8_o3uF6ZO_~VY{=rY`l65r!I%TC5Gw8`@Pw$33NRI&@EEpfV8^(} z$hE7!Pj1bcMC?EvFE~`1CUd3%6AO~tlR?N~2OXC$^5>49YIZ~zBIhj zN(V$0WlIra7fOy;QLHg@dYu#$Jbhj$ige&0V(ZKHj96) zo!Y^rZp(A&y!~VwkaX?Ac4K{$74TOtdlYpXK16A`j0BEASH)(0laIA!V)7H}q*+n* zHx6MNwf(rMp4U}P)s5wIA%I$58B&|0*2M-rO z8%9rU#0R|m%PD1LlcNnrP>fav5#X4@4pA9I^VY>Pcy$Bp;q=nX;Re=ot9<}o$f|yF zX&~Lef{CDc))&V85PK_)PS+t;_6m-Gg3#bT-7_b71to|_O5iAS*Sfmg@X|iaI0YYk z-Q-Lp?umu?H02H40pjR4pGk6RYT4zyI-SA9LC16JM!BQq4PH!Iv|<7guj#FwGB&rc z3+)4(u_=9Y*tQ-sn6zUsY3XkgdCLkWaM9_?xWKRGDbd>NvC9SSWvB~B9Cs;^zyySy z)m}xMX+IbT_-v6TM<0=b73%>N4}A5rl?u3LjyXojX+P*Wz(Ym2)m3$WdIkmYhyU(+ z(fqsT=VJQdMuD08U|L2&gEm+^vqN*~Pxsw)>$2*qAsK=AO zx31?xjd!;Ior`U3_wMiMujpgswsdc)TY7&D$HiUl03A;o*x%oO-!`$(Lpcm}jhREt z^u9fOY%|cs1cBDOj`lyG2tml0C|)V%>SL3X0H4OeIU2pnb5*7-}J$&;KCx1CGFggEjfxw2@A&wYV$OC;peU zW)mluj^x8P37m^Q*yEeq8P2KwbboKZjUcgptrNPmERs%9H)m;@RC{|UKnEiKA5EPY zg)gcv$}wf3kWOH&o$Bf7t1&F>8DehN^oJ}|sL@G`Cvb!-A7kanF)KuW3bEYXCs11b zx=)5@s!qL{XoEM*3p9GMcKcX*?7eX5<+|~3TXTJPYFB0dh;K?CcSw6`pY#y6-*)IV zcCnsOSk65K@-eePM^Yrx%FQ+9*(*OFuuW#^XJw$~MXvZSLK^AYXHnVTj7*wo2PpHZ z@*n#gVgWt&cu9;)ID8h5)QEtOvQPv5;uqT z(zeeQa-PQ?N}0{P_lu32S_^fIn^O(BO4c1QHrcY%l%~fvHo}+{HUA)3_FOxx5*|)wy%H|H>YNCg~iIY{N5ggAB6)cy& z*2?|0aJ5y@HTuuOB-p1P?&N+*;17E>sd&EPECbG5hhpr!rw48Tme1OoR zq>Dn)_=(X*JPQMMB-KPn_zLF($sBD5wEK85ragOP_1NKWXp9JK!GD9AA>(+v$+wr4 z_IBq~jk^G8nr;g`@5DK}L9U|@ED0p)g$a6L-v*jwfpP* zpEQm5cfLs5Ng_K}2jkn3Yn_aji|CcS--KKCNYcu_$B0OKvbd)9{A3Snu5#ugwt_bI zz2Y7F(wO3sL)JqGeqx<@$QYy|+a`JYdoGFStKPQitT)Q7#zfHvs~mvB4Z^v&eo7Ay zlx^^53|8(jwqF{JVF)^_`ya%^}2LH@6BG8v98<`}@cwH@MFiqabax{0` zY;>A_kiA>U4_wDMp5I@NKzpt7E;h)+iHLt^)V!C9{IW5ks2bsY&w6n#8~nQIa_gKO zNx}IXXxS)-Sm~JoTjyNSzWPk^D)k z9VRz$0n9(Su%!8V##-LW+$JE3ZevjG5>9y<6SbOCTLI)^#Q9##kafH9A93BQOnmbm7TBrbx?qN$6%nt`Jwed(tY~!w!X->Be3VrLYg@KHPbyH`?Nj|zH9exVy%GMLNpU1!?O#!)dU%vAEkz6Q7 z(cxBs;Ap~E^sgg%WvCp{Reo1ZW!;~FFTiT1lodTxLj1{#Mf)Y~T4i6E=VmVNj#t@V zU9X6cy3)Mda+jw7KA&9hK6mx!t3b0RN#L=0J~=m=?Ir>f51g=CF}F-%vhv%X`4J0K=vyE0&Q#YdEKBK1!+YSX zflm>?G~h%WtO`iNzIjiMgR|m(#TculxiHgDjSmy)p#lT+pVEG#acA4X``gte90r_rNiJdYy zOp3%Vc}^8E=EU7zMuGSTb5mVCtia0|__F_&U#eE99W2pq_!41%(&)otnYeH4OA!h| zXHERXQp#1LV^W~yd!C%l_VujSMKdHu!UdIYFezP8I|5uH7xT2@%!20j!g*p)k$o^) zh48BU!z%d3keJ1HLz&E9mD2C|lg95hf1VQ=0<2}vMU+aqN*tE26-{+-N;cQ~8<5q> z@z}y2$gMk``6AU^=lX}--CWCoF-~dW5lM*eK*@IFu;JjL{T5LT z9Do;ff_{7Wu0-u#bs-IqQ`(ZC`S{N&T@vNCJd_ZTA^S)fSN@SChR}|U4j>X>sZGYfo zJ6ks%b1L1s>I#ZIs6R-$f6kDnS`hy2rSz>Fmi*Q^0}`H&ncnPxp$2%df`SUb%H*qq zNF3%x^3O}MnnA+@TxA90UknB_P{ z?5uLr{NM@egcF2%w|Hb}xii)(VN(JH5C^bSfK3-)p1RSWuOsb~j`VTsW>%hB@GAf; zN|^G}ce&JTwt_jP3r7y}ZsL;2Cp6*a^H}xr9XE?#&>FKM-MK9cT5dvAt>L&v5EX;A zX@axv%aW()wR+o97&Bzg)w+9Hd7glDdROw<8rx>fDbC`PE_xY+`UTz4p&XUEu%-Ql zv5wN4z}pG*Pl2^xLc;dAy=v1qDG79~!QKSI_83#AF1J!Q7^_`7?iX`7zaCzKI2mhF zJ&3qU0B~z1t=zX~Wsn^9IhIz3aMSw__>WV}r?|{{hEp#YA`enpM%Z(mNR>nh!pe zSG$U}V_9~&6j}ENBBzC3nlXVVclruqzc1>v1#R7*hN@_;J=zjEU3^|GI`ao4=y0iV zF^c|a2!6)E9yD7u2Nv1ksdg6GXppU`s1FmiS3O6Mp;vIsw6g}?$Vkv|-}p{DA!CFX zXlm9^g;wBl4KDkak41tTTD(%xyJ<81ev0Z0IX@42gZQD56dZI`r(^$Rhdgn}ifA>D zVbN~w?PD`u7`~yJ&QQ28_wY#ro(zk)d8Zp=8@!CKh?#aRy=FVBrTu2Lj$0d$byBQzFq?ZFUTqi$<$Msx&(I(pX z9QkfK1=t_lbT>*C&_gqWoxn@PbUYJnG-S@gG#)VjA^N7&K`PoF=a7|;pM##&9DSM( zp@}{3#*N^57&O!Cw@}Utf)It>+6lQ71cvINVQrjuCZ}SsYVF(>AJ$>4+FPSM1OoXh zaP>lV3*4GuG)r$UmbDxgQz&2(=A=4C6HpcsUWjEjrs%quw#p6%Yv&8n0hozn`kS5} zwt(6DWN; z^+j9=WrJz~8IP*EoR{KV8hD27N#KGN1C%f%Q0FYr= z*Fw!Cdh{pQeDgNq`PBb>p5M+ES{y9DA@CKQ$sd-q1-8oH(^EP4I`MDNeSNMDd#JFy z&6fK|FqPv#*4B5Ch`K7Zdbfd43n-i!o0}Q1SqPJ#UqdVh>mrLvHr+#9^gU??w({x) zpKo@0F-8=SdCH;iRHgoyu>8-Ius#h4#Z0s-v(KHo*v^T;-b>P~==Z{X-j^+2yjC5E zrTB!HP$CL?~EphzSB zg1~2~Igujq3>3)G!89m_ifrm-A^Te0uM8NK%eEywppI;5NC$4gSe2gY-5cT&yZD3sZA^9sx1Syv(nC|yyaR!@3hV+h%Vkix0|UR zeP8Bo6WO(?TsCLr{dd9idQSOdV8-K17`(j$RDIc==Jqb^h586Htt^kVZNWma4^7J7 z!CnbW$ULak5WE?20{Q}oSZw2>sLNC?JfUxMCNnd_1FbZtM?1y=whG~?PU~A!5aj&Y zT2pL-od+p-7j*t=#1*SKYT0dJ&?|@oavy(e64q|^KsQbfQXn#6q*-sKEb9&j@%7Mu zS6zWd#Gj7Kz1z-QiPf}HxZFGIO;;&wX9T&?4ZR5rU#oh;tyOJJno?=94(sSYCZ!St zm3)XGUc`8KX}R5ydt|54W&qh_3R0E>Ldye9X5zLLO&G1pisii1=_1xDFV)G{-e*jl zq^g&Le``5BwI83(QTt)F4iJ(zcHqa}bwUau`DBG!trJ8Vut?YcLxn1|p6hdp#f~1r z@2aP0ipa=tI2JsEQARdtTpTU(H zi<$Zp%Vm=FS<1fFlq>4{uSk7BCLDrsIK!o*QF9S});;#+@#eyqs74W~S)f%M!dmi+ zVTZQQggQQ0MZ4G2SWqsfAr^DWS!H)57zYH!z%@e@41@^38A#jL=7AU5%k%z)OJNm6 z-&dw8Gg>9FVmEpzT-caIVIX7OH=?Ut*=&>3srUsd_(!WaD;}J5t*71iBOB<_UGDA} z+(rBlOX4#Ha`vdHpHPMlk{^(D358G&hwBo*7?B1(0|eknuk|?-u1nix4LGHPdTC+8 z&j4>xgTKt1W&v6nGu2aDwyA&g+rbN<08*#rWdm!Zkg&o5rDj~JGxP~3O523RqRUK0 z*JCX>>_GdbLcU2*i^B3Rcc98}GAhK^4g^~QBdOwin7qId86f+fz$aXJCrd=;Yn3yx z59H|siSPb=HZ}#3>XyfgwUL)XdAHzK95o!C0=e*(+g8NnfAAoXFqnSd&5fn>SXV_t zGk3r`{o55uURe~#dSdt24tUh^=)+48!!&`gj9l8_C71@6v9sAH+HG#n_7R=L;i zY&}Ujv|T=Z`3u;cbN}ef??b4h!|>(?^@xK33`%fFPb~f`U}(cbn}1o6?z_Q7_#JQf2S0pn92FCYc*B%00PVW!Lr zD#u`HazT1t^a|EedIDN$xbBATF9F*WtONm5?Kq1KcpYF>q=B_f_v|SvD!n9#%>jHm z`l|Z0ybE8IyS#Wwct~6FqPtTDhMDE49bheKUsEXX)^`xDrF zRyY8T`xVKkTZZQ1HvzvS1DXbDu6_Fx}0mq>`6}gt{SCO z$<<{0oTH{`1k*1{%0QkfCegK6eT9TdC?qH>b`R04Xy6<>nBG zPPRC~@ra)JvLK!+y~I9M@<=hyXHj(6TTrt#udcx1^=}l&1dfG;fkR-Ng_VE!3-u7H zz&RXiCVQ(tgilyiUxxefpd%$X`MWaT-nNNfOT8h%RdYDZNNUFWinnvM0jlXjPdA~v zzOT{9ZcRp$(BFZB2H1XI<@(rGhBo@JOzp%1>aY8 zp&*^TUfNCr8%ze!=xBQBGS0(+HmunZa{SAyoC~1s0QOX0M%;)M)K32J=}_G*p_#vx z`HJ)xXI{mdQwSB3oA)mv4uNHHPU2P(#4D5VNR7c&d;x&>K;$tvu*Ee=T_8T9@vPwl zLP-$M^Rxh7>Z;rv7xb_40=P{|I3R6h4;$_X3U%|2s$W^GpgqHbFaeL+h$mGj5G;q_ zP6w668820&7Q(mO@ljXt0U2mWP|nVC-*LV-j^Jg#WDZFU*I9H7hyHZw>FcJQcs`kvdr-^yUT0dfYs+>f4A)TyK2B6ef}o>A&zeTxRuA1K%p zE~EJozof1Ts&fV`$LZAXAwFeLEsaz4G#qppX$-z(JlFAfeBfQ~_;-$wc>1cQ7V&C6 zpje^u?BHGIw7n2<@;?4)^QU&4OaUWHU1ny01s65L4v2UEhXu&*;$kp&3HEwKhxq1! zunV+dcq_5uz7TA{Pg_$oj}pU00^f@|p-yX%pBf+(Hf;qNBy=lMqXt6l9g92hyDe{Xao4xO7RYT; z-?dW1qe+kOg_gc6{R22jtrh@P&&nR0GeB!v)hL|%fSCTeJn$Wbfx{NON0hDdEz(Rh z4tg{Y1zP=&vCq2_v|h*^5J7McXFNogF@z_;$cGf^^?iN;R`6E0MqddT(gTn`Lc2a9 zvfk(BHq6!Kc#YgWI?NNhUC7|P%gm`fivv|)v+ab%S=xG8%E;$%k`_+@Eww z)3n%rJn=y;SdKO^Ou<4~Kg&z)hN;SYcL-VE$agS;wQ=C8{K&>>o8wjC~QX4nP3IHn6Hxa5MAI zfEQ#cSIdG!3AjUsU~m`Du$}nE>IFzRAbV>rDmE@tQ6ah2%-AFi3PM@RCw5G*Og&Q+ za1;PXFp#mzYonzZ&yfDQiXE~H-Yv!MrD^%?`;NhiP0@!`y1@T4<)^KCV zdc$wlszzsEC1_}t1c8q{5HqKuEema`U=X^9vM0lWEJyXUziLz< zj{f$YV~UVVISC0T#8MhpnymcVdPLpG&QoGQ9L%;;9$U7`)&X@Ef9X}d32=8h>!erl zxlRJ5Flh)m>dMK1^i9&`Ggf+dXp(^Q;;-`QAlp4enB`M%8IPC7f>zcx>`c#$?7V;7 z!yBG#K~s?iC+ZLV?!RI3SfK(>IjgQbC??oWY_#=?`UnVE)Pq7)y)PF>0qGM1QZ!PB z=Q_udqeULmtETn|Ou#{}8>&on?^c!|C%0%q34q#8Sxg9o->;9dMgzM08Ex)+9l(+Z z{KqUo|5l2mBRJP=X|QOnwJw|tLKZ8rw$~_ll-)U*s6w1}e0t@EB@gfWu@1aDSU-u* z;eaQUYlQE~0-6d2RrR?A;>)ROad}XaNJO}*6wf+npRw@aml4a>Dxlc5i>u0i3x6l) zyt$gq2ARLWHiBJ6$Y!MBjGs{>i$}BGnERwEQXVRzjea_tH>=p7o6?TNvYN ze^Y=e1^Lq+al0Mx;)Z`J%L>0c&se5@KLW6Bnx;AM;H6#wUh2#u&o0cn`#WzDqBo|9 zGRB&?w5@PV@CAECfpFIW7zuN#qm8($j_B>cugB<91&C`vp#F@ESu2N}&(?Yu8-zr| zF~!492u0! zIzWs-6M_*`tkdP!b}!at><(L%T1H5&>hHaM4VkxP8SZU%{bz?FRov{_)y9{-2?SYk zru8+=?mOy|ypt{TcEGd5QVv)t25Zm;y#DC;+h;Y(>fH^2A)epIZ>b=bYuGA`Ni$*2 zyir^1?b95mu6ZmkyY_gWP+IU+#v`|kSb68463yw{vnH>N(nAC(L%LjVlG{2nl!_CYes;1+y@0%~ zCdXNprpyeI-O1N{hajsj$m|XGU^nLU)A8Sy5DT_~1Vea>6A7S$(Zc`)l{z@tjeuZ< zN?5IflJ3e~bm|UpB?>)5o3t~?>vhtY&=x8}z|CKxa&bvgx7r4q*VCfft9z(M1bCG3 ztXyM}k{|LuGeI_-^22j50OZ)rU|%&7i7t7J(rw?&;AJ$#M!Qz7r;{!gj-Xx9{HRc+ z9!TvSfx5DRAcoNPDQ8GzRl3ihH9ftP97!;!zC3}Wg7e{EXdnxq3Z-Zi64SV2;lPuu zZ1e$SaLby=+m1kReJ?V%fb z8#&=QCOh7YbxCQ)G(=Of{6q&r7=}Q-a!w`WDa}-1A^tUQ*RoGcyO%l{M=sQf0e0%a zan>5y+HK);)VJ}}4eC#c%doSzLEg#y`P1l!NUX_8lWl9dypbTL$u-fU$P1QvuuqzW z=4`Dl5@ud5*y{hOe8hL0>v8EY24Ey#@1tMuqs3fM1^QQ&{c*8KAxCxqv`8#RNHdOp z2Dne3J@q7k-YDzUW^T0aj18CxKKIXJ^Kwk%sCiC2j7Vy}nZS+>Fph^(Q9Zb?lFeZs zBCv)5lDx-L?h|OS%$feTSvSEud*&^sd*yP_(acN~rJ4RtZ>nsnUiNa$%zxveP}@{+9AcS1(f5){okz0-0uYjYKJfN{DeOdJGtkTTE^Y} zFc-gYf#U}T^kT^Lp7n+%%5lVLdGDJ9&Gv7rIh{tw1V2hpof{O!3zXhxBz)Q+Nu0Y2 zn~Z0>`H>Ut1VgtDKbGYCq4x)#?@6NP!4gTWYA>WI83b0;m0iq|v_6ie6#}rUUASKc zP!n@`@?lyaK4Iu-Kw$-9+<%XGQ>*e`b#UkKVb6-~Z_s%cMEd&=%`eclZO*LogXVIv zc@QvnYRga&3KjQ;A*`_gXMF1hUL2HmE_t$aK*_4#_AfR>>LOGH1^Hvg`;6VQw*lUy z*@xJMKgDSI;GxoApdx5hY-f`Bj?z}B$KJaRGOZL0Uu$;)Hb*3+`2Mm%jd<~I0f zP*=VnlxLcQXWBj|aGYmn(ocyMGTHHk8H2_9fV3Kkx>}AmW})>h2w@Z@Ba)vyyy=4v zd&k(x%DfOt9+x=c$+DOBQLHS<6=yxcnJFDa-eeNMRq_NV(r0PDVhwrG<9e^$W{~ek z0ZUXAm z)_G+}F0w$D`t!}O*rR2tmt+Gsxg?np1?Y$<35<0i^zgav%jBm{TnRn&a3M-jb#+Ys z?@ZBUqQRRhi{bw4K3j$Ij7**b&&k3I`o}N_Of;v#^B~;idExJGr|c|u6qY^UVnwxb z>{8pTzmK1Mh!*x=MnehuPTdMYRDg+Ol52 zVelnQs&9$gF;}?5mBP{ed(hTT%!Ac#54+DK89Ph2W`D-KxEf>)1pNdr_75S{&-F7L zx8_s^ootr0Kwu+Z9qx#^N@IpW1pMqPdXJvwQ{2OCWgU=5b z#%?2beyh0sG16#jNfK^9yW2n1q%z5syU?z-5unF0`Lp(wpG9;3j-md-M=!|VSYm@z zKT-dR#-VEWxCsJofxt1~x-@`iH!Kq}ZTJI^0o1e5LOk>dgICAx&P6TF-rCf0c{Md4 z5{VT539sQWWg~?@M8WL$tMMuRG%rpe)2#)A&MM=vu@`_zj-E66zt&VzTz{Z-&n`Qm zeBWTr&P>qCG~x{@>yj4R#fNT0m}*&{2neLH!b+X&qELa@6yyNZCJ`UxS9#pt|fpY`HjwVw?{Fz5;gXLV`{Y+Sv|G5 zmA>g@jQ3o>J}+or%IWp18^4sRs0r=h8L!_RutLVwn2L9o_^UBDi!8ybt_QS!Lrgah zkI5|`zYm$F2GdpC^ZPFzD^zmwJT_{1KU@`G<+W!166{6}(;07ex;aFBV^lfSziV^c zRH}Os99)++-klGh`by$-uZ()h?K7=Y<*j7X`oP7jZ_Hl1yQL+GPVX-fmt!1lMiqvT zTXO3T$=2(A$YqN?&7+=DpQVPj=pbo~%RM}Pd#s$s`+YH$TKRCX=W|-|S&~2$;4pW$Gq)d_ zv1pG+z~O3C^&zLyZFp#H-J~eF)OihMuvI9PKR`=7O*}lJ!MYnZsU~#Fnh5%imhX#$ zK2PyOnOEQ=MAtw9P4v|RS)-fFj?~v2cMlf4RzXQw$w;I=2CHl4b?5In>+gM^_c`x5=Q+2V5A0_x?M;L!e(|m~C9-_OXGMgs zs(iMQhklA08fRnk0wQQ9Dw*$!woK^v&VBLe(t(WTs48Q!1<8f3$W5_AZ4kY+ZMc7Z zBAA|vxOPoQq}Lh8w6TMZ!hovZ(EokPSeuq$jKxVC45e{u18Dp7j|806(6lmbI-2or z0~>LiD<@9?uDb$w-4xilOGnQDsSQ){;p6L)`X^?ER%y`OBc5#aDG4pu9~dL7qv(RE zyE?`2dNMwUB?LGH*c{kX@!V;(1J{Z7qIAf0-1Ss%z`T@+9T~XgGkUq0MmW}S^z!~X zt3ls^9ffKs1V$R@Qx5*_O?-$c3+zY(N+K*4P#iC)=3`?xc|& zmw%Sd=!&ld&aL>nO^d~RJNT7}y&822^qLlchDN&DWE3`P!@@K(>HV9o8QVDKXDjM3 z&>Ku*0QL;fPrfwh#BgvubaEj;uW=^0`K;Yd<|j|OGO?`Y)pZVm)cCvE;0 zm>NA88*M|pSM*WKUb*dJgrgvi)=S-l_4 zx!7aOMIB;e$JZKsI`kBHlZ$3MnH1)VZsD!J8(JgM>x(l_Z95aNmc!B>+Ef9k6+E!R zOl|w+w1b4?*gJ=(2_|^wAueyWVNNFw*k;fhh>KMD;+D#7_O`M406+^5XT(IL}zeDH-Onv z3~uk`$x=Tx<@jYn)1I4E!O1^|V@m@ArW}lDmQaYAVM$V<3L221xRZIYDXKZt(=hWl3d|yKrg+2JjVCM(gs5s2?J>FN2h|ekmN@Sedhd zTQg`?oh>nj1EWe)zB4!OC^t$DKgIL>_`yh0*_TVX=PaTi^&rB#thUXwRbyC?&rtqh zglxBRh58OUT{{%%AP_$qb3|x06&~E=jT4GU4! z_=iqL-*oXt>fZhC@G(C&8BS-pH_(?(1Z4dsnKGXw5lRGO&8OLiB#@B`1nYh~Poe0B zE6mr~W}%(fu{rByI#aM+5>?rQK+Ix$?|>%=)<0rq3c2 zAy-wVx>bL~HA%DYO}b<&o`)$PtR;9h$!wr*_Qj{IbZYPeGqeKshq7K1*A4g6#w|^U z2gK#P4YjIl+4!rmS-V_{7IP0HO3g)=rwk_4U^G`X(7+H|w?ea8l&cpMF(Pur@1+c- zJ9Pqfh0AS!<05i|xFKGEaxRrWK6BHuxTTo$%juZZwv)lbZ^A?w^N;vOC(%jm5N&Mw zkn_#eH51`(ZGB8(Dz{_i$FyypK!Cv5e>+uRW4bWFNBRG7DGBO-0wLyWIsu)-g z6GarnOOm?PrqLXmMW`du2cN1uqZ*fEBs%eF+;xo?b{@8`0K4*X`TEo3W8@*#AsRt& z+z36sWd#e4x*6yr`|{BdS!3=mN39DSuLUP3C%m2s8 zLh%jPDjQI|epGicc_JFk5KLX7EI^H5!}HA^4N*sptp-fyhc6`S#5nSGycja$-*B9I|V)WzUNkSe)s|muh>a(SXF;H zU81Jllrj^Iwc3u6kxFN~3RXgKEQ*6{2d-YiGM_Y|fbS?Xp7r~i{n3@-9(8ihftAy^ zxMq85h#j*#0y!3>?t6KgK`SpdhqvV(zEIN(7SuT`Euuonn7-E#WA&J_g6JWb?K4W}x@!?;gkluconXGa2^) zP#nFaj)VK-7@xhep~^B6`tt7Jfv9i4LEBg;3DXS*(mn5FlH>bwo;@hE+WJGlotZZF z^67^V3RnyMnsde{3wNSO0V0}|2K0=~DVzlrY(}5RTqJ#NvsDH0urLnbv(Pyy`T(lP zgIg%A*{vhBiLPc~HfOO_@Zzs~htJ&WruLr8pilnAI~6pZw5rvgq4a7!tHp0K2mc=I zPI0g*QJkUVm2lTS=xBOVa&IOHH4U#2_!FTN<9}CK%Uz7F+%QE>vbOf@8 z@WAuH1%0~#O$F%D8<8jDf3T^dZi~ttUTnEnNNyL?jI3c7#R#sdYsyni8$1k%iadL* zsZpVjlY$=cI)fnEJ^tG31$u6^I}a4}x}8*HF3%Aj>82+@jcrKX?=v9q@Ir}8aWOO) zn;S^sg%kq7)_VHbG_q(d>7)fB3TaJydSbTh2F(JTqei>9q$j`h%ZI=l1YWH=)ZlRD zHsS2M{2;pp1AuEx5stm(9V-KNZ!~yB(z3lHWrn}Tb2|CvLEsop6JhUJQ+)EH7gktb z?AJA;5e=HiD7Zzl%f*_VKW|0dHCMk_;c5n>+f22nCu@oyr$R4#>GE2STo->h@+m+h z8|acmTu=Tou|a0U1{~rfD0D*ISp97TK*vm|Dv7Py-_{Ax<&7eTdWPCA^s1Q%ivumy z%YG_v=5+|d22NRz8lq`!JLVZvUO`fr_!W>2hDU(S%CXbomFAlDWq~z)(J@U%A|zz* zOW#uam5^Ob9dCt`bNcnQdovpT-zQ>APl@SWZ!FuY>q>-bgZ2adSA1{!xD5T8F0Z#~ z_6MM+StX&^D449NPpurNaxBXg=uhIVN5$Ee+~I!cob{+R`cT-Zoo%)F67`PdCn*j*{biV3U`|s*=AC> zAO2N_ua&&bU(leiRY$XFqiTU=ck6OnSP7Vs_xDwm7sRgE9r~!4?oX*Bfq+R~CEF!y z!kSMI_w+d*H%i`Sa@TWzwjqN+nCF}|ht>(w{P9AB$Wf*Ia_dTG39R$aedTqc5{%RlW65HCqUA#`lKL1$AN z3@8BrEma)rNMlxhXV1w+5m16lTQ$jvj?LOvm&tk7=mp#OwLj7ZFv{$^Q{&M^;R}kR z#Y{w@h7q@!Zq$!@7R6cwg3e{EC`7un!-f5rud7({=W8bgK)`&z=p2Lj_%-%dwu=e5 zr11+rhDMtZ9P|k{*etw|D8u`iDC%vnCX!ibd9!*;n>k)LrtFg1#MRvs!OF^GQz zc#|%0V2Inmq-cgz|A!LWx(?g9)9Gd1e^;kzo<22`Zy{am`295xXLKvKm?SLw@4E~o zwan4AaQeP?b4ZEa`z!&tB`dYC`*(}AzzwOTx1_J1dF1G!jmyl%Th{Z;0+JA_vY_FHvgrbyo93&cD(uH$y@`iOdZmz(UYa zYNl3P>H)O$z~24RJhN=UCazNAGpXS9H3p~_(0T|}BD!Cy4^@O?;pu^-TQOLmYr}jT z+LaBq5w}F{jKJ}JT`5HfRYPrZf{3ex6o)?D$xmFT?!w_5HaX#{e}I2(nl*?r)zvxQ z;#!D%@53y{##J}924{gYbTX#QqUnh?oREpEpR_07-3AWH3+GHOs=iEO|CA^2wJ{}UUlNnh5ulcI~jtG(TCi#L3ju7VV5 zUx{X_T93gLx^8W9Sr$KaA%AdiRP-8gjNBxB)~PtL)aL45Yx>Sj;{`BW*5{($j<_av z5?R%x7}8@Meuhmf|5W%f2|@2irC&`C08=l&=LMk)ct4VwL!xqnm4{mVwhHmL6go|j zfR&hl@HJcQVb8cLr1Q8&YCY6)`w{(oQ8stO`}QhFB2*bigd#5Hl>XVSgoz*JjaYZ~ zimF|X$(}3YL#~NA zvDi^}SD{M-uUDPybU&@BV%l!f!705+)uBBptA&sjU|f>qh_VMz#3sDG4#8WpK*Yv$ zUBedj?&lk6d8Ot;-34Wk39T^{5845jCdPE>cxkh(G1 zqsAU2Oq?S1OI=eU^Z=;;hqAmc%<=2bHnl)uTCbKr2tY8=vAi+P?_MiiL2Ddm2>G*Q z$U~K%+IeO9Opx=rFR}sAnssW{VjBvpCKpPTG0R)h>!r_@&X2k1fcPK2YXidDJ~($* z%t9>Bf8dv-pE#f9ZaaTo@kxU{pXbFoAp_)x15&j8gKsrT*6B+75(MD-&CARk zR-L*aS|~3rX!P>;x=jGW=g7JY2Mc z{U|k-(vn&3vs(9uHiYT`4!(h37qE~M{l)n2k^D9&=5d%8uAe=_GRY!+WW#6wEh8R#N{ZCUWo zimL>@^S}0+2Hbn9jEM((=0BRZj3Ey|>cYHlP>-P~{ z$}hST^|*sh_7%B6diL&46>3D0z^3MK##nj!6|8UEK fhN-IHLAPJT^OPm!wW>+b1;_=o)wvo|_k{lfj4?cG literal 0 HcmV?d00001 diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 65b26515..9b609604 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,4 +1,5 @@ import { Outlet, NavLink, Link } from "react-router-dom"; +import salesLogo from "../../img/logo.png"; import github from "../../assets/github.svg"; @@ -10,7 +11,7 @@ const Layout = () => {
- +

-

Chat On Your Data

+

Clew

From 3be7f1baaca90d31f72a3f63009baf2fe6568c5b Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Wed, 20 Mar 2024 16:52:54 -0400 Subject: [PATCH 002/820] release 1.0.0 --- frontend/package-lock.json | 8186 ++++++++++++++++++------------------ frontend/package.json | 70 +- 2 files changed, 4128 insertions(+), 4128 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 528e1489..d3df2529 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,4097 +1,4097 @@ { - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@cyntler/react-doc-viewer": "^1.14.1", - "@fluentui/react": "^8.105.3", - "@fluentui/react-icons": "^2.0.195", - "@pdftron/webviewer": "^10.7.2", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", - "@react-spring/web": "^9.7.1", - "dompurify": "^3.0.1", - "mammoth": "^1.7.0", - "microsoft-cognitiveservices-speech-sdk": "^1.27.0", - "react": "^18.2.0", - "react-doc-viewer": "^0.1.5", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" - }, - "devDependencies": { - "@types/dompurify": "^2.4.0", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", - "@vitejs/plugin-react": "^3.1.0", - "prettier": "^2.8.3", - "typescript": "^4.9.3", - "vite": "^4.1.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", - "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", - "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", - "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", - "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", - "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cyntler/react-doc-viewer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", - "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", - "dependencies": { - "@types/mustache": "^4.2.3", - "@types/papaparse": "^5.3.9", - "mustache": "^4.2.0", - "papaparse": "^5.4.1", - "react-pdf": "7.5.0", - "styled-components": "^6.0.8" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "react": ">=16.13.1", - "react-dom": ">=16.13.1" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fluentui/date-time-utilities": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", - "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/dom-utilities": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", - "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.32", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", - "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/foundation-legacy": { - "version": "8.2.52", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", - "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/keyboard-key": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", - "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/merge-styles": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", - "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/react": { - "version": "8.115.6", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", - "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", - "dependencies": { - "@fluentui/date-time-utilities": "^8.5.16", - "@fluentui/font-icons-mdl2": "^8.5.32", - "@fluentui/foundation-legacy": "^8.2.52", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/react-focus": "^8.8.40", - "@fluentui/react-hooks": "^8.6.36", - "@fluentui/react-portal-compat-context": "^9.0.11", - "@fluentui/react-window-provider": "^2.2.18", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/theme": "^2.6.41", - "@fluentui/utilities": "^8.13.24", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-focus": { - "version": "8.8.40", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", - "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", - "dependencies": { - "@fluentui/keyboard-key": "^0.4.14", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-hooks": { - "version": "8.6.36", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", - "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", - "dependencies": { - "@fluentui/react-window-provider": "^2.2.18", - "@fluentui/set-version": "^8.2.14", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-icons": { - "version": "2.0.232", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.232.tgz", - "integrity": "sha512-v2KKdRx68Pkz8FPQsOxvD8X7u7cCZ9/dodP/KdycaGY2FKEjAdiSzPboHfTLqkKhvrLr8Zgfs3gSDWDOf7au3A==", - "dependencies": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-portal-compat-context": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", - "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", - "dependencies": { - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-window-provider": { - "version": "2.2.18", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", - "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/set-version": { - "version": "8.2.14", - "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", - "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/style-utilities": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", - "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/theme": "^2.6.41", - "@fluentui/utilities": "^8.13.24", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/theme": { - "version": "2.6.41", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", - "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/utilities": { - "version": "8.13.24", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", - "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", - "dependencies": { - "@fluentui/dom-utilities": "^2.2.14", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/core": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.2.tgz", - "integrity": "sha512-RlsIXoSS3gaYykUgxFpwKAs/DV9cRUKp3CW1kt3iPAtsDTWn/o+8bT1jvBws/tMM2GBu/Uc0EkaIzUPqD7uA+Q==", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.0.3", - "csstype": "^3.1.3", - "rtl-css-js": "^1.16.1", - "stylis": "^4.2.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@griffel/react": { - "version": "1.5.20", - "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.20.tgz", - "integrity": "sha512-1P2yaPctENFSCwyPIYXBmgpNH68c0lc/jwSzPij1QATHDK1AASKuSeq6hW108I67RKjhRyHCcALshdZ3GcQXSg==", - "dependencies": { - "@griffel/core": "^1.15.2", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/style-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.3.tgz", - "integrity": "sha512-AzbbYV/EobNIBtfMtyu2edFin895gjVxtu1nsRhTETUAIb0/LCZoue3Jd/kFLuPwe95rv5WRUBiQpVwJsrrFcw==", - "dependencies": { - "csstype": "^3.1.3" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/@microsoft/load-themed-styles": { - "version": "1.10.295", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", - "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" - }, - "node_modules/@pdftron/webviewer": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", - "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" - }, - "node_modules/@react-pdf-viewer/attachment": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", - "integrity": "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/bookmark": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/bookmark/-/bookmark-3.12.0.tgz", - "integrity": "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/core": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/core/-/core-3.12.0.tgz", - "integrity": "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg==", - "peerDependencies": { - "pdfjs-dist": "^2.16.105 || ^3.0.279", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/default-layout": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/default-layout/-/default-layout-3.12.0.tgz", - "integrity": "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA==", - "dependencies": { - "@react-pdf-viewer/attachment": "3.12.0", - "@react-pdf-viewer/bookmark": "3.12.0", - "@react-pdf-viewer/core": "3.12.0", - "@react-pdf-viewer/thumbnail": "3.12.0", - "@react-pdf-viewer/toolbar": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/full-screen": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/full-screen/-/full-screen-3.12.0.tgz", - "integrity": "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/get-file": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/get-file/-/get-file-3.12.0.tgz", - "integrity": "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/open": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/open/-/open-3.12.0.tgz", - "integrity": "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/page-navigation": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/page-navigation/-/page-navigation-3.12.0.tgz", - "integrity": "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/print": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/print/-/print-3.12.0.tgz", - "integrity": "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/properties": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/properties/-/properties-3.12.0.tgz", - "integrity": "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/rotate": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/rotate/-/rotate-3.12.0.tgz", - "integrity": "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/scroll-mode": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/scroll-mode/-/scroll-mode-3.12.0.tgz", - "integrity": "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/search": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/search/-/search-3.12.0.tgz", - "integrity": "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/selection-mode": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/selection-mode/-/selection-mode-3.12.0.tgz", - "integrity": "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/theme": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/theme/-/theme-3.12.0.tgz", - "integrity": "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/thumbnail": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/thumbnail/-/thumbnail-3.12.0.tgz", - "integrity": "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/toolbar": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/toolbar/-/toolbar-3.12.0.tgz", - "integrity": "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0", - "@react-pdf-viewer/full-screen": "3.12.0", - "@react-pdf-viewer/get-file": "3.12.0", - "@react-pdf-viewer/open": "3.12.0", - "@react-pdf-viewer/page-navigation": "3.12.0", - "@react-pdf-viewer/print": "3.12.0", - "@react-pdf-viewer/properties": "3.12.0", - "@react-pdf-viewer/rotate": "3.12.0", - "@react-pdf-viewer/scroll-mode": "3.12.0", - "@react-pdf-viewer/search": "3.12.0", - "@react-pdf-viewer/selection-mode": "3.12.0", - "@react-pdf-viewer/theme": "3.12.0", - "@react-pdf-viewer/zoom": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/zoom": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/zoom/-/zoom-3.12.0.tgz", - "integrity": "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-spring/animated": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", - "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", - "dependencies": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", - "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/shared": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", - "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", - "dependencies": { - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", - "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" - }, - "node_modules/@react-spring/web": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", - "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", - "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" - }, - "node_modules/@types/dompurify": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", - "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", - "dev": true, - "dependencies": { - "@types/trusted-types": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", - "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "peer": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/mustache": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", - "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==" - }, - "node_modules/@types/node": { - "version": "20.11.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", - "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/papaparse": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", - "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" - }, - "node_modules/@types/react": { - "version": "18.2.64", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", - "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", - "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, - "node_modules/@types/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true - }, - "node_modules/@types/webrtc": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", - "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" - }, - "node_modules/@vitejs/plugin-react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", - "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.20.12", - "@babel/plugin-transform-react-jsx-self": "^7.18.6", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", - "magic-string": "^0.27.0", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.1.0-beta.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "peer": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bent": { - "version": "7.3.12", - "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", - "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", - "dependencies": { - "bytesish": "^0.4.1", - "caseless": "~0.12.0", - "is-stream": "^2.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "peer": true - }, - "node_modules/bytesish": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", - "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" - }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "peer": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" - }, - "node_modules/dompurify": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", - "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "dependencies": { - "underscore": "^1.13.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "optional": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", - "peer": true - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "optional": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "peer": true - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "peer": true - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lop": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", - "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-cancellable-promise": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", - "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", - "funding": { - "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-event-props": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", - "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", - "funding": { - "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" - } - }, - "node_modules/mammoth": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", - "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.1", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/merge-class-names": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", - "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" - } - }, - "node_modules/merge-refs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", - "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "peer": true - }, - "node_modules/microsoft-cognitiveservices-speech-sdk": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", - "integrity": "sha512-wPxuEXgjLdqMMIrdBtl8jquGahLV19LQE0ie8MI/PcBcNLG5buVzwS2rQEyHMsRGx+C/4OdBo1ROdNIUzCm4Lg==", - "dependencies": { - "@types/webrtc": "^0.0.37", - "agent-base": "^6.0.1", - "bent": "^7.3.12", - "https-proxy-agent": "^4.0.0", - "uuid": "^9.0.0", - "ws": "^7.5.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "peer": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "optional": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/papaparse": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", - "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", - "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", - "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", - "dependencies": { - "pdfjs-dist": "2.4.456", - "react-pdf": "5.0.0", - "styled-components": "^5.1.1", - "wl-msg-reader": "^0.2.0" - } - }, - "node_modules/react-doc-viewer/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { - "version": "2.4.456", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", - "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" - }, - "node_modules/react-doc-viewer/node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-pdf": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", - "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "make-cancellable-promise": "^1.0.0", - "make-event-props": "^1.1.0", - "merge-class-names": "^1.1.1", - "pdfjs-dist": "2.4.456", - "prop-types": "^15.6.2", - "worker-loader": "^3.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "react": "^16.3.0", - "react-dom": "^16.3.0" - } - }, - "node_modules/react-doc-viewer/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/react-doc-viewer/node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true - }, - "node_modules/react-pdf": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", - "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", - "dependencies": { - "clsx": "^2.0.0", - "make-cancellable-promise": "^1.3.1", - "make-event-props": "^1.6.0", - "merge-refs": "^1.2.1", - "pdfjs-dist": "3.11.174", - "prop-types": "^15.6.2", - "tiny-invariant": "^1.0.0", - "tiny-warning": "^1.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", - "dependencies": { - "@remix-run/router": "1.15.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", - "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rtl-css-js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", - "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", - "dependencies": { - "@babel/runtime": "^7.1.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "optional": true - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/styled-components": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", - "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", - "dependencies": { - "@emotion/is-prop-valid": "1.2.1", - "@emotion/unitless": "0.8.0", - "@types/stylis": "4.2.0", - "css-to-react-native": "3.2.0", - "csstype": "3.1.2", - "postcss": "8.4.31", - "shallowequal": "1.1.0", - "stylis": "4.3.1", - "tslib": "2.5.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/styled-components/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/terser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", - "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "optional": true - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/universal-cookie": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", - "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", - "dependencies": { - "@types/cookie": "^0.3.3", - "cookie": "^0.4.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true - }, - "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wl-msg-reader": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", - "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" - }, - "node_modules/worker-loader": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", - "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "optional": true - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "dependencies": { + "@cyntler/react-doc-viewer": "^1.14.1", + "@fluentui/react": "^8.105.3", + "@fluentui/react-icons": "^2.0.195", + "@pdftron/webviewer": "^10.7.2", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", + "@react-spring/web": "^9.7.1", + "dompurify": "^3.0.1", + "mammoth": "^1.7.0", + "microsoft-cognitiveservices-speech-sdk": "^1.27.0", + "react": "^18.2.0", + "react-doc-viewer": "^0.1.5", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "universal-cookie": "^4.0.4" + }, + "devDependencies": { + "@types/dompurify": "^2.4.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "prettier": "^2.8.3", + "typescript": "^4.9.3", + "vite": "^4.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cyntler/react-doc-viewer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", + "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", + "dependencies": { + "@types/mustache": "^4.2.3", + "@types/papaparse": "^5.3.9", + "mustache": "^4.2.0", + "papaparse": "^5.4.1", + "react-pdf": "7.5.0", + "styled-components": "^6.0.8" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fluentui/date-time-utilities": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", + "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/dom-utilities": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", + "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/font-icons-mdl2": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", + "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/foundation-legacy": { + "version": "8.2.52", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", + "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/keyboard-key": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", + "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/merge-styles": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", + "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/react": { + "version": "8.115.6", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", + "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", + "dependencies": { + "@fluentui/date-time-utilities": "^8.5.16", + "@fluentui/font-icons-mdl2": "^8.5.32", + "@fluentui/foundation-legacy": "^8.2.52", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/react-focus": "^8.8.40", + "@fluentui/react-hooks": "^8.6.36", + "@fluentui/react-portal-compat-context": "^9.0.11", + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-focus": { + "version": "8.8.40", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", + "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", + "dependencies": { + "@fluentui/keyboard-key": "^0.4.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-hooks": { + "version": "8.6.36", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", + "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", + "dependencies": { + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.232", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.232.tgz", + "integrity": "sha512-v2KKdRx68Pkz8FPQsOxvD8X7u7cCZ9/dodP/KdycaGY2FKEjAdiSzPboHfTLqkKhvrLr8Zgfs3gSDWDOf7au3A==", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal-compat-context": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", + "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", + "dependencies": { + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-window-provider": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", + "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/set-version": { + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", + "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/style-utilities": { + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", + "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/theme": { + "version": "2.6.41", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", + "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/utilities": { + "version": "8.13.24", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", + "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", + "dependencies": { + "@fluentui/dom-utilities": "^2.2.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.2.tgz", + "integrity": "sha512-RlsIXoSS3gaYykUgxFpwKAs/DV9cRUKp3CW1kt3iPAtsDTWn/o+8bT1jvBws/tMM2GBu/Uc0EkaIzUPqD7uA+Q==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.0.3", + "csstype": "^3.1.3", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.20", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.20.tgz", + "integrity": "sha512-1P2yaPctENFSCwyPIYXBmgpNH68c0lc/jwSzPij1QATHDK1AASKuSeq6hW108I67RKjhRyHCcALshdZ3GcQXSg==", + "dependencies": { + "@griffel/core": "^1.15.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.3.tgz", + "integrity": "sha512-AzbbYV/EobNIBtfMtyu2edFin895gjVxtu1nsRhTETUAIb0/LCZoue3Jd/kFLuPwe95rv5WRUBiQpVwJsrrFcw==", + "dependencies": { + "csstype": "^3.1.3" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/@microsoft/load-themed-styles": { + "version": "1.10.295", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", + "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" + }, + "node_modules/@pdftron/webviewer": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", + "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" + }, + "node_modules/@react-pdf-viewer/attachment": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", + "integrity": "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/bookmark": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/bookmark/-/bookmark-3.12.0.tgz", + "integrity": "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/core": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/core/-/core-3.12.0.tgz", + "integrity": "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg==", + "peerDependencies": { + "pdfjs-dist": "^2.16.105 || ^3.0.279", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/default-layout": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/default-layout/-/default-layout-3.12.0.tgz", + "integrity": "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA==", + "dependencies": { + "@react-pdf-viewer/attachment": "3.12.0", + "@react-pdf-viewer/bookmark": "3.12.0", + "@react-pdf-viewer/core": "3.12.0", + "@react-pdf-viewer/thumbnail": "3.12.0", + "@react-pdf-viewer/toolbar": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/full-screen": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/full-screen/-/full-screen-3.12.0.tgz", + "integrity": "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/get-file": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/get-file/-/get-file-3.12.0.tgz", + "integrity": "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/open": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/open/-/open-3.12.0.tgz", + "integrity": "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/page-navigation": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/page-navigation/-/page-navigation-3.12.0.tgz", + "integrity": "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/print": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/print/-/print-3.12.0.tgz", + "integrity": "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/properties": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/properties/-/properties-3.12.0.tgz", + "integrity": "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/rotate": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/rotate/-/rotate-3.12.0.tgz", + "integrity": "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/scroll-mode": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/scroll-mode/-/scroll-mode-3.12.0.tgz", + "integrity": "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/search": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/search/-/search-3.12.0.tgz", + "integrity": "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/selection-mode": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/selection-mode/-/selection-mode-3.12.0.tgz", + "integrity": "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/theme": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/theme/-/theme-3.12.0.tgz", + "integrity": "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/thumbnail": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/thumbnail/-/thumbnail-3.12.0.tgz", + "integrity": "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/toolbar": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/toolbar/-/toolbar-3.12.0.tgz", + "integrity": "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0", + "@react-pdf-viewer/full-screen": "3.12.0", + "@react-pdf-viewer/get-file": "3.12.0", + "@react-pdf-viewer/open": "3.12.0", + "@react-pdf-viewer/page-navigation": "3.12.0", + "@react-pdf-viewer/print": "3.12.0", + "@react-pdf-viewer/properties": "3.12.0", + "@react-pdf-viewer/rotate": "3.12.0", + "@react-pdf-viewer/scroll-mode": "3.12.0", + "@react-pdf-viewer/search": "3.12.0", + "@react-pdf-viewer/selection-mode": "3.12.0", + "@react-pdf-viewer/theme": "3.12.0", + "@react-pdf-viewer/zoom": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/zoom": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/zoom/-/zoom-3.12.0.tgz", + "integrity": "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, + "node_modules/@types/dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", + "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==" + }, + "node_modules/@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "node_modules/@types/react": { + "version": "18.2.64", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", + "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", + "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, + "node_modules/@types/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, + "node_modules/@types/webrtc": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", + "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "peer": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "dependencies": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "peer": true + }, + "node_modules/bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" + }, + "node_modules/dompurify": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", + "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "dependencies": { + "underscore": "^1.13.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.701", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", + "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "peer": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lop": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", + "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-cancellable-promise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-event-props": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, + "node_modules/mammoth": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", + "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.1", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/merge-class-names": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", + "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" + } + }, + "node_modules/merge-refs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", + "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "peer": true + }, + "node_modules/microsoft-cognitiveservices-speech-sdk": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", + "integrity": "sha512-wPxuEXgjLdqMMIrdBtl8jquGahLV19LQE0ie8MI/PcBcNLG5buVzwS2rQEyHMsRGx+C/4OdBo1ROdNIUzCm4Lg==", + "dependencies": { + "@types/webrtc": "^0.0.37", + "agent-base": "^6.0.1", + "bent": "^7.3.12", + "https-proxy-agent": "^4.0.0", + "uuid": "^9.0.0", + "ws": "^7.5.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "peer": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-doc-viewer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", + "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", + "dependencies": { + "pdfjs-dist": "2.4.456", + "react-pdf": "5.0.0", + "styled-components": "^5.1.1", + "wl-msg-reader": "^0.2.0" + } + }, + "node_modules/react-doc-viewer/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { + "version": "2.4.456", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", + "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" + }, + "node_modules/react-doc-viewer/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-doc-viewer/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-doc-viewer/node_modules/react-pdf": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", + "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "make-cancellable-promise": "^1.0.0", + "make-event-props": "^1.1.0", + "merge-class-names": "^1.1.1", + "pdfjs-dist": "2.4.456", + "prop-types": "^15.6.2", + "worker-loader": "^3.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "react": "^16.3.0", + "react-dom": "^16.3.0" + } + }, + "node_modules/react-doc-viewer/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/react-doc-viewer/node_modules/styled-components": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", + "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true + }, + "node_modules/react-pdf": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", + "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", + "dependencies": { + "clsx": "^2.0.0", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.2.1", + "pdfjs-dist": "3.11.174", + "prop-types": "^15.6.2", + "tiny-invariant": "^1.0.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-components": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", + "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.1", + "@emotion/unitless": "0.8.0", + "@types/stylis": "4.2.0", + "css-to-react-native": "3.2.0", + "csstype": "3.1.2", + "postcss": "8.4.31", + "shallowequal": "1.1.0", + "stylis": "4.3.1", + "tslib": "2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/webpack": { + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wl-msg-reader": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", + "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" + }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } - } - }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } - } } diff --git a/frontend/package.json b/frontend/package.json index e2758f12..f7cb880b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,37 +1,37 @@ { - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "watch": "tsc && vite build --watch" - }, - "dependencies": { - "@cyntler/react-doc-viewer": "^1.14.1", - "@fluentui/react": "^8.105.3", - "@fluentui/react-icons": "^2.0.195", - "@pdftron/webviewer": "^10.7.2", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", - "@react-spring/web": "^9.7.1", - "dompurify": "^3.0.1", - "mammoth": "^1.7.0", - "microsoft-cognitiveservices-speech-sdk": "^1.27.0", - "react": "^18.2.0", - "react-doc-viewer": "^0.1.5", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" - }, - "devDependencies": { - "@types/dompurify": "^2.4.0", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", - "@vitejs/plugin-react": "^3.1.0", - "prettier": "^2.8.3", - "typescript": "^4.9.3", - "vite": "^4.1.0" - } + "name": "frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "watch": "tsc && vite build --watch" + }, + "dependencies": { + "@cyntler/react-doc-viewer": "^1.14.1", + "@fluentui/react": "^8.105.3", + "@fluentui/react-icons": "^2.0.195", + "@pdftron/webviewer": "^10.7.2", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", + "@react-spring/web": "^9.7.1", + "dompurify": "^3.0.1", + "mammoth": "^1.7.0", + "microsoft-cognitiveservices-speech-sdk": "^1.27.0", + "react": "^18.2.0", + "react-doc-viewer": "^0.1.5", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "universal-cookie": "^4.0.4" + }, + "devDependencies": { + "@types/dompurify": "^2.4.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "prettier": "^2.8.3", + "typescript": "^4.9.3", + "vite": "^4.1.0" + } } From 8964118c413929666ea393c6f5328f26771da5cd Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Wed, 20 Mar 2024 17:10:33 -0400 Subject: [PATCH 003/820] Revert "release 1.0.0" This reverts commit 3be7f1baaca90d31f72a3f63009baf2fe6568c5b. --- frontend/package-lock.json | 8186 ++++++++++++++++++------------------ frontend/package.json | 70 +- 2 files changed, 4128 insertions(+), 4128 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d3df2529..528e1489 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,4097 +1,4097 @@ { - "name": "frontend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "1.0.0", - "dependencies": { - "@cyntler/react-doc-viewer": "^1.14.1", - "@fluentui/react": "^8.105.3", - "@fluentui/react-icons": "^2.0.195", - "@pdftron/webviewer": "^10.7.2", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", - "@react-spring/web": "^9.7.1", - "dompurify": "^3.0.1", - "mammoth": "^1.7.0", - "microsoft-cognitiveservices-speech-sdk": "^1.27.0", - "react": "^18.2.0", - "react-doc-viewer": "^0.1.5", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" - }, - "devDependencies": { - "@types/dompurify": "^2.4.0", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", - "@vitejs/plugin-react": "^3.1.0", - "prettier": "^2.8.3", - "typescript": "^4.9.3", - "vite": "^4.1.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", - "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", - "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", - "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", - "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", - "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cyntler/react-doc-viewer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", - "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", - "dependencies": { - "@types/mustache": "^4.2.3", - "@types/papaparse": "^5.3.9", - "mustache": "^4.2.0", - "papaparse": "^5.4.1", - "react-pdf": "7.5.0", - "styled-components": "^6.0.8" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "react": ">=16.13.1", - "react-dom": ">=16.13.1" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fluentui/date-time-utilities": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", - "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/dom-utilities": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", - "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.32", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", - "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/foundation-legacy": { - "version": "8.2.52", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", - "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/keyboard-key": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", - "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/merge-styles": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", - "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/react": { - "version": "8.115.6", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", - "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", - "dependencies": { - "@fluentui/date-time-utilities": "^8.5.16", - "@fluentui/font-icons-mdl2": "^8.5.32", - "@fluentui/foundation-legacy": "^8.2.52", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/react-focus": "^8.8.40", - "@fluentui/react-hooks": "^8.6.36", - "@fluentui/react-portal-compat-context": "^9.0.11", - "@fluentui/react-window-provider": "^2.2.18", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/theme": "^2.6.41", - "@fluentui/utilities": "^8.13.24", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-focus": { - "version": "8.8.40", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", - "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", - "dependencies": { - "@fluentui/keyboard-key": "^0.4.14", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-hooks": { - "version": "8.6.36", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", - "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", - "dependencies": { - "@fluentui/react-window-provider": "^2.2.18", - "@fluentui/set-version": "^8.2.14", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-icons": { - "version": "2.0.232", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.232.tgz", - "integrity": "sha512-v2KKdRx68Pkz8FPQsOxvD8X7u7cCZ9/dodP/KdycaGY2FKEjAdiSzPboHfTLqkKhvrLr8Zgfs3gSDWDOf7au3A==", - "dependencies": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-portal-compat-context": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", - "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", - "dependencies": { - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-window-provider": { - "version": "2.2.18", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", - "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/set-version": { - "version": "8.2.14", - "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", - "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/style-utilities": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", - "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/theme": "^2.6.41", - "@fluentui/utilities": "^8.13.24", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/theme": { - "version": "2.6.41", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", - "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/utilities": { - "version": "8.13.24", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", - "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", - "dependencies": { - "@fluentui/dom-utilities": "^2.2.14", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/core": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.2.tgz", - "integrity": "sha512-RlsIXoSS3gaYykUgxFpwKAs/DV9cRUKp3CW1kt3iPAtsDTWn/o+8bT1jvBws/tMM2GBu/Uc0EkaIzUPqD7uA+Q==", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.0.3", - "csstype": "^3.1.3", - "rtl-css-js": "^1.16.1", - "stylis": "^4.2.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@griffel/react": { - "version": "1.5.20", - "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.20.tgz", - "integrity": "sha512-1P2yaPctENFSCwyPIYXBmgpNH68c0lc/jwSzPij1QATHDK1AASKuSeq6hW108I67RKjhRyHCcALshdZ3GcQXSg==", - "dependencies": { - "@griffel/core": "^1.15.2", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/style-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.3.tgz", - "integrity": "sha512-AzbbYV/EobNIBtfMtyu2edFin895gjVxtu1nsRhTETUAIb0/LCZoue3Jd/kFLuPwe95rv5WRUBiQpVwJsrrFcw==", - "dependencies": { - "csstype": "^3.1.3" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/@microsoft/load-themed-styles": { - "version": "1.10.295", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", - "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" - }, - "node_modules/@pdftron/webviewer": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", - "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" - }, - "node_modules/@react-pdf-viewer/attachment": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", - "integrity": "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/bookmark": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/bookmark/-/bookmark-3.12.0.tgz", - "integrity": "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/core": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/core/-/core-3.12.0.tgz", - "integrity": "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg==", - "peerDependencies": { - "pdfjs-dist": "^2.16.105 || ^3.0.279", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/default-layout": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/default-layout/-/default-layout-3.12.0.tgz", - "integrity": "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA==", - "dependencies": { - "@react-pdf-viewer/attachment": "3.12.0", - "@react-pdf-viewer/bookmark": "3.12.0", - "@react-pdf-viewer/core": "3.12.0", - "@react-pdf-viewer/thumbnail": "3.12.0", - "@react-pdf-viewer/toolbar": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/full-screen": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/full-screen/-/full-screen-3.12.0.tgz", - "integrity": "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/get-file": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/get-file/-/get-file-3.12.0.tgz", - "integrity": "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/open": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/open/-/open-3.12.0.tgz", - "integrity": "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/page-navigation": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/page-navigation/-/page-navigation-3.12.0.tgz", - "integrity": "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/print": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/print/-/print-3.12.0.tgz", - "integrity": "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/properties": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/properties/-/properties-3.12.0.tgz", - "integrity": "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/rotate": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/rotate/-/rotate-3.12.0.tgz", - "integrity": "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/scroll-mode": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/scroll-mode/-/scroll-mode-3.12.0.tgz", - "integrity": "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/search": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/search/-/search-3.12.0.tgz", - "integrity": "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/selection-mode": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/selection-mode/-/selection-mode-3.12.0.tgz", - "integrity": "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/theme": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/theme/-/theme-3.12.0.tgz", - "integrity": "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/thumbnail": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/thumbnail/-/thumbnail-3.12.0.tgz", - "integrity": "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/toolbar": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/toolbar/-/toolbar-3.12.0.tgz", - "integrity": "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0", - "@react-pdf-viewer/full-screen": "3.12.0", - "@react-pdf-viewer/get-file": "3.12.0", - "@react-pdf-viewer/open": "3.12.0", - "@react-pdf-viewer/page-navigation": "3.12.0", - "@react-pdf-viewer/print": "3.12.0", - "@react-pdf-viewer/properties": "3.12.0", - "@react-pdf-viewer/rotate": "3.12.0", - "@react-pdf-viewer/scroll-mode": "3.12.0", - "@react-pdf-viewer/search": "3.12.0", - "@react-pdf-viewer/selection-mode": "3.12.0", - "@react-pdf-viewer/theme": "3.12.0", - "@react-pdf-viewer/zoom": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/zoom": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/zoom/-/zoom-3.12.0.tgz", - "integrity": "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-spring/animated": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", - "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", - "dependencies": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", - "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/shared": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", - "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", - "dependencies": { - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", - "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" - }, - "node_modules/@react-spring/web": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", - "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", - "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" - }, - "node_modules/@types/dompurify": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", - "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", - "dev": true, - "dependencies": { - "@types/trusted-types": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", - "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "peer": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/mustache": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", - "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==" - }, - "node_modules/@types/node": { - "version": "20.11.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", - "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/papaparse": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", - "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" - }, - "node_modules/@types/react": { - "version": "18.2.64", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", - "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", - "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, - "node_modules/@types/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true - }, - "node_modules/@types/webrtc": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", - "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" - }, - "node_modules/@vitejs/plugin-react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", - "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.20.12", - "@babel/plugin-transform-react-jsx-self": "^7.18.6", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", - "magic-string": "^0.27.0", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.1.0-beta.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "peer": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bent": { - "version": "7.3.12", - "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", - "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", - "dependencies": { - "bytesish": "^0.4.1", - "caseless": "~0.12.0", - "is-stream": "^2.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "peer": true - }, - "node_modules/bytesish": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", - "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" - }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "peer": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" - }, - "node_modules/dompurify": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", - "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "dependencies": { - "underscore": "^1.13.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "optional": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", - "peer": true - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "optional": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "peer": true - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "peer": true - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lop": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", - "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-cancellable-promise": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", - "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", - "funding": { - "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-event-props": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", - "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", - "funding": { - "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" - } - }, - "node_modules/mammoth": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", - "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.1", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/merge-class-names": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", - "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" - } - }, - "node_modules/merge-refs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", - "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "peer": true - }, - "node_modules/microsoft-cognitiveservices-speech-sdk": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", - "integrity": "sha512-wPxuEXgjLdqMMIrdBtl8jquGahLV19LQE0ie8MI/PcBcNLG5buVzwS2rQEyHMsRGx+C/4OdBo1ROdNIUzCm4Lg==", - "dependencies": { - "@types/webrtc": "^0.0.37", - "agent-base": "^6.0.1", - "bent": "^7.3.12", - "https-proxy-agent": "^4.0.0", - "uuid": "^9.0.0", - "ws": "^7.5.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "peer": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "optional": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/papaparse": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", - "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", - "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", - "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", - "dependencies": { - "pdfjs-dist": "2.4.456", - "react-pdf": "5.0.0", - "styled-components": "^5.1.1", - "wl-msg-reader": "^0.2.0" - } - }, - "node_modules/react-doc-viewer/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { - "version": "2.4.456", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", - "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" - }, - "node_modules/react-doc-viewer/node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-pdf": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", - "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "make-cancellable-promise": "^1.0.0", - "make-event-props": "^1.1.0", - "merge-class-names": "^1.1.1", - "pdfjs-dist": "2.4.456", - "prop-types": "^15.6.2", - "worker-loader": "^3.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "react": "^16.3.0", - "react-dom": "^16.3.0" - } - }, - "node_modules/react-doc-viewer/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/react-doc-viewer/node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true - }, - "node_modules/react-pdf": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", - "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", - "dependencies": { - "clsx": "^2.0.0", - "make-cancellable-promise": "^1.3.1", - "make-event-props": "^1.6.0", - "merge-refs": "^1.2.1", - "pdfjs-dist": "3.11.174", - "prop-types": "^15.6.2", - "tiny-invariant": "^1.0.0", - "tiny-warning": "^1.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", - "dependencies": { - "@remix-run/router": "1.15.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", - "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rtl-css-js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", - "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", - "dependencies": { - "@babel/runtime": "^7.1.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "optional": true - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/styled-components": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", - "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", - "dependencies": { - "@emotion/is-prop-valid": "1.2.1", - "@emotion/unitless": "0.8.0", - "@types/stylis": "4.2.0", - "css-to-react-native": "3.2.0", - "csstype": "3.1.2", - "postcss": "8.4.31", - "shallowequal": "1.1.0", - "stylis": "4.3.1", - "tslib": "2.5.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/styled-components/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/terser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", - "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "optional": true - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/universal-cookie": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", - "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", - "dependencies": { - "@types/cookie": "^0.3.3", - "cookie": "^0.4.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true - }, - "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wl-msg-reader": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", - "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" - }, - "node_modules/worker-loader": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", - "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "optional": true - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@cyntler/react-doc-viewer": "^1.14.1", + "@fluentui/react": "^8.105.3", + "@fluentui/react-icons": "^2.0.195", + "@pdftron/webviewer": "^10.7.2", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", + "@react-spring/web": "^9.7.1", + "dompurify": "^3.0.1", + "mammoth": "^1.7.0", + "microsoft-cognitiveservices-speech-sdk": "^1.27.0", + "react": "^18.2.0", + "react-doc-viewer": "^0.1.5", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "universal-cookie": "^4.0.4" + }, + "devDependencies": { + "@types/dompurify": "^2.4.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "prettier": "^2.8.3", + "typescript": "^4.9.3", + "vite": "^4.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cyntler/react-doc-viewer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", + "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", + "dependencies": { + "@types/mustache": "^4.2.3", + "@types/papaparse": "^5.3.9", + "mustache": "^4.2.0", + "papaparse": "^5.4.1", + "react-pdf": "7.5.0", + "styled-components": "^6.0.8" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fluentui/date-time-utilities": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", + "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/dom-utilities": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", + "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/font-icons-mdl2": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", + "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/foundation-legacy": { + "version": "8.2.52", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", + "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/keyboard-key": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", + "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/merge-styles": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", + "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/react": { + "version": "8.115.6", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", + "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", + "dependencies": { + "@fluentui/date-time-utilities": "^8.5.16", + "@fluentui/font-icons-mdl2": "^8.5.32", + "@fluentui/foundation-legacy": "^8.2.52", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/react-focus": "^8.8.40", + "@fluentui/react-hooks": "^8.6.36", + "@fluentui/react-portal-compat-context": "^9.0.11", + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-focus": { + "version": "8.8.40", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", + "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", + "dependencies": { + "@fluentui/keyboard-key": "^0.4.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-hooks": { + "version": "8.6.36", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", + "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", + "dependencies": { + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.232", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.232.tgz", + "integrity": "sha512-v2KKdRx68Pkz8FPQsOxvD8X7u7cCZ9/dodP/KdycaGY2FKEjAdiSzPboHfTLqkKhvrLr8Zgfs3gSDWDOf7au3A==", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal-compat-context": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", + "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", + "dependencies": { + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-window-provider": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", + "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/set-version": { + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", + "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/style-utilities": { + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", + "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/theme": { + "version": "2.6.41", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", + "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/utilities": { + "version": "8.13.24", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", + "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", + "dependencies": { + "@fluentui/dom-utilities": "^2.2.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.2.tgz", + "integrity": "sha512-RlsIXoSS3gaYykUgxFpwKAs/DV9cRUKp3CW1kt3iPAtsDTWn/o+8bT1jvBws/tMM2GBu/Uc0EkaIzUPqD7uA+Q==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.0.3", + "csstype": "^3.1.3", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.20", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.20.tgz", + "integrity": "sha512-1P2yaPctENFSCwyPIYXBmgpNH68c0lc/jwSzPij1QATHDK1AASKuSeq6hW108I67RKjhRyHCcALshdZ3GcQXSg==", + "dependencies": { + "@griffel/core": "^1.15.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.3.tgz", + "integrity": "sha512-AzbbYV/EobNIBtfMtyu2edFin895gjVxtu1nsRhTETUAIb0/LCZoue3Jd/kFLuPwe95rv5WRUBiQpVwJsrrFcw==", + "dependencies": { + "csstype": "^3.1.3" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/@microsoft/load-themed-styles": { + "version": "1.10.295", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", + "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" + }, + "node_modules/@pdftron/webviewer": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", + "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" + }, + "node_modules/@react-pdf-viewer/attachment": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", + "integrity": "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/bookmark": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/bookmark/-/bookmark-3.12.0.tgz", + "integrity": "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/core": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/core/-/core-3.12.0.tgz", + "integrity": "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg==", + "peerDependencies": { + "pdfjs-dist": "^2.16.105 || ^3.0.279", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/default-layout": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/default-layout/-/default-layout-3.12.0.tgz", + "integrity": "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA==", + "dependencies": { + "@react-pdf-viewer/attachment": "3.12.0", + "@react-pdf-viewer/bookmark": "3.12.0", + "@react-pdf-viewer/core": "3.12.0", + "@react-pdf-viewer/thumbnail": "3.12.0", + "@react-pdf-viewer/toolbar": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/full-screen": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/full-screen/-/full-screen-3.12.0.tgz", + "integrity": "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/get-file": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/get-file/-/get-file-3.12.0.tgz", + "integrity": "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/open": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/open/-/open-3.12.0.tgz", + "integrity": "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/page-navigation": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/page-navigation/-/page-navigation-3.12.0.tgz", + "integrity": "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/print": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/print/-/print-3.12.0.tgz", + "integrity": "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/properties": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/properties/-/properties-3.12.0.tgz", + "integrity": "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/rotate": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/rotate/-/rotate-3.12.0.tgz", + "integrity": "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/scroll-mode": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/scroll-mode/-/scroll-mode-3.12.0.tgz", + "integrity": "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/search": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/search/-/search-3.12.0.tgz", + "integrity": "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/selection-mode": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/selection-mode/-/selection-mode-3.12.0.tgz", + "integrity": "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/theme": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/theme/-/theme-3.12.0.tgz", + "integrity": "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/thumbnail": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/thumbnail/-/thumbnail-3.12.0.tgz", + "integrity": "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/toolbar": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/toolbar/-/toolbar-3.12.0.tgz", + "integrity": "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0", + "@react-pdf-viewer/full-screen": "3.12.0", + "@react-pdf-viewer/get-file": "3.12.0", + "@react-pdf-viewer/open": "3.12.0", + "@react-pdf-viewer/page-navigation": "3.12.0", + "@react-pdf-viewer/print": "3.12.0", + "@react-pdf-viewer/properties": "3.12.0", + "@react-pdf-viewer/rotate": "3.12.0", + "@react-pdf-viewer/scroll-mode": "3.12.0", + "@react-pdf-viewer/search": "3.12.0", + "@react-pdf-viewer/selection-mode": "3.12.0", + "@react-pdf-viewer/theme": "3.12.0", + "@react-pdf-viewer/zoom": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/zoom": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/zoom/-/zoom-3.12.0.tgz", + "integrity": "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, + "node_modules/@types/dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", + "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==" + }, + "node_modules/@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "node_modules/@types/react": { + "version": "18.2.64", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", + "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", + "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, + "node_modules/@types/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, + "node_modules/@types/webrtc": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", + "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "peer": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "dependencies": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "peer": true + }, + "node_modules/bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" + }, + "node_modules/dompurify": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", + "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "dependencies": { + "underscore": "^1.13.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.701", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", + "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "peer": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lop": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", + "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-cancellable-promise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-event-props": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, + "node_modules/mammoth": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", + "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.1", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/merge-class-names": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", + "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" + } + }, + "node_modules/merge-refs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", + "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "peer": true + }, + "node_modules/microsoft-cognitiveservices-speech-sdk": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", + "integrity": "sha512-wPxuEXgjLdqMMIrdBtl8jquGahLV19LQE0ie8MI/PcBcNLG5buVzwS2rQEyHMsRGx+C/4OdBo1ROdNIUzCm4Lg==", + "dependencies": { + "@types/webrtc": "^0.0.37", + "agent-base": "^6.0.1", + "bent": "^7.3.12", + "https-proxy-agent": "^4.0.0", + "uuid": "^9.0.0", + "ws": "^7.5.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "peer": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-doc-viewer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", + "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", + "dependencies": { + "pdfjs-dist": "2.4.456", + "react-pdf": "5.0.0", + "styled-components": "^5.1.1", + "wl-msg-reader": "^0.2.0" + } + }, + "node_modules/react-doc-viewer/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { + "version": "2.4.456", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", + "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" + }, + "node_modules/react-doc-viewer/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-doc-viewer/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-doc-viewer/node_modules/react-pdf": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", + "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "make-cancellable-promise": "^1.0.0", + "make-event-props": "^1.1.0", + "merge-class-names": "^1.1.1", + "pdfjs-dist": "2.4.456", + "prop-types": "^15.6.2", + "worker-loader": "^3.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "react": "^16.3.0", + "react-dom": "^16.3.0" + } + }, + "node_modules/react-doc-viewer/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/react-doc-viewer/node_modules/styled-components": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", + "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true + }, + "node_modules/react-pdf": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", + "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", + "dependencies": { + "clsx": "^2.0.0", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.2.1", + "pdfjs-dist": "3.11.174", + "prop-types": "^15.6.2", + "tiny-invariant": "^1.0.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-components": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", + "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.1", + "@emotion/unitless": "0.8.0", + "@types/stylis": "4.2.0", + "css-to-react-native": "3.2.0", + "csstype": "3.1.2", + "postcss": "8.4.31", + "shallowequal": "1.1.0", + "stylis": "4.3.1", + "tslib": "2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/webpack": { + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wl-msg-reader": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", + "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" + }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true } + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } + } } diff --git a/frontend/package.json b/frontend/package.json index f7cb880b..e2758f12 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,37 +1,37 @@ { - "name": "frontend", - "private": true, - "version": "1.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "watch": "tsc && vite build --watch" - }, - "dependencies": { - "@cyntler/react-doc-viewer": "^1.14.1", - "@fluentui/react": "^8.105.3", - "@fluentui/react-icons": "^2.0.195", - "@pdftron/webviewer": "^10.7.2", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", - "@react-spring/web": "^9.7.1", - "dompurify": "^3.0.1", - "mammoth": "^1.7.0", - "microsoft-cognitiveservices-speech-sdk": "^1.27.0", - "react": "^18.2.0", - "react-doc-viewer": "^0.1.5", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" - }, - "devDependencies": { - "@types/dompurify": "^2.4.0", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", - "@vitejs/plugin-react": "^3.1.0", - "prettier": "^2.8.3", - "typescript": "^4.9.3", - "vite": "^4.1.0" - } + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "watch": "tsc && vite build --watch" + }, + "dependencies": { + "@cyntler/react-doc-viewer": "^1.14.1", + "@fluentui/react": "^8.105.3", + "@fluentui/react-icons": "^2.0.195", + "@pdftron/webviewer": "^10.7.2", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", + "@react-spring/web": "^9.7.1", + "dompurify": "^3.0.1", + "mammoth": "^1.7.0", + "microsoft-cognitiveservices-speech-sdk": "^1.27.0", + "react": "^18.2.0", + "react-doc-viewer": "^0.1.5", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "universal-cookie": "^4.0.4" + }, + "devDependencies": { + "@types/dompurify": "^2.4.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "prettier": "^2.8.3", + "typescript": "^4.9.3", + "vite": "^4.1.0" + } } From 38ff0554c729f477e9b94e0d38eb8a9c984d1bd5 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Wed, 20 Mar 2024 17:12:22 -0400 Subject: [PATCH 004/820] release v1.0.0 --- frontend/package-lock.json | 8186 ++++++++++++++++++------------------ frontend/package.json | 70 +- 2 files changed, 4128 insertions(+), 4128 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 528e1489..d3df2529 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,4097 +1,4097 @@ { - "name": "frontend", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "version": "0.0.0", - "dependencies": { - "@cyntler/react-doc-viewer": "^1.14.1", - "@fluentui/react": "^8.105.3", - "@fluentui/react-icons": "^2.0.195", - "@pdftron/webviewer": "^10.7.2", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", - "@react-spring/web": "^9.7.1", - "dompurify": "^3.0.1", - "mammoth": "^1.7.0", - "microsoft-cognitiveservices-speech-sdk": "^1.27.0", - "react": "^18.2.0", - "react-doc-viewer": "^0.1.5", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" - }, - "devDependencies": { - "@types/dompurify": "^2.4.0", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", - "@vitejs/plugin-react": "^3.1.0", - "prettier": "^2.8.3", - "typescript": "^4.9.3", - "vite": "^4.1.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", - "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", - "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", - "dependencies": { - "@babel/types": "^7.22.15" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", - "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", - "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", - "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", - "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", - "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", - "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", - "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", - "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cyntler/react-doc-viewer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", - "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", - "dependencies": { - "@types/mustache": "^4.2.3", - "@types/papaparse": "^5.3.9", - "mustache": "^4.2.0", - "papaparse": "^5.4.1", - "react-pdf": "7.5.0", - "styled-components": "^6.0.8" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "react": ">=16.13.1", - "react-dom": ">=16.13.1" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", - "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" - }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fluentui/date-time-utilities": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", - "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/dom-utilities": { - "version": "2.2.14", - "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", - "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/font-icons-mdl2": { - "version": "8.5.32", - "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", - "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/foundation-legacy": { - "version": "8.2.52", - "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", - "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/keyboard-key": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", - "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/merge-styles": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", - "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/react": { - "version": "8.115.6", - "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", - "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", - "dependencies": { - "@fluentui/date-time-utilities": "^8.5.16", - "@fluentui/font-icons-mdl2": "^8.5.32", - "@fluentui/foundation-legacy": "^8.2.52", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/react-focus": "^8.8.40", - "@fluentui/react-hooks": "^8.6.36", - "@fluentui/react-portal-compat-context": "^9.0.11", - "@fluentui/react-window-provider": "^2.2.18", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/theme": "^2.6.41", - "@fluentui/utilities": "^8.13.24", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "@types/react-dom": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0", - "react-dom": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-focus": { - "version": "8.8.40", - "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", - "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", - "dependencies": { - "@fluentui/keyboard-key": "^0.4.14", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/style-utilities": "^8.10.3", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-hooks": { - "version": "8.6.36", - "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", - "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", - "dependencies": { - "@fluentui/react-window-provider": "^2.2.18", - "@fluentui/set-version": "^8.2.14", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-icons": { - "version": "2.0.232", - "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.232.tgz", - "integrity": "sha512-v2KKdRx68Pkz8FPQsOxvD8X7u7cCZ9/dodP/KdycaGY2FKEjAdiSzPboHfTLqkKhvrLr8Zgfs3gSDWDOf7au3A==", - "dependencies": { - "@griffel/react": "^1.0.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-portal-compat-context": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", - "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", - "dependencies": { - "@swc/helpers": "^0.5.1" - }, - "peerDependencies": { - "@types/react": ">=16.14.0 <19.0.0", - "react": ">=16.14.0 <19.0.0" - } - }, - "node_modules/@fluentui/react-window-provider": { - "version": "2.2.18", - "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", - "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", - "dependencies": { - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/set-version": { - "version": "8.2.14", - "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", - "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/style-utilities": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", - "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/theme": "^2.6.41", - "@fluentui/utilities": "^8.13.24", - "@microsoft/load-themed-styles": "^1.10.26", - "tslib": "^2.1.0" - } - }, - "node_modules/@fluentui/theme": { - "version": "2.6.41", - "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", - "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", - "dependencies": { - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "@fluentui/utilities": "^8.13.24", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@fluentui/utilities": { - "version": "8.13.24", - "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", - "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", - "dependencies": { - "@fluentui/dom-utilities": "^2.2.14", - "@fluentui/merge-styles": "^8.5.15", - "@fluentui/set-version": "^8.2.14", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@types/react": ">=16.8.0 <19.0.0", - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/core": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.2.tgz", - "integrity": "sha512-RlsIXoSS3gaYykUgxFpwKAs/DV9cRUKp3CW1kt3iPAtsDTWn/o+8bT1jvBws/tMM2GBu/Uc0EkaIzUPqD7uA+Q==", - "dependencies": { - "@emotion/hash": "^0.9.0", - "@griffel/style-types": "^1.0.3", - "csstype": "^3.1.3", - "rtl-css-js": "^1.16.1", - "stylis": "^4.2.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@griffel/react": { - "version": "1.5.20", - "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.20.tgz", - "integrity": "sha512-1P2yaPctENFSCwyPIYXBmgpNH68c0lc/jwSzPij1QATHDK1AASKuSeq6hW108I67RKjhRyHCcALshdZ3GcQXSg==", - "dependencies": { - "@griffel/core": "^1.15.2", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": ">=16.8.0 <19.0.0" - } - }, - "node_modules/@griffel/style-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.3.tgz", - "integrity": "sha512-AzbbYV/EobNIBtfMtyu2edFin895gjVxtu1nsRhTETUAIb0/LCZoue3Jd/kFLuPwe95rv5WRUBiQpVwJsrrFcw==", - "dependencies": { - "csstype": "^3.1.3" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/@microsoft/load-themed-styles": { - "version": "1.10.295", - "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", - "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" - }, - "node_modules/@pdftron/webviewer": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", - "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" - }, - "node_modules/@react-pdf-viewer/attachment": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", - "integrity": "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/bookmark": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/bookmark/-/bookmark-3.12.0.tgz", - "integrity": "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/core": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/core/-/core-3.12.0.tgz", - "integrity": "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg==", - "peerDependencies": { - "pdfjs-dist": "^2.16.105 || ^3.0.279", - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/default-layout": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/default-layout/-/default-layout-3.12.0.tgz", - "integrity": "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA==", - "dependencies": { - "@react-pdf-viewer/attachment": "3.12.0", - "@react-pdf-viewer/bookmark": "3.12.0", - "@react-pdf-viewer/core": "3.12.0", - "@react-pdf-viewer/thumbnail": "3.12.0", - "@react-pdf-viewer/toolbar": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/full-screen": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/full-screen/-/full-screen-3.12.0.tgz", - "integrity": "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/get-file": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/get-file/-/get-file-3.12.0.tgz", - "integrity": "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/open": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/open/-/open-3.12.0.tgz", - "integrity": "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/page-navigation": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/page-navigation/-/page-navigation-3.12.0.tgz", - "integrity": "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/print": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/print/-/print-3.12.0.tgz", - "integrity": "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/properties": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/properties/-/properties-3.12.0.tgz", - "integrity": "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/rotate": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/rotate/-/rotate-3.12.0.tgz", - "integrity": "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/scroll-mode": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/scroll-mode/-/scroll-mode-3.12.0.tgz", - "integrity": "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/search": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/search/-/search-3.12.0.tgz", - "integrity": "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/selection-mode": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/selection-mode/-/selection-mode-3.12.0.tgz", - "integrity": "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/theme": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/theme/-/theme-3.12.0.tgz", - "integrity": "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/thumbnail": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/thumbnail/-/thumbnail-3.12.0.tgz", - "integrity": "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/toolbar": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/toolbar/-/toolbar-3.12.0.tgz", - "integrity": "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0", - "@react-pdf-viewer/full-screen": "3.12.0", - "@react-pdf-viewer/get-file": "3.12.0", - "@react-pdf-viewer/open": "3.12.0", - "@react-pdf-viewer/page-navigation": "3.12.0", - "@react-pdf-viewer/print": "3.12.0", - "@react-pdf-viewer/properties": "3.12.0", - "@react-pdf-viewer/rotate": "3.12.0", - "@react-pdf-viewer/scroll-mode": "3.12.0", - "@react-pdf-viewer/search": "3.12.0", - "@react-pdf-viewer/selection-mode": "3.12.0", - "@react-pdf-viewer/theme": "3.12.0", - "@react-pdf-viewer/zoom": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-pdf-viewer/zoom": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@react-pdf-viewer/zoom/-/zoom-3.12.0.tgz", - "integrity": "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA==", - "dependencies": { - "@react-pdf-viewer/core": "3.12.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@react-spring/animated": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", - "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", - "dependencies": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", - "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/shared": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", - "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", - "dependencies": { - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", - "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" - }, - "node_modules/@react-spring/web": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", - "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", - "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" - }, - "node_modules/@types/dompurify": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", - "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", - "dev": true, - "dependencies": { - "@types/trusted-types": "*" - } - }, - "node_modules/@types/eslint": { - "version": "8.56.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", - "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "peer": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/mustache": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", - "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==" - }, - "node_modules/@types/node": { - "version": "20.11.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", - "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/papaparse": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", - "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" - }, - "node_modules/@types/react": { - "version": "18.2.64", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", - "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.21", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", - "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" - }, - "node_modules/@types/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true - }, - "node_modules/@types/webrtc": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", - "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" - }, - "node_modules/@vitejs/plugin-react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", - "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.20.12", - "@babel/plugin-transform-react-jsx-self": "^7.18.6", - "@babel/plugin-transform-react-jsx-source": "^7.19.6", - "magic-string": "^0.27.0", - "react-refresh": "^0.14.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.1.0-beta.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "peer": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "peer": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bent": { - "version": "7.3.12", - "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", - "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", - "dependencies": { - "bytesish": "^0.4.1", - "caseless": "~0.12.0", - "is-stream": "^2.0.0" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, - "node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "peer": true - }, - "node_modules/bytesish": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", - "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" - }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/canvas": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", - "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.17.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "optional": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "peer": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "optional": true - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" - }, - "node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "optional": true, - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "optional": true - }, - "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/dingbat-to-unicode": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", - "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" - }, - "node_modules/dompurify": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", - "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" - }, - "node_modules/duck": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", - "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", - "dependencies": { - "underscore": "^1.13.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "optional": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", - "peer": true - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "optional": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "peer": true - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "peer": true - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "optional": true - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "dependencies": { - "agent-base": "5", - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/https-proxy-agent/node_modules/agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lop": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", - "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", - "dependencies": { - "duck": "^0.1.12", - "option": "~0.2.1", - "underscore": "^1.13.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-cancellable-promise": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", - "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", - "funding": { - "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "optional": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-event-props": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", - "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", - "funding": { - "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" - } - }, - "node_modules/mammoth": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", - "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", - "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.1", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/merge-class-names": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", - "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" - } - }, - "node_modules/merge-refs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", - "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "peer": true - }, - "node_modules/microsoft-cognitiveservices-speech-sdk": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", - "integrity": "sha512-wPxuEXgjLdqMMIrdBtl8jquGahLV19LQE0ie8MI/PcBcNLG5buVzwS2rQEyHMsRGx+C/4OdBo1ROdNIUzCm4Lg==", - "dependencies": { - "@types/webrtc": "^0.0.37", - "agent-base": "^6.0.1", - "bent": "^7.3.12", - "https-proxy-agent": "^4.0.0", - "uuid": "^9.0.0", - "ws": "^7.5.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "optional": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "optional": true, - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "optional": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/mustache": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", - "bin": { - "mustache": "bin/mustache" - } - }, - "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "peer": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "optional": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "optional": true, - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/option": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", - "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "node_modules/papaparse": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", - "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path2d-polyfill": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", - "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pdfjs-dist": { - "version": "3.11.174", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", - "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "canvas": "^2.11.2", - "path2d-polyfill": "^2.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", - "dev": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", - "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", - "dependencies": { - "pdfjs-dist": "2.4.456", - "react-pdf": "5.0.0", - "styled-components": "^5.1.1", - "wl-msg-reader": "^0.2.0" - } - }, - "node_modules/react-doc-viewer/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { - "version": "2.4.456", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", - "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" - }, - "node_modules/react-doc-viewer/node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" - }, - "peerDependencies": { - "react": "^16.14.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-pdf": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", - "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", - "dependencies": { - "@babel/runtime": "^7.0.0", - "make-cancellable-promise": "^1.0.0", - "make-event-props": "^1.1.0", - "merge-class-names": "^1.1.1", - "pdfjs-dist": "2.4.456", - "prop-types": "^15.6.2", - "worker-loader": "^3.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "react": "^16.3.0", - "react-dom": "^16.3.0" - } - }, - "node_modules/react-doc-viewer/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/react-doc-viewer/node_modules/styled-components": { - "version": "5.3.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", - "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true - }, - "node_modules/react-pdf": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", - "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", - "dependencies": { - "clsx": "^2.0.0", - "make-cancellable-promise": "^1.3.1", - "make-event-props": "^1.6.0", - "merge-refs": "^1.2.1", - "pdfjs-dist": "3.11.174", - "prop-types": "^15.6.2", - "tiny-invariant": "^1.0.0", - "tiny-warning": "^1.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", - "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", - "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", - "dependencies": { - "@remix-run/router": "1.15.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.22.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", - "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", - "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/rtl-css-js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", - "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", - "dependencies": { - "@babel/runtime": "^7.1.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "optional": true - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "optional": true, - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "peer": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/styled-components": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", - "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", - "dependencies": { - "@emotion/is-prop-valid": "1.2.1", - "@emotion/unitless": "0.8.0", - "@types/stylis": "4.2.0", - "css-to-react-native": "3.2.0", - "csstype": "3.1.2", - "postcss": "8.4.31", - "shallowequal": "1.1.0", - "stylis": "4.3.1", - "tslib": "2.5.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/styled-components/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, - "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", - "optional": true, - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "optional": true - }, - "node_modules/terser": { - "version": "5.29.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", - "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "optional": true - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/universal-cookie": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", - "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", - "dependencies": { - "@types/cookie": "^0.3.3", - "cookie": "^0.4.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true - }, - "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wl-msg-reader": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", - "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" - }, - "node_modules/worker-loader": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", - "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "optional": true - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true + "name": "frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "1.0.0", + "dependencies": { + "@cyntler/react-doc-viewer": "^1.14.1", + "@fluentui/react": "^8.105.3", + "@fluentui/react-icons": "^2.0.195", + "@pdftron/webviewer": "^10.7.2", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", + "@react-spring/web": "^9.7.1", + "dompurify": "^3.0.1", + "mammoth": "^1.7.0", + "microsoft-cognitiveservices-speech-sdk": "^1.27.0", + "react": "^18.2.0", + "react-doc-viewer": "^0.1.5", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "universal-cookie": "^4.0.4" + }, + "devDependencies": { + "@types/dompurify": "^2.4.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "prettier": "^2.8.3", + "typescript": "^4.9.3", + "vite": "^4.1.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz", + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", + "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz", + "integrity": "sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.23.3.tgz", + "integrity": "sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz", + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cyntler/react-doc-viewer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", + "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", + "dependencies": { + "@types/mustache": "^4.2.3", + "@types/papaparse": "^5.3.9", + "mustache": "^4.2.0", + "papaparse": "^5.4.1", + "react-pdf": "7.5.0", + "styled-components": "^6.0.8" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", + "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fluentui/date-time-utilities": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-8.5.16.tgz", + "integrity": "sha512-l+mLfJ2VhdHjBpELLLPDaWgT7GMLynm2aqR7SttbEb6Jh7hc/7ck1MWm93RTb3gYVHYai8SENqimNcvIxHt/zg==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/dom-utilities": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-2.2.14.tgz", + "integrity": "sha512-+4DVm5sNfJh+l8fM+7ylpOkGNZkNr4X1z1uKQPzRJ1PRhlnvc6vLpWNNicGwpjTbgufSrVtGKXwP5sf++r81lg==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/font-icons-mdl2": { + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/@fluentui/font-icons-mdl2/-/font-icons-mdl2-8.5.32.tgz", + "integrity": "sha512-PCZMijJlDQ5Zy8oNb80vUD6I4ORiR03qFgDT8o08mAGu+KzQO96q4jm0rzPRQuI9CO7pDD/6naOo8UVrmhZ2Aw==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/foundation-legacy": { + "version": "8.2.52", + "resolved": "https://registry.npmjs.org/@fluentui/foundation-legacy/-/foundation-legacy-8.2.52.tgz", + "integrity": "sha512-tHCD0m58Zja7wN1FTsvj4Gaj0B22xOhRTpyDzyvxRfjFGYPpR2Jgx/y/KRB3JTOX5EfJHAVzInyWZBeN5IfsVA==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/keyboard-key": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.4.14.tgz", + "integrity": "sha512-XzZHcyFEM20H23h3i15UpkHi2AhRBriXPGAHq0Jm98TKFppXehedjjEFuUsh+CyU5JKBhDalWp8TAQ1ArpNzow==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/merge-styles": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/@fluentui/merge-styles/-/merge-styles-8.5.15.tgz", + "integrity": "sha512-4CdKwo4k1Un2QLulpSVIz/KMgLNBMgin4NPyapmKDMVuO1OOxJUqfocubRGNO5x9mKgAMMYwBKGO9i0uxMMpJw==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/react": { + "version": "8.115.6", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-8.115.6.tgz", + "integrity": "sha512-lao6u6AfA9uE+jWsmmRriCYXlQ9IU3W2jlapJiOJGyQvF9JGdVCyKDi2w4dIvsJyhA4ucfcKqg+9EgyrgbWcNg==", + "dependencies": { + "@fluentui/date-time-utilities": "^8.5.16", + "@fluentui/font-icons-mdl2": "^8.5.32", + "@fluentui/foundation-legacy": "^8.2.52", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/react-focus": "^8.8.40", + "@fluentui/react-hooks": "^8.6.36", + "@fluentui/react-portal-compat-context": "^9.0.11", + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-focus": { + "version": "8.8.40", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-8.8.40.tgz", + "integrity": "sha512-ha0CbLv5EIbjYCtQky6LVZObxOeMfhixrgrzfXm3Ta2eGs1NyZRDm1VeM6acOolWB/8QiN/CbdGckjALli8L2g==", + "dependencies": { + "@fluentui/keyboard-key": "^0.4.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/style-utilities": "^8.10.3", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-hooks": { + "version": "8.6.36", + "resolved": "https://registry.npmjs.org/@fluentui/react-hooks/-/react-hooks-8.6.36.tgz", + "integrity": "sha512-kI0Z4Q4xHUs4SOmmI5n5OH5fPckqMSCovTRpiuxzCO2TNzLmfC861+nqf4Ygw/ChqNm2gWNZZfUADfnNAEsq+Q==", + "dependencies": { + "@fluentui/react-window-provider": "^2.2.18", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.232", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.232.tgz", + "integrity": "sha512-v2KKdRx68Pkz8FPQsOxvD8X7u7cCZ9/dodP/KdycaGY2FKEjAdiSzPboHfTLqkKhvrLr8Zgfs3gSDWDOf7au3A==", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal-compat-context": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal-compat-context/-/react-portal-compat-context-9.0.11.tgz", + "integrity": "sha512-ubvW/ej0O+Pago9GH3mPaxzUgsNnBoqvghNamWjyKvZIViyaXUG6+sgcAl721R+qGAFac+A20akI5qDJz/xtdg==", + "dependencies": { + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-window-provider": { + "version": "2.2.18", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-2.2.18.tgz", + "integrity": "sha512-nBKqxd0P8NmIR0qzFvka1urE2LVbUm6cse1I1T7TcOVNYa5jDf5BrO06+JRZfwbn00IJqOnIVoP0qONqceypWQ==", + "dependencies": { + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/set-version": { + "version": "8.2.14", + "resolved": "https://registry.npmjs.org/@fluentui/set-version/-/set-version-8.2.14.tgz", + "integrity": "sha512-f/QWJnSeyfAjGAqq57yjMb6a5ejPlwfzdExPmzFBuEOuupi8hHbV8Yno12XJcTW4I0KXEQGw+PUaM1aOf/j7jw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/style-utilities": { + "version": "8.10.3", + "resolved": "https://registry.npmjs.org/@fluentui/style-utilities/-/style-utilities-8.10.3.tgz", + "integrity": "sha512-pyO9BGkwIxXaIMVT6ma98GIZAgTjGc0LZ5iUai9GLIrFLQWnIKnS//hgUx8qG4AecUeqZ26Wb0e+Ale9NyPQCQ==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/theme": "^2.6.41", + "@fluentui/utilities": "^8.13.24", + "@microsoft/load-themed-styles": "^1.10.26", + "tslib": "^2.1.0" + } + }, + "node_modules/@fluentui/theme": { + "version": "2.6.41", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-2.6.41.tgz", + "integrity": "sha512-h9RguEzqzJ0+59ys5Kkp7JtsjhDUxBLmQunu5rpHp5Mp788OtEjI/n1a9FIcOAL/priPSQwXN7RbuDpeP7+aSw==", + "dependencies": { + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "@fluentui/utilities": "^8.13.24", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/utilities": { + "version": "8.13.24", + "resolved": "https://registry.npmjs.org/@fluentui/utilities/-/utilities-8.13.24.tgz", + "integrity": "sha512-/jo6hWCzTGCx06l2baAMwsjjBZ/dyMouls53uNaQLUGUUhUwXh/DcDDXMqLRJB3MaH9zvgfvRw61iKmm2s9fIA==", + "dependencies": { + "@fluentui/dom-utilities": "^2.2.14", + "@fluentui/merge-styles": "^8.5.15", + "@fluentui/set-version": "^8.2.14", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.2.tgz", + "integrity": "sha512-RlsIXoSS3gaYykUgxFpwKAs/DV9cRUKp3CW1kt3iPAtsDTWn/o+8bT1jvBws/tMM2GBu/Uc0EkaIzUPqD7uA+Q==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.0.3", + "csstype": "^3.1.3", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.20", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.20.tgz", + "integrity": "sha512-1P2yaPctENFSCwyPIYXBmgpNH68c0lc/jwSzPij1QATHDK1AASKuSeq6hW108I67RKjhRyHCcALshdZ3GcQXSg==", + "dependencies": { + "@griffel/core": "^1.15.2", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.3.tgz", + "integrity": "sha512-AzbbYV/EobNIBtfMtyu2edFin895gjVxtu1nsRhTETUAIb0/LCZoue3Jd/kFLuPwe95rv5WRUBiQpVwJsrrFcw==", + "dependencies": { + "csstype": "^3.1.3" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/@microsoft/load-themed-styles": { + "version": "1.10.295", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.295.tgz", + "integrity": "sha512-W+IzEBw8a6LOOfRJM02dTT7BDZijxm+Z7lhtOAz1+y9vQm1Kdz9jlAO+qCEKsfxtUOmKilW8DIRqFw2aUgKeGg==" + }, + "node_modules/@pdftron/webviewer": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", + "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" + }, + "node_modules/@react-pdf-viewer/attachment": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", + "integrity": "sha512-mhwrYJSIpCvHdERpLUotqhMgSjhtF+BTY1Yb9Fnzpcq3gLZP+Twp5Rynq21tCrVdDizPaVY7SKu400GkgdMfZw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/bookmark": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/bookmark/-/bookmark-3.12.0.tgz", + "integrity": "sha512-i7nEit8vIFMAES8RFGwprZ9cXOOZb9ZStPW6E6yuObJEXcvBj/ctsbBJGZxqUZOGklM0JoB7sjHyxAriHfe92A==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/core": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/core/-/core-3.12.0.tgz", + "integrity": "sha512-8MsdlQJ4jaw3GT+zpCHS33nwnvzpY0ED6DEahZg9WngG++A5RMhk8LSlxdHelwaFFHFiXBjmOaj2Kpxh50VQRg==", + "peerDependencies": { + "pdfjs-dist": "^2.16.105 || ^3.0.279", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/default-layout": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/default-layout/-/default-layout-3.12.0.tgz", + "integrity": "sha512-K2fS4+TJynHxxCBFuIDiFuAw3nqOh4bkBgtVZ/2pGvnFn9lLg46YGLMnTXCQqtyZzzXYh696jmlFViun3is4pA==", + "dependencies": { + "@react-pdf-viewer/attachment": "3.12.0", + "@react-pdf-viewer/bookmark": "3.12.0", + "@react-pdf-viewer/core": "3.12.0", + "@react-pdf-viewer/thumbnail": "3.12.0", + "@react-pdf-viewer/toolbar": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/full-screen": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/full-screen/-/full-screen-3.12.0.tgz", + "integrity": "sha512-hQouJ26QUaRBCXNMU1aI1zpJn4l4PJRvlHhuE2dZYtLl37ycjl7vBCQYZW1FwnuxMWztZsY47R43DKaZORg0pg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/get-file": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/get-file/-/get-file-3.12.0.tgz", + "integrity": "sha512-Uhq45n2RWlZ7Ec/BtBJ0WQESRciaYIltveDXHNdWvXgFdOS8XsvB+mnTh/wzm7Cfl9hpPyzfeezifdU9AkQgQg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/open": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/open/-/open-3.12.0.tgz", + "integrity": "sha512-vhiDEYsiQLxvZkIKT9VPYHZ1BOnv46x9eCEmRWxO1DJ8fa/GRDTA9ivXmq/ap0dGEJs6t+epleCkCEfllLR/Yw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/page-navigation": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/page-navigation/-/page-navigation-3.12.0.tgz", + "integrity": "sha512-tVEJ48Dd5kajV1nKkrPWijglJRNBiKBTyYDKVexhiRdTHUP1f6QQXiSyDgCUb0IGSZeJzOJb1h7ApKHe8OTtuw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/print": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/print/-/print-3.12.0.tgz", + "integrity": "sha512-xJn76CgbU/M2iNaN7wLHTg+sdOekkRMfCakFLwPrE+SR7qD6NUF4vQQKJBSVCCK5bUijzb6cWfKGfo8VA72o4Q==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/properties": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/properties/-/properties-3.12.0.tgz", + "integrity": "sha512-dYTCHtVwFNkpDo7QxL2qk/8zAKndLwdD1FFxBftl6jIlQbtvNdxkFfkv1HcQING9Ic+7DBryOiD7W0ze4IERYg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/rotate": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/rotate/-/rotate-3.12.0.tgz", + "integrity": "sha512-yaxaMYPChvNOjR8+AxRmj0kvojyJKPq4XHEcIB2lJJgBY1Zra3mliDUP3Nlb4yV8BS9+yBqWn9U9mtnopQD+tw==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/scroll-mode": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/scroll-mode/-/scroll-mode-3.12.0.tgz", + "integrity": "sha512-okII7Xqhl6cMvl1izdEvlXNJ+vJVq/qdg53hJIDYVgBCWskLk/cpjUg/ZonBxseG9lIDP3w2VO1McT8Gn11OAg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/search": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/search/-/search-3.12.0.tgz", + "integrity": "sha512-jAkLpis49fsDDY/HrbUZIOIhzF5vynONQNA4INQKI38r/MjveblrkNv7qbr9j5lQ/WFic5+gD1e+Mtpf1/7DiA==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/selection-mode": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/selection-mode/-/selection-mode-3.12.0.tgz", + "integrity": "sha512-yysWEu2aCtBvzSgbhgI9kT5cq2hf0FU6Z+3B7MMXz14Kxyc3y18wUqxtgbvpFEfWF0bNUUq16JtWRljtxvZ83w==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/theme": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/theme/-/theme-3.12.0.tgz", + "integrity": "sha512-cdBi+wR1VOZ6URCcO9plmAZQu4ZGFcd7HJdBe7VIFiGyrvl9I/Of74ONLycnDImSuONt8D3uNjPBLieeaShVeg==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/thumbnail": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/thumbnail/-/thumbnail-3.12.0.tgz", + "integrity": "sha512-Vc8j3bO6wumWZV4o6pAbktPWKDSC9tQAzOCJ3cof541u4i44C11ccYC4W9aNcsMMUSO3bNwAGWtP8OFthV5akQ==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/toolbar": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/toolbar/-/toolbar-3.12.0.tgz", + "integrity": "sha512-qACTU3qXHgtNK8J+T13EWio+0liilj86SJ87BdapqXynhl720OKPlSKOQqskUGqg3oTUJAhrse9XG6SFdHJx+g==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0", + "@react-pdf-viewer/full-screen": "3.12.0", + "@react-pdf-viewer/get-file": "3.12.0", + "@react-pdf-viewer/open": "3.12.0", + "@react-pdf-viewer/page-navigation": "3.12.0", + "@react-pdf-viewer/print": "3.12.0", + "@react-pdf-viewer/properties": "3.12.0", + "@react-pdf-viewer/rotate": "3.12.0", + "@react-pdf-viewer/scroll-mode": "3.12.0", + "@react-pdf-viewer/search": "3.12.0", + "@react-pdf-viewer/selection-mode": "3.12.0", + "@react-pdf-viewer/theme": "3.12.0", + "@react-pdf-viewer/zoom": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-pdf-viewer/zoom": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@react-pdf-viewer/zoom/-/zoom-3.12.0.tgz", + "integrity": "sha512-V0GUTyPM77+LzhoKX+T3XI10/HfGdqRTbgeP7ID60FCzcwu6kXWqJn5tzabjDKLTlFv8mJmn0aa/ppkIU97nfA==", + "dependencies": { + "@react-pdf-viewer/core": "3.12.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", + "dependencies": { + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", + "dependencies": { + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", + "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" + }, + "node_modules/@types/dompurify": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", + "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", + "dev": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", + "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "peer": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/mustache": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", + "integrity": "sha512-PLwiVvTBg59tGFL/8VpcGvqOu3L4OuveNvPi0EYbWchRdEVP++yRUXJPFl+CApKEq13017/4Nf7aQ5lTtHUNsA==" + }, + "node_modules/@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/papaparse": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz", + "integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" + }, + "node_modules/@types/react": { + "version": "18.2.64", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", + "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.21", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", + "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" + }, + "node_modules/@types/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true + }, + "node_modules/@types/webrtc": { + "version": "0.0.37", + "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", + "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "peer": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "peer": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "dependencies": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "peer": true + }, + "node_modules/bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "peer": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==" + }, + "node_modules/dompurify": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.9.tgz", + "integrity": "sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ==" + }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "dependencies": { + "underscore": "^1.13.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.701", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", + "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "peer": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "peer": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lop": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.1.tgz", + "integrity": "sha512-9xyho9why2A2tzm5aIcMWKvzqKsnxrf9B5I+8O30olh6lQU8PH978LqZoI4++37RBgS1Em5i54v1TFs/3wnmXQ==", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-cancellable-promise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-event-props": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, + "node_modules/mammoth": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", + "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.1", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/merge-class-names": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", + "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" + } + }, + "node_modules/merge-refs": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", + "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "peer": true + }, + "node_modules/microsoft-cognitiveservices-speech-sdk": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", + "integrity": "sha512-wPxuEXgjLdqMMIrdBtl8jquGahLV19LQE0ie8MI/PcBcNLG5buVzwS2rQEyHMsRGx+C/4OdBo1ROdNIUzCm4Lg==", + "dependencies": { + "@types/webrtc": "^0.0.37", + "agent-base": "^6.0.1", + "bent": "^7.3.12", + "https-proxy-agent": "^4.0.0", + "uuid": "^9.0.0", + "ws": "^7.5.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/nan": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "peer": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path2d-polyfill": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", + "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "3.11.174", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", + "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d-polyfill": "^2.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-doc-viewer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", + "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", + "dependencies": { + "pdfjs-dist": "2.4.456", + "react-pdf": "5.0.0", + "styled-components": "^5.1.1", + "wl-msg-reader": "^0.2.0" + } + }, + "node_modules/react-doc-viewer/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { + "version": "2.4.456", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", + "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" + }, + "node_modules/react-doc-viewer/node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-doc-viewer/node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-doc-viewer/node_modules/react-pdf": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", + "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "make-cancellable-promise": "^1.0.0", + "make-event-props": "^1.1.0", + "merge-class-names": "^1.1.1", + "pdfjs-dist": "2.4.456", + "prop-types": "^15.6.2", + "worker-loader": "^3.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "react": "^16.3.0", + "react-dom": "^16.3.0" + } + }, + "node_modules/react-doc-viewer/node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/react-doc-viewer/node_modules/styled-components": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", + "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "peer": true + }, + "node_modules/react-pdf": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", + "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", + "dependencies": { + "clsx": "^2.0.0", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.2.1", + "pdfjs-dist": "3.11.174", + "prop-types": "^15.6.2", + "tiny-invariant": "^1.0.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", + "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==", + "dependencies": { + "@remix-run/router": "1.15.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.22.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz", + "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==", + "dependencies": { + "@remix-run/router": "1.15.3", + "react-router": "6.22.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-components": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", + "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "dependencies": { + "@emotion/is-prop-valid": "1.2.1", + "@emotion/unitless": "0.8.0", + "@types/stylis": "4.2.0", + "css-to-react-native": "3.2.0", + "csstype": "3.1.2", + "postcss": "8.4.31", + "shallowequal": "1.1.0", + "stylis": "4.3.1", + "tslib": "2.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universal-cookie": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", + "integrity": "sha512-lbRVHoOMtItjWbM7TwDLdl8wug7izB0tq3/YVKhT/ahB4VDvWMyvnADfnJI8y6fSvsjh51Ix7lTGC6Tn4rMPhw==", + "dependencies": { + "@types/cookie": "^0.3.3", + "cookie": "^0.4.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/webpack": { + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wl-msg-reader": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", + "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" + }, + "node_modules/worker-loader": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", + "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "optional": true + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } - } - }, - "node_modules/xmlbuilder": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", - "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" } - } } diff --git a/frontend/package.json b/frontend/package.json index e2758f12..f7cb880b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,37 +1,37 @@ { - "name": "frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "watch": "tsc && vite build --watch" - }, - "dependencies": { - "@cyntler/react-doc-viewer": "^1.14.1", - "@fluentui/react": "^8.105.3", - "@fluentui/react-icons": "^2.0.195", - "@pdftron/webviewer": "^10.7.2", - "@react-pdf-viewer/core": "^3.12.0", - "@react-pdf-viewer/default-layout": "^3.12.0", - "@react-spring/web": "^9.7.1", - "dompurify": "^3.0.1", - "mammoth": "^1.7.0", - "microsoft-cognitiveservices-speech-sdk": "^1.27.0", - "react": "^18.2.0", - "react-doc-viewer": "^0.1.5", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" - }, - "devDependencies": { - "@types/dompurify": "^2.4.0", - "@types/react": "^18.0.27", - "@types/react-dom": "^18.0.10", - "@vitejs/plugin-react": "^3.1.0", - "prettier": "^2.8.3", - "typescript": "^4.9.3", - "vite": "^4.1.0" - } + "name": "frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "watch": "tsc && vite build --watch" + }, + "dependencies": { + "@cyntler/react-doc-viewer": "^1.14.1", + "@fluentui/react": "^8.105.3", + "@fluentui/react-icons": "^2.0.195", + "@pdftron/webviewer": "^10.7.2", + "@react-pdf-viewer/core": "^3.12.0", + "@react-pdf-viewer/default-layout": "^3.12.0", + "@react-spring/web": "^9.7.1", + "dompurify": "^3.0.1", + "mammoth": "^1.7.0", + "microsoft-cognitiveservices-speech-sdk": "^1.27.0", + "react": "^18.2.0", + "react-doc-viewer": "^0.1.5", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "universal-cookie": "^4.0.4" + }, + "devDependencies": { + "@types/dompurify": "^2.4.0", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "prettier": "^2.8.3", + "typescript": "^4.9.3", + "vite": "^4.1.0" + } } From 8e19df84caf75e224fa5c9863813ed767fb7ac71 Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Mon, 25 Mar 2024 21:53:17 -0400 Subject: [PATCH 005/820] SFC-49 history pannel and button working (#4) * SFC-49 history pannel and button working * SFC-49 file changed * SFC-49 comments resolved and description in the chat about the assistant added * SFC-49 button close inside pannel modified --- .../ChatHistoryButton.module.css | 33 +++ .../ChatHistoryButton/ChatHistoryButton.tsx | 22 ++ .../HistoryPannel/ChatHistoryPanel.tsx | 32 +++ .../ChatHistoryPannel.module.css | 55 ++++ .../QuestionInput/QuestionInput.module.css | 1 + frontend/src/index.tsx | 19 +- frontend/src/pages/chat/Chat.module.css | 19 +- frontend/src/pages/chat/Chat.tsx | 236 ++++++++++-------- frontend/src/pages/layout/Layout.module.css | 7 +- frontend/src/pages/layout/Layout.tsx | 11 +- frontend/src/providers/AppProviders.tsx | 25 ++ 11 files changed, 335 insertions(+), 125 deletions(-) create mode 100644 frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css create mode 100644 frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx create mode 100644 frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx create mode 100644 frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css create mode 100644 frontend/src/providers/AppProviders.tsx diff --git a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css new file mode 100644 index 00000000..fe1dab18 --- /dev/null +++ b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css @@ -0,0 +1,33 @@ +.container { + display: flex; + align-items: center; + margin-top: 5px; + gap: 6px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + border-radius: 5px; + padding: 4px 12px; + transition: .2s; + background-color: white; +} + +.container:hover{ + background-color: rgb(243, 243, 243); +} + +.container:active{ + background-color: white; +} + +.button{ + color: rgb(63, 63, 63); + font-size: 24px; +} + +.buttonText{ + font-weight: 500; +} + +.disabled { + opacity: 0.4; +} diff --git a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx new file mode 100644 index 00000000..94d53410 --- /dev/null +++ b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx @@ -0,0 +1,22 @@ +import { Text } from "@fluentui/react"; +import { HistoryFilled } from "@fluentui/react-icons"; + +import styles from "./ChatHistoryButton.module.css"; +import { useAppContext } from "../../providers/AppProviders"; + +interface Props { + className?: string; + onClick: () => void; + disabled?: boolean; +} + +export const ChatHistoryButton = ({ className, disabled, onClick }: Props) => { + const {showHistoryPanel} = useAppContext() + const buttonContent = showHistoryPanel ? "Hide chat history" : 'Show chat history' + return ( + + ); +}; diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx new file mode 100644 index 00000000..0b800b97 --- /dev/null +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -0,0 +1,32 @@ +import { AddFilled } from "@fluentui/react-icons" + +import styles from "./ChatHistoryPannel.module.css"; +import { useAppContext } from "../../providers/AppProviders"; + +export const ChatHistoryPanel = () => { + const { showHistoryPanel, setShowHistoryPanel} = useAppContext() + + const handleClosePannel = () => { + setShowHistoryPanel(!showHistoryPanel) + } + return ( +
+
+
Chat history
+
+
+ +
+
+ +
+
+
+
+

Conversations

+
+
+
+ + ) +} \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css new file mode 100644 index 00000000..a2f400ac --- /dev/null +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -0,0 +1,55 @@ +.clearButton{ + border: none; + font-size: 25px; + color: #5da3cc; + background-color: transparent; + cursor: pointer; + padding: 10.2px 7.5px; + margin-top: -15px; +} + +.closeButton{ + border: none; + font-size: 25px; + color: #5da3cc; + background-color: transparent; + cursor: pointer; + transform: rotate(45deg); + margin-bottom: -5px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +.container { + margin-right: 10px; + margin-top: 5px; + width: 250px; + padding: 0 10px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.title { + font-weight: 600; + font-size: 18px; +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} + +.content { + display: flex; + flex-direction: column; + max-width: 100%; +} \ No newline at end of file diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index 4025562e..2fc672a8 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -5,6 +5,7 @@ width: 100%; padding: 15px; background: white; + border-bottom: 5px solid #a2e700; } .questionInputTextArea { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 3f551c48..f68d468f 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,19 +8,22 @@ import "./index.css"; import Layout from "./pages/layout/Layout"; import NoPage from "./pages/NoPage"; import Chat from "./pages/chat/Chat"; +import { AppProvider } from "./providers/AppProviders"; initializeIcons(); export default function App() { return ( - - - }> - } /> - } /> - - - + + + + }> + } /> + } /> + + + + ); } diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 91c9818a..378641af 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -1,13 +1,23 @@ .container { flex: 1; + background-color: white; + border: 1px solid rgb(231, 231, 231); + border-radius: 10px; + margin: 0px 10px; + margin-bottom: 30px; + box-shadow: 0px 5px 4px -2px rgba(0, 0, 0, 0.2); +} + +.mainContainer{ display: flex; - flex-direction: column; - margin-top: 20px; + flex-direction: row-reverse; + height: 100%; } .chatRoot { flex: 1; display: flex; + height: 100%; } .chatContainer { @@ -59,6 +69,7 @@ overflow-y: auto; padding-left: 24px; padding-right: 24px; + padding-top: 15px; display: flex; flex-direction: column; } @@ -85,7 +96,7 @@ padding-right: 24px; width: 100%; max-width: 1028px; - background: #f2f2f2; + background: #ffffff; } .chatAnalysisPanel { @@ -106,7 +117,7 @@ .commandsContainer { display: flex; - align-self: flex-end; + align-self: flex-start; } .commandButton { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 609693d7..df5f1166 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -14,6 +14,10 @@ import { ClearChatButton } from "../../components/ClearChatButton"; import { getTokenOrRefresh } from "../../components/QuestionInput/token_util"; import { SpeechConfig, AudioConfig, SpeechSynthesizer, ResultReason } from "microsoft-cognitiveservices-speech-sdk"; import { getFileType } from "../../utils/functions"; +import salesLogo from "../../img/logo.png"; +import { useAppContext } from "../../providers/AppProviders"; +import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; + const userLanguage = navigator.language; let error_message_text = ""; @@ -38,6 +42,9 @@ const Chat = () => { const [excludeCategory, setExcludeCategory] = useState(""); const [useSuggestFollowupQuestions, setUseSuggestFollowupQuestions] = useState(false); + const {showHistoryPanel} = useAppContext() + + const lastQuestionRef = useRef(""); const chatMessageStreamEnd = useRef(null); const [fileType, setFileType] = useState(""); @@ -245,121 +252,134 @@ const Chat = () => { }; return ( -
-
- +
+
+
+ {showHistoryPanel && ( + + )} +
-
-
- {!lastQuestionRef.current ? ( -
- {/*
- ) : ( -
- {answers.map((answer, index) => ( -
- -
- onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q)} - showFollowupQuestions={false} - showSources={true} - /> -
+
+
+
+ {!lastQuestionRef.current ? ( +
+
+ +

Clew

+

Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver actionable insights.

- ))} - {isLoading && ( - <> - -
- -
- - )} - {error ? ( - <> - -
- makeApiRequestGpt(lastQuestionRef.current)} /> +
+ ) : ( +
+ {answers.map((answer, index) => ( +
+ +
+ onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q)} + showFollowupQuestions={false} + showSources={true} + /> +
- - ) : null} -
+ ))} + {isLoading && ( + <> + +
+ +
+ + )} + {error ? ( + <> + +
+ makeApiRequestGpt(lastQuestionRef.current)} /> +
+ + ) : null} +
+
+ )} + +
+ makeApiRequestGpt(question)} />
+
+ + {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( + onToggleTab(x, selectedAnswer)} + citationHeight="810px" + answer={answers[selectedAnswer][1]} + activeTab={activeAnalysisPanelTab} + fileType={fileType} + /> )} -
- makeApiRequestGpt(question)} /> -
+ setIsConfigPanelOpen(false)} + closeButtonAriaLabel="Close" + onRenderFooterContent={() => setIsConfigPanelOpen(false)}>Close} + isFooterAtBottom={true} + > + + + + + + + +
- - {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( - onToggleTab(x, selectedAnswer)} - citationHeight="810px" - answer={answers[selectedAnswer][1]} - activeTab={activeAnalysisPanelTab} - fileType={fileType} - /> - )} - - setIsConfigPanelOpen(false)} - closeButtonAriaLabel="Close" - onRenderFooterContent={() => setIsConfigPanelOpen(false)}>Close} - isFooterAtBottom={true} - > - - - - - - - -
); diff --git a/frontend/src/pages/layout/Layout.module.css b/frontend/src/pages/layout/Layout.module.css index cda5f9cf..86a4ad1d 100644 --- a/frontend/src/pages/layout/Layout.module.css +++ b/frontend/src/pages/layout/Layout.module.css @@ -5,16 +5,15 @@ } .header { - background-color: #FFFFFF; + background-color: #f1f1f1; color: #f2f2f2; } .headerContainer { display: flex; align-items: center; - justify-content: space-around; - margin-right: 12px; - margin-left: 12px; + justify-content: space-between; + margin: 10px 12px; } .headerTitleContainer { diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 9b609604..508adfa9 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -4,8 +4,17 @@ import salesLogo from "../../img/logo.png"; import github from "../../assets/github.svg"; import styles from "./Layout.module.css"; +import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; +import { useAppContext } from "../../providers/AppProviders"; const Layout = () => { + + const {showHistoryPanel, setShowHistoryPanel} = useAppContext() + + const handleShowHistoryPanel = () => { + setShowHistoryPanel(!showHistoryPanel) + } + return (
@@ -42,7 +51,7 @@ const Layout = () => { */} -

Clew

+
diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx new file mode 100644 index 00000000..57cc4980 --- /dev/null +++ b/frontend/src/providers/AppProviders.tsx @@ -0,0 +1,25 @@ +import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; +interface AppContextType { + showHistoryPanel: boolean; + setShowHistoryPanel: Dispatch>; +} + +const AppContext = createContext(undefined); + +export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [showHistoryPanel, setShowHistoryPanel] = useState(false); + + return ( + + {children} + + ); +}; + +export const useAppContext = (): AppContextType => { + const context = useContext(AppContext); + if (!context) { + throw new Error("useAppContext must be used inside AppProvider"); + } + return context; +}; From e50c9c650acf63bb1ebf7166d5f7e3506ea19632 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:35:04 -0400 Subject: [PATCH 006/820] SFC- 39 Settings UI and connection (#6) --- .gitignore | 4 +- backend/app.py | 79 ++++++++++ frontend/src/api/api.ts | 74 +++++++++- frontend/src/api/models.ts | 16 ++ .../SettingsModal/SettingsModal.module.css | 74 ++++++++++ .../src/components/SettingsModal/index.tsx | 139 ++++++++++++++++++ frontend/src/pages/layout/Layout.tsx | 8 +- 7 files changed, 390 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/SettingsModal/SettingsModal.module.css create mode 100644 frontend/src/components/SettingsModal/index.tsx diff --git a/.gitignore b/.gitignore index 5187a348..b9ea4e13 100644 --- a/.gitignore +++ b/.gitignore @@ -150,4 +150,6 @@ cython_debug/ # NPM npm-debug.log* node_modules -static/ \ No newline at end of file +static/ + +settings.json \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index 89dbe758..24c33593 100644 --- a/backend/app.py +++ b/backend/app.py @@ -16,6 +16,7 @@ SPEECH_REGION = os.getenv('SPEECH_REGION') ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') +SETTINGS_ENDPOINT = os.getenv('SETTINGS_ENDPOINT') ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI') STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper() @@ -130,5 +131,83 @@ def getBlob(): logging.exception(blob_name) return jsonify({"error": str(e)}), 500 +@app.route("/api/settings", methods=["GET"]) +def getSettings(): + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + + if not client_principal_id or not client_principal_name: + return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = 'orchestrator-host--functionKey' + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") + return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + + try: + url = SETTINGS_ENDPOINT + payload = json.dumps({ + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name + }) + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + response = requests.request("GET", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return(response.text) + except Exception as e: + logging.exception("[webbackend] exception in /api/settings") + return jsonify({"error": str(e)}), 500 + +@app.route("/api/settings", methods=["POST"]) +def setSettings(): + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + + if not client_principal_id or not client_principal_name: + return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + + temperature = request.json["temperature"] + presence_penalty = request.json["presence_penalty"] + frequency_penalty = request.json["frequency_penalty"] + + if not temperature or not presence_penalty or not frequency_penalty: + return jsonify({"error": "Missing required parameters, temperature, presence_penalty or frequency_penalty"}), 400 + + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = 'orchestrator-host--functionKey' + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") + return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + + try: + url = SETTINGS_ENDPOINT + payload = json.dumps({ + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + "temperature": temperature, + "presence_penalty": presence_penalty, + "frequency_penalty": frequency_penalty + }) + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + response = requests.request("POST", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return(response.text) + except Exception as e: + logging.exception("[webbackend] exception in /api/settings") + return jsonify({"error": str(e)}), 500 + if __name__ == "__main__": app.run(host='0.0.0.0', port=8000) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 7d666853..19af781a 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,9 +1,79 @@ -import { AskRequest, AskResponse, AskResponseGpt, ChatRequest, ChatRequestGpt } from "./models"; +import { + AskRequest, + AskResponse, + AskResponseGpt, + ChatRequest, + ChatRequestGpt, + GetSettingsProps, + PostSettingsProps +} from "./models"; +const loadModule = async (modulePath : string) => { + try { + return (await import(/* @vite-ignore */modulePath))?.default; + } catch (e) { + console.log("Error loading " + modulePath, e); + } +} + +export async function getSettings({ user }: GetSettingsProps): Promise { + const settings = await loadModule("./settings.json"); + const baseUrl = settings?.ENVIRONMENT === "development" ? settings?.LOCAL_API : ""; + + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; + const url = baseUrl + "/api/settings"; + try { + const response = await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name + } + }); + const fetchedData = await response.json(); + return fetchedData; + } catch (error) { + console.error("Error fetching settings", error); + return {}; + } +} +export async function postSettings({ user, temperature, presence_penalty, frequency_penalty } : PostSettingsProps): Promise { + const settings = await loadModule("./settings.json"); + const baseUrl = settings?.ENVIRONMENT === "development" ? settings?.LOCAL_API : ""; + + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; + const url = baseUrl + "/api/settings"; + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name + }, + body: JSON.stringify({ + temperature, + presence_penalty, + frequency_penalty + }) + }); + const fetchedData = await response.json(); + console.log("Settings posted", fetchedData); + return fetchedData; + } catch (error) { + console.error("Error posting settings", error); + return {}; + } +} export async function chatApiGpt(options: ChatRequestGpt): Promise { - const response = await fetch("/chatgpt", { + const settings = await loadModule("./settings.json"); + const baseUrl = settings?.ENVIRONMENT === "development" ? settings?.LOCAL_API : ""; + const response = await fetch(baseUrl + "/chatgpt", { method: "POST", headers: { "Content-Type": "application/json" diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 74b0a639..fc36b441 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -64,3 +64,19 @@ export type ChatRequestGpt = { overrides?: AskRequestOverrides; }; +export type GetSettingsProps = { + user: { + id: string; + name: string; + } | null; +} + +export type PostSettingsProps = { + user: { + id: string; + name: string; + } | null; + temperature: number; + presence_penalty: number; + frequency_penalty: number; +} diff --git a/frontend/src/components/SettingsModal/SettingsModal.module.css b/frontend/src/components/SettingsModal/SettingsModal.module.css new file mode 100644 index 00000000..d0fe3efd --- /dev/null +++ b/frontend/src/components/SettingsModal/SettingsModal.module.css @@ -0,0 +1,74 @@ +.button { + color: rgb(63, 63, 63); + font-size: 24px; + border: none; + width: 100%; + display: flex; +} + +.buttonText { + font-weight: 500; + padding-left: 10px +} + +.disabled { + opacity: 0.4 +} + +.answerContainer { + padding: 40px; + background: rgb(249, 249, 249); + border-radius: 16px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + outline: transparent solid 1px; + width: 300px; +} + +.saveButton { + background: rgb(0, 0, 0); + color: white; + border-radius: 8px; + padding: 10px; + font-size: 20px; + width: 100%; + border: none; + cursor: pointer; + margin-top: 20px; +} + +.saveButton:hover { + background: rgb(0, 0, 0, 0.8); + color: white; +} + +.closeButton { + background: rgb(249, 249, 249); + color: rgb(0, 0, 0); + font-size: 20px; + border: none; + cursor: pointer; + position: absolute; + top: 1rem; + right: 0; +} + +.closeButton:hover { + background: rgb(249, 249, 249); + color: rgb(0, 0, 0, 0.8); +} + +.closeButton:focus { + outline: none; +} + +.header { + padding: 10px; + display: flex; +} + +.modal-title { + font-size: 24px; + font-weight: 500; + padding-left: 10px; + color: red; +} \ No newline at end of file diff --git a/frontend/src/components/SettingsModal/index.tsx b/frontend/src/components/SettingsModal/index.tsx new file mode 100644 index 00000000..76819493 --- /dev/null +++ b/frontend/src/components/SettingsModal/index.tsx @@ -0,0 +1,139 @@ +import React, { useState, useEffect } from "react"; +import { SettingsFilled, SaveFilled } from "@fluentui/react-icons"; +import styles from "./SettingsModal.module.css"; +import { getSettings, postSettings } from "../../api/api"; + +import { DefaultButton, Modal, Stack, Text, TextField, Spinner } from "@fluentui/react"; + +interface Props { + user: { + id: string; + name: string; + } | null; +} + +const SettingsModal = ({ user }: Props) => { + const [isModalOpen, setIsModalOpen] = useState(true); + const [temperature, setTemperature] = useState(""); + const [presencePenalty, setPresencePenalty] = useState(""); + const [frequencyPenalty, setFrequencyPenalty] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + getSettings({ user }).then(data => { + setTemperature(data.temperature); + setPresencePenalty(data.presencePenalty); + setFrequencyPenalty(data.frequencyPenalty); + setLoading(false); + }); + }; + + setLoading(true); + fetchData(); + }, [user]); + + const showModal = () => { + setIsModalOpen(true); + }; + + const hideModal = () => { + setIsModalOpen(false); + }; + + const handleSubmit = () => { + if (!temperature || !presencePenalty || !frequencyPenalty) { + return; + } + + const parsedTemperature = parseFloat(temperature); + const parsedPresencePenalty = parseFloat(presencePenalty); + const parsedFrequencyPenalty = parseFloat(frequencyPenalty); + + postSettings({ + user, + temperature: parsedTemperature, + presence_penalty: parsedPresencePenalty, + frequency_penalty: parsedFrequencyPenalty + }); + + hideModal(); + }; + + const validateValue = (val: any, func: any) => { + if (val.match(/^[0]+$/)) { + func("0"); + return false; + } + + if (val.match(/[^0-9.]/) || val < 0 || val > 1 || val.split(".").length > 2 || val === "1." || val.length > 4) { + return false; + } + + return true; + }; + + const handleSetTemperature = (val: any) => { + if (validateValue(val.target.value, setTemperature)) { + setTemperature(val.target.value); + } + }; + + const handleSetPresencePenalty = (val: any) => { + if (validateValue(val.target.value, setPresencePenalty)) { + setPresencePenalty(val.target.value); + } + }; + + const handleSetFrequencyPenalty = (val: any) => { + if (validateValue(val.target.value, setFrequencyPenalty)) { + setFrequencyPenalty(val.target.value); + } + }; + + return ( +
+ + + Settings + + + + + +
+

Adjust your settings

+ + ✖ + +
+ {loading ? ( +
+ +

Loading your settings

+
+ ) : ( +
+ handleSetTemperature(e)} /> + handleSetFrequencyPenalty(e)} /> + handleSetPresencePenalty(e)} /> + + +   Save + +
+ )} +
+
+
+
+ ); +}; + +export default SettingsModal; diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 508adfa9..15f11614 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -7,6 +7,8 @@ import styles from "./Layout.module.css"; import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; import { useAppContext } from "../../providers/AppProviders"; +import SettingsModal from "../../components/SettingsModal"; + const Layout = () => { const {showHistoryPanel, setShowHistoryPanel} = useAppContext() @@ -51,7 +53,11 @@ const Layout = () => { */} - +
+ {/* needs an user to be sent ↓ */} + + +
From 0901a747ed3ce62e041cb6ad75f5343070d51dc7 Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Mon, 1 Apr 2024 11:27:21 -0400 Subject: [PATCH 007/820] SFC 50 populate conversation list (#5) * add local enpoint to call orchestrator * fix URL add HISTORY ENV endpoint * SFC-50 list of history conversation and favicon done * add client id to the request * conflict resolved --------- Co-authored-by: Alejandro Lopez --- .env.template | 3 + backend/app.py | 28 ++++- frontend/public/favicon.ico | Bin 4286 -> 15406 bytes frontend/src/api/api.ts | 28 ++++- frontend/src/api/models.ts | 6 + frontend/src/assets/pencil.png | Bin 0 -> 1082 bytes frontend/src/assets/trash.png | Bin 0 -> 734 bytes .../HistoryPannel/ChatHistoryListItem.tsx | 110 ++++++++++++++++++ .../HistoryPannel/ChatHistoryPanel.tsx | 30 ++--- .../ChatHistoryPannel.module.css | 69 +++++++++++ frontend/src/pages/chat/Chat.module.css | 27 +++++ frontend/src/pages/layout/Layout.tsx | 1 + frontend/src/providers/AppProviders.tsx | 7 +- 13 files changed, 286 insertions(+), 23 deletions(-) create mode 100644 frontend/src/assets/pencil.png create mode 100644 frontend/src/assets/trash.png create mode 100644 frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx diff --git a/.env.template b/.env.template index a8fb5735..19e037f7 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,8 @@ # orchestrator's endpoint: use http://localhost:7071/api/orc when running everything locally. ORCHESTRATOR_ENDPOINT="http://localhost:7071/api/orc" +# conversation history endpoint: use http://localhost:7071/api/conversations when running everything locally. +HISTORY_ENDPOINT="http://localhost:7071/api/conversations" + AZURE_KEY_VAULT_NAME="key_vault_name" # Speech sythesis and recognition diff --git a/backend/app.py b/backend/app.py index 24c33593..ff05977c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -16,6 +16,7 @@ SPEECH_REGION = os.getenv('SPEECH_REGION') ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') +HISTORY_ENDPOINT = os.getenv('HISTORY_ENDPOINT') SETTINGS_ENDPOINT = os.getenv('SETTINGS_ENDPOINT') ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI') STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') @@ -84,7 +85,32 @@ def chatgpt(): logging.exception("[webbackend] exception in /chatgpt") return jsonify({"error": str(e)}), 500 - +@app.route("/api/get-chat-history", methods=["GET"]) +def getChatHistory(): + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = 'orchestrator-host--functionKey' + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") + return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + try: + url = HISTORY_ENDPOINT + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + payload = json.dumps({ + "user_id": client_principal_id + }) + response = requests.request("GET",url,headers=headers,data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return(response.text) + except Exception as e: + logging.exception("[webbackend] exception in /get-chat-history") + return jsonify({"error": str(e)}), 500 # methods to provide access to speech services and blob storage account blobs @app.route("/api/get-speech-token", methods=["GET"]) diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index f1fe50511ca0c33d95783506d4af99426dfc10bf..94cc94600468f1bd5fd20b41d5e460be8f139944 100644 GIT binary patch literal 15406 zcmeI235-?c702JGym$Bg-M0^Kh8bo+0y9o!P^_UlU_=n4zw_X>FUBfXj4`=4`Eu|5mh=73 z|D5l;-#h0e5;cka6ONPMUQV2IKq4_Pkw}!wyWgALMB)zWT3YtB_f901q!Wo|`Y?nM zyX(2d*O$ORI-NFr+y1s;7^7_4?q!;0lWiL(Te^NK<#gLHPO>cRm{8Xn?73C4fF-Pz`)YP>HEJK@Z>grs}(C;eJPuH~DZBw7<+V*%uQ&;F} zYNcfuQ;>I?W$M4wRpl;CQMx?OJk-#%<(}hQ=vdZR%1xBHx~4AYJ`0(Pe9Jo6)U@w; zo^>=>2C1p!tD2H}+OzFTXd7r6+WpKiOUBUe)=VbTkN3sW&YX8cVNErS9)b)->C{wZ zo$I=_hM`TRu4J0p2*)%>2tQz&>RQ%mj-^ki9$Wp)m_w`IB#YqBzbD!EPO*EQuC&P3zF&>Jc^Y}2@p^UM~?Hs&AZTE;0n zch}a|a!TGkKYH(??gQ60CJh-fgp+fne!6MskAP<+b7YzS9m;xewi&wi1pHj*qdRiy zv-Dy7_WDxk8=TetPTO?HaV~(Qv^+lP3ahLJ@Pi((147@9O zzrI(mUM2Xv9}X_&{J9kUmWDwvP3}^_{4xN{4XDdLvr-?3sl=~KvNzg5iTMhBjI8~(XH zT0j2_gV+hVJNDq%iErI3Ye)K9x^}0oaRzpr$>L+o{W!WUp1%H^^G7uPFde@$H-jloYc=`Sb#3OBOO z#*7)GSJz8hM@NSuZL%(h4b{2llrAm(fr3{4(UmGGhi}24V(c24V&x z1|&{Gh&ItdiQ^=Y5MMC^F#|CJF#|CJF#|CJF#|CJ`QdQ)kk&m9>w0J zE*j5cB_7|<(2$Z6y??Bayhhi45@(b6SX5CxSM@XP6^wF^`bPIYefs2l-#MRH<<(Lo zuV3%Fl~_Nrla6CuNSyd$FjaB`#Fx*J_<0ZnCUL|b#HCM=af4f1^MT`xATE59rD->l z%O5HDL>Je!PC?F9f=T8j##kgicqwt@*^K?M#5{=whty3Zzkgn-RO-d^<$>p&C39qb z|6uw}AwJ$7;gl=QYZP6H$~@6+Pjc=?t(q} zgoB!!n|*K&mfS-HzY2aO^}gW8UL#Z`xe+@p96WfiNt}GT=mOWB+`B{`_w7d z)DU~W4E)>4tz61`#fA}n`tL&aI_z~rU0uG3d_kkcx2y5P#TGdIGqK>`_9Ny`4rV_2 zg4MY&=mhhtg&^q7_$T3FIewLwIU`^BIDYa$qrtYB+`x@82e|`yPwvET=aDm73zjaP zuU3=Ejl`{&H8s^WabGDovEyT57-nQ#HGXMB#)sJPNn*ywM05X#e|XO2!g0jlPa;oN zM_hb7{Uryqi8%dSiSc``eJk(X*lrSek+H0iYkK$Y-AoQ)ti;g4wXIw#jVk1F<9L1( zy;p&KH1>X!aho$~|0md`3#_kz;S!!VklT4Ko%XMw{R7{z=i|)(%BH+j$U0wi#NHadvJ`)RH!9@X#xrjpWIaRM8?4RI(pP+4@;u1fBzUmJ zli2ctXk6uv->@}Yfo{{7=PmkeVa;r0ZQk2hF1KqbWXfFB(Y`ht23Hz}zL2%~EF5L= zxw*EXKg}9>-uJwR^ZERF!X@nwiho5qSf;sHFyX%n_L}Iq?mbe--(22ME)T&+j)$X> zl8@?%F0%f`-^qQP=DYT%mbT-3&$*p`B6C## z{{3~yerShTA4p8;Mlq1Ot@JEzl5D|7vZ zachwIKKs^v(S8w)`>Z<{+mP`vvUf&0$UOMLE3!W2n<+91>2wQre~o=$4eMYGHfope zJ2Z(<_OmR4B0Pao$#)+myP4>F)11gM+cg^J|iml$`y=n;2U-x-@x1Px7pb8yHWrD z^IpT=?c-AuglEyA8o$Vt?=k#p)n`RHcM9%m-<5W$i*Q7Jq%JD7Rldz4PtJGp9`*mC zcl>1lTrF}Pd(jt_^;L~$f6MdTJn^fVe_za7{rhBJ$ctsf48#n?48#n?48#n?48#n? P48#n?48#n4JsJ2n-pT47 literal 4286 zcmeH}dr(y88OA?KWOvysX{UdrX?5Dk2n0}AK@g0WnRH4JL8DO=m3RY_c8nwq(U1w# zblPb%a#2_oP(cAfxywyJkeD=zq)sBaX_L6ZDzLE2?us@Vd!D}Eo;|v0z+{sC8JwT* z@$8=Sd*0_P<1*$&e`d{M{BKsRW9;XQF@J(ivBd;G&wsM$wa4g1&W}I$8|c~T?c4qQ zLmRqMWkp?SJ~>@Ge6l;!e6l+@sXjS`p?#Yy_Wl*$JQ~=;HeYy=6c%Kf!Ux=ZJCigSyTk z@tp5xu(*$Rv#qzA zCYq+a+3TcxoJ^Ce$lKy}3f$YeO^%kWGB1yf?-Yd+b-GCy?h&Her946SKixNPvpa9IcjOKAV%$L~XVuJSBK@)riCNcWK@xgMq+ z?}$zr@0#{ippo;eLLg9`>`T4KB0qhVZRCBs{N(Qa>}`v?~M8s1&(j@Mdf-W`qI5^mopg#Jp315 zrrozt2Y-DO)|zmb%gA3A1T*Cra^>Jzf&`w0I%)NEdLGlRDcsjUKJFw0K9AAfeD`xN zc;nKm;@*6B4)6ZGUF1*o!oNxaVX3D5j|3ATFdwEIyo<6R!CXoiTv>usJe{7$4JoRJ z$#=UhMUL7{3ZZtRLg4ON+Py#hC%E!D!>4-~=-G?ilb?6rvz_>(29~;T3?H3?>y;t6 zULJyL1fS-^Ap-B@T3PTNnp1W!<*47}+fcJnff{o1&MT6XXrtdpzBljugO|N9kiowE z{53XyDU&%bq%&tj5*vFzd)6&`bEN36;u(mFIk?7g=OBNhSU$XXRg;InLv3i%1j#nLsCrxz|BZ_s13hTNRv zp%^(9>dG=)J(sfZ40HZ%t_add*F0z*U9W85pY=-eaw<4UDxq8iG;v_1tWvs%t#w|7 zr9KRn`cRD4k((H;B`C`<$Ub7={&qviNoaN@Xk4<&v4WzOz?$qwRZ0H z`mkU}X9UKY^%y%rG*X5}J;5`GolqZkYjfz7|94H7rEIObvTTj=p7W6C6%MUd3kS(x zuv~?`@#;czU8vD{DgxuJ^Dx%p&OkXVt}HaEld-1I^65O&oiAOZP2{^CTCD>29b8SU zqUXL!jRSO?{X1X0nWzzloAv*>(KBBdZwn{p2|UL*&*9EuY0|^mtWTKEGu?Ue`;gP= z@~zU4Q-flf#pEw}T7%aX_~6Lze}UU)qv1FeAy`j_b6{-`2W6o?!o>_NdWWqwQa+vM zLFbE~QJ0rIO`kRL6?u@KphiZN9B1t95$Z8m4lv1`#f0NBJTa3 zJjgZGYsOWYr;1jpVO*&};R+GsN-h5Oq!NFL_roYTZ@ses6Q|~jbWx5@^7A|d&tYqe z_{MjZsa#W?GbHL}7*=SkE_b3zP0 zKv{%|Zt9>r>TBPZzN^#oeFX`+xcvz_xDno1qQalz{9x^k!kzbG;CMRm>}=nfhTa|d<;SUz6D>;@op6L5%Yt)m;AZ0 zx*30u^)v0%D^Z*I8{o4gxb@E%O!m8>9Fr1#)XCZC&+Wa7{x9828UDOf8=JXQXZWm1 zpNq?jvnTtbvv2mtWZyjRp7ig@^U=At`lIu2o{N4`y6(sM;0ECS$(q^gm8H!hm{*pD zdG)H8cQ0e!CdOnEcE+?W=v`lWWzBTK*vz|(Es_uzdtCyI;T~CDfJCpiq{V*$i&S=H diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 19af781a..a81f2630 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,3 +1,4 @@ +import { List } from "@fluentui/react"; import { AskRequest, AskResponse, @@ -5,7 +6,8 @@ import { ChatRequest, ChatRequestGpt, GetSettingsProps, - PostSettingsProps + PostSettingsProps, + ConversationHistoryItem } from "./models"; const loadModule = async (modulePath : string) => { @@ -105,21 +107,35 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise { + const response = await fetch("/api/get-chat-history", { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + const parsedResponse: ConversationHistoryItem[] = await response.json(); + if (response.status > 299 || !response.ok) { + throw Error("Error getting chat history"); + } + return parsedResponse; +} + export function getCitationFilePath(citation: string): string { var storage_account = "please_check_if_storage_account_is_in_frontend_app_settings"; - + const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/get-storage-account", false); xhr.send(); if (xhr.status > 299) { console.log("Please check if STORAGE_ACCOUNT is in frontend app settings"); - return storage_account + return storage_account; } else { const parsedResponse = JSON.parse(xhr.responseText); - storage_account = parsedResponse['storageaccount']; + storage_account = parsedResponse["storageaccount"]; } - console.log('storage account:' + storage_account); + console.log("storage account:" + storage_account); return `https://${storage_account}.blob.core.windows.net/documents/${citation}`; -} \ No newline at end of file +} diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index fc36b441..862cf474 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -4,6 +4,12 @@ export const enum Approaches { ReadDecomposeAsk = "rda" } +export type ConversationHistoryItem = { + id: string, + start_date: string, + content: string, +}; + export type AskRequestOverrides = { semanticRanker?: boolean; semanticCaptions?: boolean; diff --git a/frontend/src/assets/pencil.png b/frontend/src/assets/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..a3648d8bdebd63f0ab3a77962d25126007998b46 GIT binary patch literal 1082 zcmV-A1jYM_P)=9((I-|(!@k6rBI2HZYn9XATgyEFIq^G z-DGAqv-5h`OpIwsljgmC!|(aNhi^V&hjTPLSzmHOX8|;W{FUWESLVnIfBEamPVn}= z8+9cobO{tsATf&E5yidIVNN~0-*oI z1pq!O9(H(1fV+`pY5Vq*_{!R%2s_Q`Zv<$jr@p%F6I5{g1WJUEhNY;o`+pyhl1axt zC2+-4+;iIs=rE_M!zCF94sYX(0wK!w4!pJLU&66hq5;x3p>`hfQ7pG>I3WW zw+IXfFzkm!m%Ldk!T~~_vNR+fT9J{NiE7UB7$8N}T|DUHo#u4wDt_2B;$41SRzyZX z@5wUVH*=}@!lnV;XYvuy9Rp|w+~R?r4_~u=Ttxawwwp$L3;-NWKLz5hfK?vKiN5qp zivSSV47Vb<2(s*GrMCX`D|V%p=>#AH0x^1x_{G%3ZR5ab~4k|RPY`3!JSX) zwj(0|gYeX3E)}yXJHM6`k@EsEOrvqc$92cf2~i&^*~xQ9_ZQY%_nqMQ=~=BDbcY0t z*qYul(7tc!Kb(Ub@vyTz1fV`xvRgLd_XQlv-VBw4#bE(zY)x;;rS=v#;AtN}etK4O z)#3$^wYH|W^cnHugW6Em<6+yn-~qM4lKoKpUhTq=P^#B&WYYlMv3fwXDnP*1wEY!) zx>baOmD}RiGO6FIUmPVxk1R8}84j#Ib{*zawIb?20y#!eo(JVUdU8S(_L6MRq+_E% zG@FoWM%qovq@P01*n10P*(W{;4Mbz%*i4 zZSD460zZf-p9@Pr08rDhoLWNRt80?F3R*M96ja@!r1Kmvk=KX*+JDqu5!0*S{V zJn1PbKl_i?qmByT84#ezI7B(wdn9&aC*a2U7XxhQx;5i54*&oF07*qoM6N<$g7fj} AD*ylh literal 0 HcmV?d00001 diff --git a/frontend/src/assets/trash.png b/frontend/src/assets/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..813612cec95730b130d14ea0d4644cfa8307963d GIT binary patch literal 734 zcmV<40wMj0P)X_`x#=E1?e z-+S--dGGh*cPI3)Vr4ataViRvz?o=yS6~?%OjoDJ|8%QQ^#Z8llu#xSUWu{;pu$;I z=EOKpfJXxXu#6&n^a^7STbEtWzoE+YUgZ+MOfC7NMtKWd>j~{{o2cIRiZ{PN%LJm7 zfC~c82T@m;#YhRbcN72@fv;6}?C~G~*Yn3wF2#QP8RUwUPj?0ZDCB^@9ROMfnNTCk zg8(9}_s0NY?W`K-HE>(3-aVD9k$1ocvGzg71h?uxb3K12_7@hn(lh1tk=QR*R`bQm zYCiU7%IhNwi(Bb~de3jVOa9YVP1DBM28JhAR#E`9Tx~u`1sjJQnz9;Gj{P7NY}9hK zd4Q-U4Z58SFwy`R`FTDQNuC3;hY6Amp99NAIb8mo&jP518XWndHULDm??Xnynlro2 z { + + const [hoveredItemIndex, setHoveredItemIndex] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const {dataHistory, setDataHistory} = useAppContext() + + + const handleMouseEnter = (index: string) => { + setHoveredItemIndex(index); + }; + + const handleMouseLeave = () => { + setHoveredItemIndex(null); + }; + + + const fetchData = async () => { + try { + const data = await getChatHistory(); + if(data.length > 0){ + setDataHistory(data) + setIsLoading(false); + }else{ + setIsLoading(false); + setErrorMessage("There are not conversation history yet.") + } + } catch (error) { + console.error('Error fetching data:', error); + setIsLoading(false); + setErrorMessage(`Was an error fetching data: ${error}`) + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const months = [ + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December" + ]; + + const sortedDataByMonth = dataHistory.sort((a, b) => { + const monthA = new Date(a.start_date).getMonth(); + const monthB = new Date(b.start_date).getMonth(); + return monthA - monthB; + }); + + const sortedDataListByMonth = months.map(month => { + const monthData = sortedDataByMonth.filter(item => { + return new Date(item.start_date).getMonth() === months.indexOf(month); + }); + return { month, data: monthData }; + }); + + + return ( +
+ {isLoading && ( +
+
+
+ )} + {errorMessage !== null ? ( +

{errorMessage}

+ ) : ( + <> + {sortedDataListByMonth.map(({ month, data }, monthIndex) => ( +
+ {data.length > 0 && ( + <> +

{month}

+ {data.map((conversation, index) => ( +
handleMouseEnter(`${monthIndex}-${index}`)} + onMouseLeave={handleMouseLeave} + > + + {hoveredItemIndex === `${monthIndex}-${index}` && ( +
+ Destroy + Edit +
+ )} +
+ ))} + + )} +
+ ))} + + )} +
+ ); + + +} \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index 0b800b97..81152df0 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -2,6 +2,7 @@ import { AddFilled } from "@fluentui/react-icons" import styles from "./ChatHistoryPannel.module.css"; import { useAppContext } from "../../providers/AppProviders"; +import { ChatHistoryPanelList } from "./ChatHistoryListItem"; export const ChatHistoryPanel = () => { const { showHistoryPanel, setShowHistoryPanel} = useAppContext() @@ -11,22 +12,21 @@ export const ChatHistoryPanel = () => { } return (
-
-
Chat history
-
-
- +
+
Chat history
+
+
+ +
+
+ +
+
-
- -
-
-
-
-

Conversations

-
-
-
+
+ +
+ ) } \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index a2f400ac..d4244204 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -35,6 +35,7 @@ display: flex; justify-content: space-between; align-items: center; + position: sticky; } .title { @@ -52,4 +53,72 @@ display: flex; flex-direction: column; max-width: 100%; +} + +.buttonConversation{ + border: none; + padding: 10px; + text-align: left; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background-color: transparent; + cursor: pointer; +} + + +.actionsButtons{ + display: flex; +} + +.conversationContainer{ + display: flex; + border-radius: 5px; +} + +.conversationContainer:hover{ + background-color: rgb(228, 228, 228); +} + +.actionButton{ + height: 28px; + justify-content: center; + align-items: center; + margin: auto; + background-color: white; + padding: 3px; + border-radius: 5px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + margin-right: 5px; + transition: .3s; +} + +.actionButton:hover{ + border: 1.5px solid rgb(212, 212, 212); + box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.34) inset; +} + +.loaderContainer{ + margin-top: 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.customLoader { + width: 25px; + height: 25px; + border-radius: 50%; + border: 3px solid; + border-color: #E4E4ED; + border-right-color: #5da3cc; + animation: s2 1s infinite linear; +} + +@keyframes s2 { + to { + transform: rotate(1turn) + } } \ No newline at end of file diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 378641af..a4968dfc 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -12,6 +12,7 @@ display: flex; flex-direction: row-reverse; height: 100%; + overflow: hidden; } .chatRoot { @@ -118,6 +119,32 @@ .commandsContainer { display: flex; align-self: flex-start; + height: 95%; + overflow-y: auto; +} + +.loadingData { + display: flex; + align-self: flex-start; + height: 95%; + overflow-y: hidden; +} + +.commandsContainer::-webkit-scrollbar { + width: 8px; +} + +.commandsContainer::-webkit-scrollbar-track { + background: transparent; +} + +.commandsContainer::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.commandsContainer::-webkit-scrollbar-thumb:hover { + background: #555; } .commandButton { diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 15f11614..14f206f8 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,4 +1,5 @@ import { Outlet, NavLink, Link } from "react-router-dom"; +import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; import github from "../../assets/github.svg"; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 57cc4980..584ce04e 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,16 +1,21 @@ import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; +import { ConversationHistoryItem } from "../api"; interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; + dataHistory: ConversationHistoryItem[]; + setDataHistory: Dispatch>; } const AppContext = createContext(undefined); export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [showHistoryPanel, setShowHistoryPanel] = useState(false); + const [dataHistory, setDataHistory] = useState([]); + return ( - + {children} ); From 4989aed371a25bb92ff4ff4fe2edd9fda90eac7c Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Mon, 1 Apr 2024 11:33:58 -0400 Subject: [PATCH 008/820] Revert "SFC 50 populate conversation list (#5)" (#7) This reverts commit 0901a747ed3ce62e041cb6ad75f5343070d51dc7. --- .env.template | 3 - backend/app.py | 28 +---- frontend/public/favicon.ico | Bin 15406 -> 4286 bytes frontend/src/api/api.ts | 28 +---- frontend/src/api/models.ts | 6 - frontend/src/assets/pencil.png | Bin 1082 -> 0 bytes frontend/src/assets/trash.png | Bin 734 -> 0 bytes .../HistoryPannel/ChatHistoryListItem.tsx | 110 ------------------ .../HistoryPannel/ChatHistoryPanel.tsx | 30 ++--- .../ChatHistoryPannel.module.css | 69 ----------- frontend/src/pages/chat/Chat.module.css | 27 ----- frontend/src/pages/layout/Layout.tsx | 1 - frontend/src/providers/AppProviders.tsx | 7 +- 13 files changed, 23 insertions(+), 286 deletions(-) delete mode 100644 frontend/src/assets/pencil.png delete mode 100644 frontend/src/assets/trash.png delete mode 100644 frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx diff --git a/.env.template b/.env.template index 19e037f7..a8fb5735 100644 --- a/.env.template +++ b/.env.template @@ -1,8 +1,5 @@ # orchestrator's endpoint: use http://localhost:7071/api/orc when running everything locally. ORCHESTRATOR_ENDPOINT="http://localhost:7071/api/orc" -# conversation history endpoint: use http://localhost:7071/api/conversations when running everything locally. -HISTORY_ENDPOINT="http://localhost:7071/api/conversations" - AZURE_KEY_VAULT_NAME="key_vault_name" # Speech sythesis and recognition diff --git a/backend/app.py b/backend/app.py index ff05977c..24c33593 100644 --- a/backend/app.py +++ b/backend/app.py @@ -16,7 +16,6 @@ SPEECH_REGION = os.getenv('SPEECH_REGION') ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') -HISTORY_ENDPOINT = os.getenv('HISTORY_ENDPOINT') SETTINGS_ENDPOINT = os.getenv('SETTINGS_ENDPOINT') ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI') STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') @@ -85,32 +84,7 @@ def chatgpt(): logging.exception("[webbackend] exception in /chatgpt") return jsonify({"error": str(e)}), 500 -@app.route("/api/get-chat-history", methods=["GET"]) -def getChatHistory(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - try: - # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function - # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--functionKey' - functionKey = get_secret(keySecretName) - except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 - try: - url = HISTORY_ENDPOINT - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } - payload = json.dumps({ - "user_id": client_principal_id - }) - response = requests.request("GET",url,headers=headers,data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) - except Exception as e: - logging.exception("[webbackend] exception in /get-chat-history") - return jsonify({"error": str(e)}), 500 + # methods to provide access to speech services and blob storage account blobs @app.route("/api/get-speech-token", methods=["GET"]) diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index 94cc94600468f1bd5fd20b41d5e460be8f139944..f1fe50511ca0c33d95783506d4af99426dfc10bf 100644 GIT binary patch literal 4286 zcmeH}dr(y88OA?KWOvysX{UdrX?5Dk2n0}AK@g0WnRH4JL8DO=m3RY_c8nwq(U1w# zblPb%a#2_oP(cAfxywyJkeD=zq)sBaX_L6ZDzLE2?us@Vd!D}Eo;|v0z+{sC8JwT* z@$8=Sd*0_P<1*$&e`d{M{BKsRW9;XQF@J(ivBd;G&wsM$wa4g1&W}I$8|c~T?c4qQ zLmRqMWkp?SJ~>@Ge6l;!e6l+@sXjS`p?#Yy_Wl*$JQ~=;HeYy=6c%Kf!Ux=ZJCigSyTk z@tp5xu(*$Rv#qzA zCYq+a+3TcxoJ^Ce$lKy}3f$YeO^%kWGB1yf?-Yd+b-GCy?h&Her946SKixNPvpa9IcjOKAV%$L~XVuJSBK@)riCNcWK@xgMq+ z?}$zr@0#{ippo;eLLg9`>`T4KB0qhVZRCBs{N(Qa>}`v?~M8s1&(j@Mdf-W`qI5^mopg#Jp315 zrrozt2Y-DO)|zmb%gA3A1T*Cra^>Jzf&`w0I%)NEdLGlRDcsjUKJFw0K9AAfeD`xN zc;nKm;@*6B4)6ZGUF1*o!oNxaVX3D5j|3ATFdwEIyo<6R!CXoiTv>usJe{7$4JoRJ z$#=UhMUL7{3ZZtRLg4ON+Py#hC%E!D!>4-~=-G?ilb?6rvz_>(29~;T3?H3?>y;t6 zULJyL1fS-^Ap-B@T3PTNnp1W!<*47}+fcJnff{o1&MT6XXrtdpzBljugO|N9kiowE z{53XyDU&%bq%&tj5*vFzd)6&`bEN36;u(mFIk?7g=OBNhSU$XXRg;InLv3i%1j#nLsCrxz|BZ_s13hTNRv zp%^(9>dG=)J(sfZ40HZ%t_add*F0z*U9W85pY=-eaw<4UDxq8iG;v_1tWvs%t#w|7 zr9KRn`cRD4k((H;B`C`<$Ub7={&qviNoaN@Xk4<&v4WzOz?$qwRZ0H z`mkU}X9UKY^%y%rG*X5}J;5`GolqZkYjfz7|94H7rEIObvTTj=p7W6C6%MUd3kS(x zuv~?`@#;czU8vD{DgxuJ^Dx%p&OkXVt}HaEld-1I^65O&oiAOZP2{^CTCD>29b8SU zqUXL!jRSO?{X1X0nWzzloAv*>(KBBdZwn{p2|UL*&*9EuY0|^mtWTKEGu?Ue`;gP= z@~zU4Q-flf#pEw}T7%aX_~6Lze}UU)qv1FeAy`j_b6{-`2W6o?!o>_NdWWqwQa+vM zLFbE~QJ0rIO`kRL6?u@KphiZN9B1t95$Z8m4lv1`#f0NBJTa3 zJjgZGYsOWYr;1jpVO*&};R+GsN-h5Oq!NFL_roYTZ@ses6Q|~jbWx5@^7A|d&tYqe z_{MjZsa#W?GbHL}7*=SkE_b3zP0 zKv{%|Zt9>r>TBPZzN^#oeFX`+xcvz_xDno1qQalz{9x^k!kzbG;CMRm>}=nfhTa|d<;SUz6D>;@op6L5%Yt)m;AZ0 zx*30u^)v0%D^Z*I8{o4gxb@E%O!m8>9Fr1#)XCZC&+Wa7{x9828UDOf8=JXQXZWm1 zpNq?jvnTtbvv2mtWZyjRp7ig@^U=At`lIu2o{N4`y6(sM;0ECS$(q^gm8H!hm{*pD zdG)H8cQ0e!CdOnEcE+?W=v`lWWzBTK*vz|(Es_uzdtCyI;T~CDfJCpiq{V*$i&S=H literal 15406 zcmeI235-?c702JGym$Bg-M0^Kh8bo+0y9o!P^_UlU_=n4zw_X>FUBfXj4`=4`Eu|5mh=73 z|D5l;-#h0e5;cka6ONPMUQV2IKq4_Pkw}!wyWgALMB)zWT3YtB_f901q!Wo|`Y?nM zyX(2d*O$ORI-NFr+y1s;7^7_4?q!;0lWiL(Te^NK<#gLHPO>cRm{8Xn?73C4fF-Pz`)YP>HEJK@Z>grs}(C;eJPuH~DZBw7<+V*%uQ&;F} zYNcfuQ;>I?W$M4wRpl;CQMx?OJk-#%<(}hQ=vdZR%1xBHx~4AYJ`0(Pe9Jo6)U@w; zo^>=>2C1p!tD2H}+OzFTXd7r6+WpKiOUBUe)=VbTkN3sW&YX8cVNErS9)b)->C{wZ zo$I=_hM`TRu4J0p2*)%>2tQz&>RQ%mj-^ki9$Wp)m_w`IB#YqBzbD!EPO*EQuC&P3zF&>Jc^Y}2@p^UM~?Hs&AZTE;0n zch}a|a!TGkKYH(??gQ60CJh-fgp+fne!6MskAP<+b7YzS9m;xewi&wi1pHj*qdRiy zv-Dy7_WDxk8=TetPTO?HaV~(Qv^+lP3ahLJ@Pi((147@9O zzrI(mUM2Xv9}X_&{J9kUmWDwvP3}^_{4xN{4XDdLvr-?3sl=~KvNzg5iTMhBjI8~(XH zT0j2_gV+hVJNDq%iErI3Ye)K9x^}0oaRzpr$>L+o{W!WUp1%H^^G7uPFde@$H-jloYc=`Sb#3OBOO z#*7)GSJz8hM@NSuZL%(h4b{2llrAm(fr3{4(UmGGhi}24V(c24V&x z1|&{Gh&ItdiQ^=Y5MMC^F#|CJF#|CJF#|CJF#|CJ`QdQ)kk&m9>w0J zE*j5cB_7|<(2$Z6y??Bayhhi45@(b6SX5CxSM@XP6^wF^`bPIYefs2l-#MRH<<(Lo zuV3%Fl~_Nrla6CuNSyd$FjaB`#Fx*J_<0ZnCUL|b#HCM=af4f1^MT`xATE59rD->l z%O5HDL>Je!PC?F9f=T8j##kgicqwt@*^K?M#5{=whty3Zzkgn-RO-d^<$>p&C39qb z|6uw}AwJ$7;gl=QYZP6H$~@6+Pjc=?t(q} zgoB!!n|*K&mfS-HzY2aO^}gW8UL#Z`xe+@p96WfiNt}GT=mOWB+`B{`_w7d z)DU~W4E)>4tz61`#fA}n`tL&aI_z~rU0uG3d_kkcx2y5P#TGdIGqK>`_9Ny`4rV_2 zg4MY&=mhhtg&^q7_$T3FIewLwIU`^BIDYa$qrtYB+`x@82e|`yPwvET=aDm73zjaP zuU3=Ejl`{&H8s^WabGDovEyT57-nQ#HGXMB#)sJPNn*ywM05X#e|XO2!g0jlPa;oN zM_hb7{Uryqi8%dSiSc``eJk(X*lrSek+H0iYkK$Y-AoQ)ti;g4wXIw#jVk1F<9L1( zy;p&KH1>X!aho$~|0md`3#_kz;S!!VklT4Ko%XMw{R7{z=i|)(%BH+j$U0wi#NHadvJ`)RH!9@X#xrjpWIaRM8?4RI(pP+4@;u1fBzUmJ zli2ctXk6uv->@}Yfo{{7=PmkeVa;r0ZQk2hF1KqbWXfFB(Y`ht23Hz}zL2%~EF5L= zxw*EXKg}9>-uJwR^ZERF!X@nwiho5qSf;sHFyX%n_L}Iq?mbe--(22ME)T&+j)$X> zl8@?%F0%f`-^qQP=DYT%mbT-3&$*p`B6C## z{{3~yerShTA4p8;Mlq1Ot@JEzl5D|7vZ zachwIKKs^v(S8w)`>Z<{+mP`vvUf&0$UOMLE3!W2n<+91>2wQre~o=$4eMYGHfope zJ2Z(<_OmR4B0Pao$#)+myP4>F)11gM+cg^J|iml$`y=n;2U-x-@x1Px7pb8yHWrD z^IpT=?c-AuglEyA8o$Vt?=k#p)n`RHcM9%m-<5W$i*Q7Jq%JD7Rldz4PtJGp9`*mC zcl>1lTrF}Pd(jt_^;L~$f6MdTJn^fVe_za7{rhBJ$ctsf48#n?48#n?48#n?48#n? P48#n?48#n4JsJ2n-pT47 diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a81f2630..19af781a 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,4 +1,3 @@ -import { List } from "@fluentui/react"; import { AskRequest, AskResponse, @@ -6,8 +5,7 @@ import { ChatRequest, ChatRequestGpt, GetSettingsProps, - PostSettingsProps, - ConversationHistoryItem + PostSettingsProps } from "./models"; const loadModule = async (modulePath : string) => { @@ -107,35 +105,21 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise { - const response = await fetch("/api/get-chat-history", { - method: "GET", - headers: { - "Content-Type": "application/json" - } - }); - const parsedResponse: ConversationHistoryItem[] = await response.json(); - if (response.status > 299 || !response.ok) { - throw Error("Error getting chat history"); - } - return parsedResponse; -} - export function getCitationFilePath(citation: string): string { var storage_account = "please_check_if_storage_account_is_in_frontend_app_settings"; - + const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/get-storage-account", false); xhr.send(); if (xhr.status > 299) { console.log("Please check if STORAGE_ACCOUNT is in frontend app settings"); - return storage_account; + return storage_account } else { const parsedResponse = JSON.parse(xhr.responseText); - storage_account = parsedResponse["storageaccount"]; + storage_account = parsedResponse['storageaccount']; } - console.log("storage account:" + storage_account); + console.log('storage account:' + storage_account); return `https://${storage_account}.blob.core.windows.net/documents/${citation}`; -} +} \ No newline at end of file diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 862cf474..fc36b441 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -4,12 +4,6 @@ export const enum Approaches { ReadDecomposeAsk = "rda" } -export type ConversationHistoryItem = { - id: string, - start_date: string, - content: string, -}; - export type AskRequestOverrides = { semanticRanker?: boolean; semanticCaptions?: boolean; diff --git a/frontend/src/assets/pencil.png b/frontend/src/assets/pencil.png deleted file mode 100644 index a3648d8bdebd63f0ab3a77962d25126007998b46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1082 zcmV-A1jYM_P)=9((I-|(!@k6rBI2HZYn9XATgyEFIq^G z-DGAqv-5h`OpIwsljgmC!|(aNhi^V&hjTPLSzmHOX8|;W{FUWESLVnIfBEamPVn}= z8+9cobO{tsATf&E5yidIVNN~0-*oI z1pq!O9(H(1fV+`pY5Vq*_{!R%2s_Q`Zv<$jr@p%F6I5{g1WJUEhNY;o`+pyhl1axt zC2+-4+;iIs=rE_M!zCF94sYX(0wK!w4!pJLU&66hq5;x3p>`hfQ7pG>I3WW zw+IXfFzkm!m%Ldk!T~~_vNR+fT9J{NiE7UB7$8N}T|DUHo#u4wDt_2B;$41SRzyZX z@5wUVH*=}@!lnV;XYvuy9Rp|w+~R?r4_~u=Ttxawwwp$L3;-NWKLz5hfK?vKiN5qp zivSSV47Vb<2(s*GrMCX`D|V%p=>#AH0x^1x_{G%3ZR5ab~4k|RPY`3!JSX) zwj(0|gYeX3E)}yXJHM6`k@EsEOrvqc$92cf2~i&^*~xQ9_ZQY%_nqMQ=~=BDbcY0t z*qYul(7tc!Kb(Ub@vyTz1fV`xvRgLd_XQlv-VBw4#bE(zY)x;;rS=v#;AtN}etK4O z)#3$^wYH|W^cnHugW6Em<6+yn-~qM4lKoKpUhTq=P^#B&WYYlMv3fwXDnP*1wEY!) zx>baOmD}RiGO6FIUmPVxk1R8}84j#Ib{*zawIb?20y#!eo(JVUdU8S(_L6MRq+_E% zG@FoWM%qovq@P01*n10P*(W{;4Mbz%*i4 zZSD460zZf-p9@Pr08rDhoLWNRt80?F3R*M96ja@!r1Kmvk=KX*+JDqu5!0*S{V zJn1PbKl_i?qmByT84#ezI7B(wdn9&aC*a2U7XxhQx;5i54*&oF07*qoM6N<$g7fj} AD*ylh diff --git a/frontend/src/assets/trash.png b/frontend/src/assets/trash.png deleted file mode 100644 index 813612cec95730b130d14ea0d4644cfa8307963d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 734 zcmV<40wMj0P)X_`x#=E1?e z-+S--dGGh*cPI3)Vr4ataViRvz?o=yS6~?%OjoDJ|8%QQ^#Z8llu#xSUWu{;pu$;I z=EOKpfJXxXu#6&n^a^7STbEtWzoE+YUgZ+MOfC7NMtKWd>j~{{o2cIRiZ{PN%LJm7 zfC~c82T@m;#YhRbcN72@fv;6}?C~G~*Yn3wF2#QP8RUwUPj?0ZDCB^@9ROMfnNTCk zg8(9}_s0NY?W`K-HE>(3-aVD9k$1ocvGzg71h?uxb3K12_7@hn(lh1tk=QR*R`bQm zYCiU7%IhNwi(Bb~de3jVOa9YVP1DBM28JhAR#E`9Tx~u`1sjJQnz9;Gj{P7NY}9hK zd4Q-U4Z58SFwy`R`FTDQNuC3;hY6Amp99NAIb8mo&jP518XWndHULDm??Xnynlro2 z { - - const [hoveredItemIndex, setHoveredItemIndex] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const {dataHistory, setDataHistory} = useAppContext() - - - const handleMouseEnter = (index: string) => { - setHoveredItemIndex(index); - }; - - const handleMouseLeave = () => { - setHoveredItemIndex(null); - }; - - - const fetchData = async () => { - try { - const data = await getChatHistory(); - if(data.length > 0){ - setDataHistory(data) - setIsLoading(false); - }else{ - setIsLoading(false); - setErrorMessage("There are not conversation history yet.") - } - } catch (error) { - console.error('Error fetching data:', error); - setIsLoading(false); - setErrorMessage(`Was an error fetching data: ${error}`) - } - }; - - useEffect(() => { - fetchData(); - }, []); - - const months = [ - "January", "February", "March", "April", - "May", "June", "July", "August", - "September", "October", "November", "December" - ]; - - const sortedDataByMonth = dataHistory.sort((a, b) => { - const monthA = new Date(a.start_date).getMonth(); - const monthB = new Date(b.start_date).getMonth(); - return monthA - monthB; - }); - - const sortedDataListByMonth = months.map(month => { - const monthData = sortedDataByMonth.filter(item => { - return new Date(item.start_date).getMonth() === months.indexOf(month); - }); - return { month, data: monthData }; - }); - - - return ( -
- {isLoading && ( -
-
-
- )} - {errorMessage !== null ? ( -

{errorMessage}

- ) : ( - <> - {sortedDataListByMonth.map(({ month, data }, monthIndex) => ( -
- {data.length > 0 && ( - <> -

{month}

- {data.map((conversation, index) => ( -
handleMouseEnter(`${monthIndex}-${index}`)} - onMouseLeave={handleMouseLeave} - > - - {hoveredItemIndex === `${monthIndex}-${index}` && ( -
- Destroy - Edit -
- )} -
- ))} - - )} -
- ))} - - )} -
- ); - - -} \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index 81152df0..0b800b97 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -2,7 +2,6 @@ import { AddFilled } from "@fluentui/react-icons" import styles from "./ChatHistoryPannel.module.css"; import { useAppContext } from "../../providers/AppProviders"; -import { ChatHistoryPanelList } from "./ChatHistoryListItem"; export const ChatHistoryPanel = () => { const { showHistoryPanel, setShowHistoryPanel} = useAppContext() @@ -12,21 +11,22 @@ export const ChatHistoryPanel = () => { } return (
-
-
Chat history
-
-
- -
-
- -
-
+
+
Chat history
+
+
+
-
- -
-
+
+ +
+
+
+
+

Conversations

+
+
+ ) } \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index d4244204..a2f400ac 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -35,7 +35,6 @@ display: flex; justify-content: space-between; align-items: center; - position: sticky; } .title { @@ -53,72 +52,4 @@ display: flex; flex-direction: column; max-width: 100%; -} - -.buttonConversation{ - border: none; - padding: 10px; - text-align: left; - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - background-color: transparent; - cursor: pointer; -} - - -.actionsButtons{ - display: flex; -} - -.conversationContainer{ - display: flex; - border-radius: 5px; -} - -.conversationContainer:hover{ - background-color: rgb(228, 228, 228); -} - -.actionButton{ - height: 28px; - justify-content: center; - align-items: center; - margin: auto; - background-color: white; - padding: 3px; - border-radius: 5px; - cursor: pointer; - border: 1.5px solid rgb(231, 231, 231); - margin-right: 5px; - transition: .3s; -} - -.actionButton:hover{ - border: 1.5px solid rgb(212, 212, 212); - box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.34) inset; -} - -.loaderContainer{ - margin-top: 10px; - display: flex; - justify-content: center; - align-items: center; -} - -.customLoader { - width: 25px; - height: 25px; - border-radius: 50%; - border: 3px solid; - border-color: #E4E4ED; - border-right-color: #5da3cc; - animation: s2 1s infinite linear; -} - -@keyframes s2 { - to { - transform: rotate(1turn) - } } \ No newline at end of file diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index a4968dfc..378641af 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -12,7 +12,6 @@ display: flex; flex-direction: row-reverse; height: 100%; - overflow: hidden; } .chatRoot { @@ -119,32 +118,6 @@ .commandsContainer { display: flex; align-self: flex-start; - height: 95%; - overflow-y: auto; -} - -.loadingData { - display: flex; - align-self: flex-start; - height: 95%; - overflow-y: hidden; -} - -.commandsContainer::-webkit-scrollbar { - width: 8px; -} - -.commandsContainer::-webkit-scrollbar-track { - background: transparent; -} - -.commandsContainer::-webkit-scrollbar-thumb { - background: #888; - border-radius: 4px; -} - -.commandsContainer::-webkit-scrollbar-thumb:hover { - background: #555; } .commandButton { diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 14f206f8..15f11614 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,5 +1,4 @@ import { Outlet, NavLink, Link } from "react-router-dom"; -import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; import github from "../../assets/github.svg"; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 584ce04e..57cc4980 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,21 +1,16 @@ import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; -import { ConversationHistoryItem } from "../api"; interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; - dataHistory: ConversationHistoryItem[]; - setDataHistory: Dispatch>; } const AppContext = createContext(undefined); export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [showHistoryPanel, setShowHistoryPanel] = useState(false); - const [dataHistory, setDataHistory] = useState([]); - return ( - + {children} ); From 65eba73bdc5903d957d96be600062a6a6aa429de Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:06:23 -0400 Subject: [PATCH 009/820] SFC- 39 fix (#9) --- backend/app.py | 4 ++-- frontend/src/api/api.ts | 24 +++---------------- .../src/components/SettingsModal/index.tsx | 2 +- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/backend/app.py b/backend/app.py index 24c33593..847f961f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -16,8 +16,8 @@ SPEECH_REGION = os.getenv('SPEECH_REGION') ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') -SETTINGS_ENDPOINT = os.getenv('SETTINGS_ENDPOINT') -ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI') +ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI', default="") +SETTINGS_ENDPOINT = ORCHESTRATOR_URI + "/settings" STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper() logging.basicConfig(level=LOGLEVEL) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 19af781a..a77d8c98 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -8,23 +8,11 @@ import { PostSettingsProps } from "./models"; -const loadModule = async (modulePath : string) => { - try { - return (await import(/* @vite-ignore */modulePath))?.default; - } catch (e) { - console.log("Error loading " + modulePath, e); - } -} - export async function getSettings({ user }: GetSettingsProps): Promise { - const settings = await loadModule("./settings.json"); - const baseUrl = settings?.ENVIRONMENT === "development" ? settings?.LOCAL_API : ""; - const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; - const url = baseUrl + "/api/settings"; try { - const response = await fetch(url, { + const response = await fetch("/api/settings", { method: "GET", headers: { "Content-Type": "application/json", @@ -41,14 +29,10 @@ export async function getSettings({ user }: GetSettingsProps): Promise { } export async function postSettings({ user, temperature, presence_penalty, frequency_penalty } : PostSettingsProps): Promise { - const settings = await loadModule("./settings.json"); - const baseUrl = settings?.ENVIRONMENT === "development" ? settings?.LOCAL_API : ""; - const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; - const url = baseUrl + "/api/settings"; try { - const response = await fetch(url, { + const response = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json", @@ -71,9 +55,7 @@ export async function postSettings({ user, temperature, presence_penalty, freque } export async function chatApiGpt(options: ChatRequestGpt): Promise { - const settings = await loadModule("./settings.json"); - const baseUrl = settings?.ENVIRONMENT === "development" ? settings?.LOCAL_API : ""; - const response = await fetch(baseUrl + "/chatgpt", { + const response = await fetch("/chatgpt", { method: "POST", headers: { "Content-Type": "application/json" diff --git a/frontend/src/components/SettingsModal/index.tsx b/frontend/src/components/SettingsModal/index.tsx index 76819493..66ef2411 100644 --- a/frontend/src/components/SettingsModal/index.tsx +++ b/frontend/src/components/SettingsModal/index.tsx @@ -13,7 +13,7 @@ interface Props { } const SettingsModal = ({ user }: Props) => { - const [isModalOpen, setIsModalOpen] = useState(true); + const [isModalOpen, setIsModalOpen] = useState(false); const [temperature, setTemperature] = useState(""); const [presencePenalty, setPresencePenalty] = useState(""); const [frequencyPenalty, setFrequencyPenalty] = useState(""); From 8120b51ce122be8808dbad8e0f7fa83b19cd5f11 Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Mon, 1 Apr 2024 15:36:57 -0400 Subject: [PATCH 010/820] SFC 50 populate conversation list (#8) * add local enpoint to call orchestrator * fix URL add HISTORY ENV endpoint * SFC-50 list of history conversation and favicon done * add client id to the request * conflict resolved * comments resolved * comments of Pedro resolved --------- Co-authored-by: Alejandro Lopez --- .env.template | 1 + backend/app.py | 27 +++++ frontend/public/favicon.ico | Bin 4286 -> 15406 bytes frontend/src/api/api.ts | 29 ++++- frontend/src/api/models.ts | 6 + frontend/src/assets/pencil.png | Bin 0 -> 1082 bytes frontend/src/assets/trash.png | Bin 0 -> 734 bytes .../HistoryPannel/ChatHistoryListItem.tsx | 110 ++++++++++++++++++ .../HistoryPannel/ChatHistoryPanel.tsx | 30 ++--- .../ChatHistoryPannel.module.css | 69 +++++++++++ frontend/src/pages/chat/Chat.module.css | 27 +++++ frontend/src/pages/layout/Layout.tsx | 1 + frontend/src/providers/AppProviders.tsx | 11 +- 13 files changed, 289 insertions(+), 22 deletions(-) create mode 100644 frontend/src/assets/pencil.png create mode 100644 frontend/src/assets/trash.png create mode 100644 frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx diff --git a/.env.template b/.env.template index a8fb5735..8711d3bb 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,6 @@ # orchestrator's endpoint: use http://localhost:7071/api/orc when running everything locally. ORCHESTRATOR_ENDPOINT="http://localhost:7071/api/orc" + AZURE_KEY_VAULT_NAME="key_vault_name" # Speech sythesis and recognition diff --git a/backend/app.py b/backend/app.py index 847f961f..b33805f9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -18,6 +18,7 @@ ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI', default="") SETTINGS_ENDPOINT = ORCHESTRATOR_URI + "/settings" +HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/conversations" STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper() logging.basicConfig(level=LOGLEVEL) @@ -84,6 +85,32 @@ def chatgpt(): logging.exception("[webbackend] exception in /chatgpt") return jsonify({"error": str(e)}), 500 +@app.route("/api/get-chat-history", methods=["GET"]) +def getChatHistory(): + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = 'orchestrator-host--functionKey' + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") + return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + try: + url = HISTORY_ENDPOINT + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + payload = json.dumps({ + "user_id": client_principal_id + }) + response = requests.request("GET",url,headers=headers,data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return(response.text) + except Exception as e: + logging.exception("[webbackend] exception in /get-chat-history") + return jsonify({"error": str(e)}), 500 # methods to provide access to speech services and blob storage account blobs diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index f1fe50511ca0c33d95783506d4af99426dfc10bf..94cc94600468f1bd5fd20b41d5e460be8f139944 100644 GIT binary patch literal 15406 zcmeI235-?c702JGym$Bg-M0^Kh8bo+0y9o!P^_UlU_=n4zw_X>FUBfXj4`=4`Eu|5mh=73 z|D5l;-#h0e5;cka6ONPMUQV2IKq4_Pkw}!wyWgALMB)zWT3YtB_f901q!Wo|`Y?nM zyX(2d*O$ORI-NFr+y1s;7^7_4?q!;0lWiL(Te^NK<#gLHPO>cRm{8Xn?73C4fF-Pz`)YP>HEJK@Z>grs}(C;eJPuH~DZBw7<+V*%uQ&;F} zYNcfuQ;>I?W$M4wRpl;CQMx?OJk-#%<(}hQ=vdZR%1xBHx~4AYJ`0(Pe9Jo6)U@w; zo^>=>2C1p!tD2H}+OzFTXd7r6+WpKiOUBUe)=VbTkN3sW&YX8cVNErS9)b)->C{wZ zo$I=_hM`TRu4J0p2*)%>2tQz&>RQ%mj-^ki9$Wp)m_w`IB#YqBzbD!EPO*EQuC&P3zF&>Jc^Y}2@p^UM~?Hs&AZTE;0n zch}a|a!TGkKYH(??gQ60CJh-fgp+fne!6MskAP<+b7YzS9m;xewi&wi1pHj*qdRiy zv-Dy7_WDxk8=TetPTO?HaV~(Qv^+lP3ahLJ@Pi((147@9O zzrI(mUM2Xv9}X_&{J9kUmWDwvP3}^_{4xN{4XDdLvr-?3sl=~KvNzg5iTMhBjI8~(XH zT0j2_gV+hVJNDq%iErI3Ye)K9x^}0oaRzpr$>L+o{W!WUp1%H^^G7uPFde@$H-jloYc=`Sb#3OBOO z#*7)GSJz8hM@NSuZL%(h4b{2llrAm(fr3{4(UmGGhi}24V(c24V&x z1|&{Gh&ItdiQ^=Y5MMC^F#|CJF#|CJF#|CJF#|CJ`QdQ)kk&m9>w0J zE*j5cB_7|<(2$Z6y??Bayhhi45@(b6SX5CxSM@XP6^wF^`bPIYefs2l-#MRH<<(Lo zuV3%Fl~_Nrla6CuNSyd$FjaB`#Fx*J_<0ZnCUL|b#HCM=af4f1^MT`xATE59rD->l z%O5HDL>Je!PC?F9f=T8j##kgicqwt@*^K?M#5{=whty3Zzkgn-RO-d^<$>p&C39qb z|6uw}AwJ$7;gl=QYZP6H$~@6+Pjc=?t(q} zgoB!!n|*K&mfS-HzY2aO^}gW8UL#Z`xe+@p96WfiNt}GT=mOWB+`B{`_w7d z)DU~W4E)>4tz61`#fA}n`tL&aI_z~rU0uG3d_kkcx2y5P#TGdIGqK>`_9Ny`4rV_2 zg4MY&=mhhtg&^q7_$T3FIewLwIU`^BIDYa$qrtYB+`x@82e|`yPwvET=aDm73zjaP zuU3=Ejl`{&H8s^WabGDovEyT57-nQ#HGXMB#)sJPNn*ywM05X#e|XO2!g0jlPa;oN zM_hb7{Uryqi8%dSiSc``eJk(X*lrSek+H0iYkK$Y-AoQ)ti;g4wXIw#jVk1F<9L1( zy;p&KH1>X!aho$~|0md`3#_kz;S!!VklT4Ko%XMw{R7{z=i|)(%BH+j$U0wi#NHadvJ`)RH!9@X#xrjpWIaRM8?4RI(pP+4@;u1fBzUmJ zli2ctXk6uv->@}Yfo{{7=PmkeVa;r0ZQk2hF1KqbWXfFB(Y`ht23Hz}zL2%~EF5L= zxw*EXKg}9>-uJwR^ZERF!X@nwiho5qSf;sHFyX%n_L}Iq?mbe--(22ME)T&+j)$X> zl8@?%F0%f`-^qQP=DYT%mbT-3&$*p`B6C## z{{3~yerShTA4p8;Mlq1Ot@JEzl5D|7vZ zachwIKKs^v(S8w)`>Z<{+mP`vvUf&0$UOMLE3!W2n<+91>2wQre~o=$4eMYGHfope zJ2Z(<_OmR4B0Pao$#)+myP4>F)11gM+cg^J|iml$`y=n;2U-x-@x1Px7pb8yHWrD z^IpT=?c-AuglEyA8o$Vt?=k#p)n`RHcM9%m-<5W$i*Q7Jq%JD7Rldz4PtJGp9`*mC zcl>1lTrF}Pd(jt_^;L~$f6MdTJn^fVe_za7{rhBJ$ctsf48#n?48#n?48#n?48#n? P48#n?48#n4JsJ2n-pT47 literal 4286 zcmeH}dr(y88OA?KWOvysX{UdrX?5Dk2n0}AK@g0WnRH4JL8DO=m3RY_c8nwq(U1w# zblPb%a#2_oP(cAfxywyJkeD=zq)sBaX_L6ZDzLE2?us@Vd!D}Eo;|v0z+{sC8JwT* z@$8=Sd*0_P<1*$&e`d{M{BKsRW9;XQF@J(ivBd;G&wsM$wa4g1&W}I$8|c~T?c4qQ zLmRqMWkp?SJ~>@Ge6l;!e6l+@sXjS`p?#Yy_Wl*$JQ~=;HeYy=6c%Kf!Ux=ZJCigSyTk z@tp5xu(*$Rv#qzA zCYq+a+3TcxoJ^Ce$lKy}3f$YeO^%kWGB1yf?-Yd+b-GCy?h&Her946SKixNPvpa9IcjOKAV%$L~XVuJSBK@)riCNcWK@xgMq+ z?}$zr@0#{ippo;eLLg9`>`T4KB0qhVZRCBs{N(Qa>}`v?~M8s1&(j@Mdf-W`qI5^mopg#Jp315 zrrozt2Y-DO)|zmb%gA3A1T*Cra^>Jzf&`w0I%)NEdLGlRDcsjUKJFw0K9AAfeD`xN zc;nKm;@*6B4)6ZGUF1*o!oNxaVX3D5j|3ATFdwEIyo<6R!CXoiTv>usJe{7$4JoRJ z$#=UhMUL7{3ZZtRLg4ON+Py#hC%E!D!>4-~=-G?ilb?6rvz_>(29~;T3?H3?>y;t6 zULJyL1fS-^Ap-B@T3PTNnp1W!<*47}+fcJnff{o1&MT6XXrtdpzBljugO|N9kiowE z{53XyDU&%bq%&tj5*vFzd)6&`bEN36;u(mFIk?7g=OBNhSU$XXRg;InLv3i%1j#nLsCrxz|BZ_s13hTNRv zp%^(9>dG=)J(sfZ40HZ%t_add*F0z*U9W85pY=-eaw<4UDxq8iG;v_1tWvs%t#w|7 zr9KRn`cRD4k((H;B`C`<$Ub7={&qviNoaN@Xk4<&v4WzOz?$qwRZ0H z`mkU}X9UKY^%y%rG*X5}J;5`GolqZkYjfz7|94H7rEIObvTTj=p7W6C6%MUd3kS(x zuv~?`@#;czU8vD{DgxuJ^Dx%p&OkXVt}HaEld-1I^65O&oiAOZP2{^CTCD>29b8SU zqUXL!jRSO?{X1X0nWzzloAv*>(KBBdZwn{p2|UL*&*9EuY0|^mtWTKEGu?Ue`;gP= z@~zU4Q-flf#pEw}T7%aX_~6Lze}UU)qv1FeAy`j_b6{-`2W6o?!o>_NdWWqwQa+vM zLFbE~QJ0rIO`kRL6?u@KphiZN9B1t95$Z8m4lv1`#f0NBJTa3 zJjgZGYsOWYr;1jpVO*&};R+GsN-h5Oq!NFL_roYTZ@ses6Q|~jbWx5@^7A|d&tYqe z_{MjZsa#W?GbHL}7*=SkE_b3zP0 zKv{%|Zt9>r>TBPZzN^#oeFX`+xcvz_xDno1qQalz{9x^k!kzbG;CMRm>}=nfhTa|d<;SUz6D>;@op6L5%Yt)m;AZ0 zx*30u^)v0%D^Z*I8{o4gxb@E%O!m8>9Fr1#)XCZC&+Wa7{x9828UDOf8=JXQXZWm1 zpNq?jvnTtbvv2mtWZyjRp7ig@^U=At`lIu2o{N4`y6(sM;0ECS$(q^gm8H!hm{*pD zdG)H8cQ0e!CdOnEcE+?W=v`lWWzBTK*vz|(Es_uzdtCyI;T~CDfJCpiq{V*$i&S=H diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a77d8c98..85f8b5a0 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,3 +1,4 @@ +import { List } from "@fluentui/react"; import { AskRequest, AskResponse, @@ -5,7 +6,8 @@ import { ChatRequest, ChatRequestGpt, GetSettingsProps, - PostSettingsProps + PostSettingsProps, + ConversationHistoryItem } from "./models"; export async function getSettings({ user }: GetSettingsProps): Promise { @@ -87,21 +89,36 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise { + const response = await fetch("/api/get-chat-history", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId + } + }); + const parsedResponse: ConversationHistoryItem[] = await response.json(); + if (response.status > 299 || !response.ok) { + throw Error("Error getting chat history"); + } + return parsedResponse; +} + export function getCitationFilePath(citation: string): string { var storage_account = "please_check_if_storage_account_is_in_frontend_app_settings"; - + const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/get-storage-account", false); xhr.send(); if (xhr.status > 299) { console.log("Please check if STORAGE_ACCOUNT is in frontend app settings"); - return storage_account + return storage_account; } else { const parsedResponse = JSON.parse(xhr.responseText); - storage_account = parsedResponse['storageaccount']; + storage_account = parsedResponse["storageaccount"]; } - console.log('storage account:' + storage_account); + console.log("storage account:" + storage_account); return `https://${storage_account}.blob.core.windows.net/documents/${citation}`; -} \ No newline at end of file +} diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index fc36b441..862cf474 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -4,6 +4,12 @@ export const enum Approaches { ReadDecomposeAsk = "rda" } +export type ConversationHistoryItem = { + id: string, + start_date: string, + content: string, +}; + export type AskRequestOverrides = { semanticRanker?: boolean; semanticCaptions?: boolean; diff --git a/frontend/src/assets/pencil.png b/frontend/src/assets/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..a3648d8bdebd63f0ab3a77962d25126007998b46 GIT binary patch literal 1082 zcmV-A1jYM_P)=9((I-|(!@k6rBI2HZYn9XATgyEFIq^G z-DGAqv-5h`OpIwsljgmC!|(aNhi^V&hjTPLSzmHOX8|;W{FUWESLVnIfBEamPVn}= z8+9cobO{tsATf&E5yidIVNN~0-*oI z1pq!O9(H(1fV+`pY5Vq*_{!R%2s_Q`Zv<$jr@p%F6I5{g1WJUEhNY;o`+pyhl1axt zC2+-4+;iIs=rE_M!zCF94sYX(0wK!w4!pJLU&66hq5;x3p>`hfQ7pG>I3WW zw+IXfFzkm!m%Ldk!T~~_vNR+fT9J{NiE7UB7$8N}T|DUHo#u4wDt_2B;$41SRzyZX z@5wUVH*=}@!lnV;XYvuy9Rp|w+~R?r4_~u=Ttxawwwp$L3;-NWKLz5hfK?vKiN5qp zivSSV47Vb<2(s*GrMCX`D|V%p=>#AH0x^1x_{G%3ZR5ab~4k|RPY`3!JSX) zwj(0|gYeX3E)}yXJHM6`k@EsEOrvqc$92cf2~i&^*~xQ9_ZQY%_nqMQ=~=BDbcY0t z*qYul(7tc!Kb(Ub@vyTz1fV`xvRgLd_XQlv-VBw4#bE(zY)x;;rS=v#;AtN}etK4O z)#3$^wYH|W^cnHugW6Em<6+yn-~qM4lKoKpUhTq=P^#B&WYYlMv3fwXDnP*1wEY!) zx>baOmD}RiGO6FIUmPVxk1R8}84j#Ib{*zawIb?20y#!eo(JVUdU8S(_L6MRq+_E% zG@FoWM%qovq@P01*n10P*(W{;4Mbz%*i4 zZSD460zZf-p9@Pr08rDhoLWNRt80?F3R*M96ja@!r1Kmvk=KX*+JDqu5!0*S{V zJn1PbKl_i?qmByT84#ezI7B(wdn9&aC*a2U7XxhQx;5i54*&oF07*qoM6N<$g7fj} AD*ylh literal 0 HcmV?d00001 diff --git a/frontend/src/assets/trash.png b/frontend/src/assets/trash.png new file mode 100644 index 0000000000000000000000000000000000000000..813612cec95730b130d14ea0d4644cfa8307963d GIT binary patch literal 734 zcmV<40wMj0P)X_`x#=E1?e z-+S--dGGh*cPI3)Vr4ataViRvz?o=yS6~?%OjoDJ|8%QQ^#Z8llu#xSUWu{;pu$;I z=EOKpfJXxXu#6&n^a^7STbEtWzoE+YUgZ+MOfC7NMtKWd>j~{{o2cIRiZ{PN%LJm7 zfC~c82T@m;#YhRbcN72@fv;6}?C~G~*Yn3wF2#QP8RUwUPj?0ZDCB^@9ROMfnNTCk zg8(9}_s0NY?W`K-HE>(3-aVD9k$1ocvGzg71h?uxb3K12_7@hn(lh1tk=QR*R`bQm zYCiU7%IhNwi(Bb~de3jVOa9YVP1DBM28JhAR#E`9Tx~u`1sjJQnz9;Gj{P7NY}9hK zd4Q-U4Z58SFwy`R`FTDQNuC3;hY6Amp99NAIb8mo&jP518XWndHULDm??Xnynlro2 z { + + const [hoveredItemIndex, setHoveredItemIndex] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const {dataHistory, setDataHistory, userId} = useAppContext() + + + const handleMouseEnter = (index: string) => { + setHoveredItemIndex(index); + }; + + const handleMouseLeave = () => { + setHoveredItemIndex(null); + }; + + + const fetchData = async () => { + try { + const data = await getChatHistory(userId); + if(data.length > 0){ + setDataHistory(data) + setIsLoading(false); + }else{ + setIsLoading(false); + setErrorMessage("There are not conversation history yet.") + } + } catch (error) { + console.error('Error fetching data:', error); + setIsLoading(false); + setErrorMessage(`Was an error fetching data: ${error}`) + } + }; + + useEffect(() => { + fetchData(); + }, [userId]); + + const months = [ + "January", "February", "March", "April", + "May", "June", "July", "August", + "September", "October", "November", "December" + ]; + + const sortedDataByMonth = dataHistory.sort((a, b) => { + const monthA = new Date(a.start_date).getMonth(); + const monthB = new Date(b.start_date).getMonth(); + return monthA - monthB; + }); + + const sortedDataListByMonth = months.map(month => { + const monthData = sortedDataByMonth.filter(item => { + return new Date(item.start_date).getMonth() === months.indexOf(month); + }); + return { month, data: monthData }; + }); + + + return ( +
+ {isLoading && ( +
+
+
+ )} + {errorMessage !== null ? ( +

{errorMessage}

+ ) : ( + <> + {sortedDataListByMonth.map(({ month, data }, monthIndex) => ( +
+ {data.length > 0 && ( + <> +

{month}

+ {data.map((conversation, index) => ( +
handleMouseEnter(`${monthIndex}-${index}`)} + onMouseLeave={handleMouseLeave} + > + + {hoveredItemIndex === `${monthIndex}-${index}` && ( +
+ Destroy + Edit +
+ )} +
+ ))} + + )} +
+ ))} + + )} +
+ ); + + +} \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index 0b800b97..81152df0 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -2,6 +2,7 @@ import { AddFilled } from "@fluentui/react-icons" import styles from "./ChatHistoryPannel.module.css"; import { useAppContext } from "../../providers/AppProviders"; +import { ChatHistoryPanelList } from "./ChatHistoryListItem"; export const ChatHistoryPanel = () => { const { showHistoryPanel, setShowHistoryPanel} = useAppContext() @@ -11,22 +12,21 @@ export const ChatHistoryPanel = () => { } return (
-
-
Chat history
-
-
- +
+
Chat history
+
+
+ +
+
+ +
+
-
- -
-
-
-
-

Conversations

-
-
-
+
+ +
+ ) } \ No newline at end of file diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index a2f400ac..d4244204 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -35,6 +35,7 @@ display: flex; justify-content: space-between; align-items: center; + position: sticky; } .title { @@ -52,4 +53,72 @@ display: flex; flex-direction: column; max-width: 100%; +} + +.buttonConversation{ + border: none; + padding: 10px; + text-align: left; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background-color: transparent; + cursor: pointer; +} + + +.actionsButtons{ + display: flex; +} + +.conversationContainer{ + display: flex; + border-radius: 5px; +} + +.conversationContainer:hover{ + background-color: rgb(228, 228, 228); +} + +.actionButton{ + height: 28px; + justify-content: center; + align-items: center; + margin: auto; + background-color: white; + padding: 3px; + border-radius: 5px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + margin-right: 5px; + transition: .3s; +} + +.actionButton:hover{ + border: 1.5px solid rgb(212, 212, 212); + box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.34) inset; +} + +.loaderContainer{ + margin-top: 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.customLoader { + width: 25px; + height: 25px; + border-radius: 50%; + border: 3px solid; + border-color: #E4E4ED; + border-right-color: #5da3cc; + animation: s2 1s infinite linear; +} + +@keyframes s2 { + to { + transform: rotate(1turn) + } } \ No newline at end of file diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 378641af..a4968dfc 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -12,6 +12,7 @@ display: flex; flex-direction: row-reverse; height: 100%; + overflow: hidden; } .chatRoot { @@ -118,6 +119,32 @@ .commandsContainer { display: flex; align-self: flex-start; + height: 95%; + overflow-y: auto; +} + +.loadingData { + display: flex; + align-self: flex-start; + height: 95%; + overflow-y: hidden; +} + +.commandsContainer::-webkit-scrollbar { + width: 8px; +} + +.commandsContainer::-webkit-scrollbar-track { + background: transparent; +} + +.commandsContainer::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +.commandsContainer::-webkit-scrollbar-thumb:hover { + background: #555; } .commandButton { diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 15f11614..14f206f8 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,4 +1,5 @@ import { Outlet, NavLink, Link } from "react-router-dom"; +import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; import github from "../../assets/github.svg"; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 57cc4980..880630b1 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,16 +1,25 @@ import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; +import { ConversationHistoryItem } from "../api"; interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; + dataHistory: ConversationHistoryItem[]; + setDataHistory: Dispatch>; + userId: string, + setUserId: Dispatch> } const AppContext = createContext(undefined); export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [showHistoryPanel, setShowHistoryPanel] = useState(false); + const [dataHistory, setDataHistory] = useState([]); + const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); + + return ( - + {children} ); From 1adb8a3374409bc62529031576d9d7eed52d0bad Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Mon, 1 Apr 2024 17:21:47 -0400 Subject: [PATCH 011/820] SFC-43 function keys --- backend/app.py | 6 +++--- frontend/vite.config.ts | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/app.py b/backend/app.py index b33805f9..060500fa 100644 --- a/backend/app.py +++ b/backend/app.py @@ -91,7 +91,7 @@ def getChatHistory(): try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--functionKey' + keySecretName = 'orchestrator-host--conversations' functionKey = get_secret(keySecretName) except Exception as e: logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") @@ -169,7 +169,7 @@ def getSettings(): try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--functionKey' + keySecretName = 'orchestrator-host--settings' functionKey = get_secret(keySecretName) except Exception as e: logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") @@ -210,7 +210,7 @@ def setSettings(): try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--functionKey' + keySecretName = 'orchestrator-host--settings' functionKey = get_secret(keySecretName) except Exception as e: logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1b368076..d711f0f9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -14,7 +14,9 @@ export default defineConfig({ '/chatgpt': "http://localhost:8000", '/api/get-speech-token': "http://localhost:8000", '/api/get-storage-account': "http://localhost:8000", - '/api/get-blob': "http://localhost:8000" + '/api/get-blob': "http://localhost:8000", + '/api/settings': "http://localhost:8000", + '/api/conversations': "http://localhost:8000", }, host: true } From 7c178fe28a385937dad911369ef210340e452e1f Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Tue, 2 Apr 2024 14:06:21 -0400 Subject: [PATCH 012/820] SFC-39 redesign interface --- frontend/src/api/api.ts | 4 +- .../SettingsModal/SettingsModal.module.css | 13 +- .../src/components/SettingsModal/index.tsx | 140 ++++++++++++++---- 3 files changed, 122 insertions(+), 35 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 85f8b5a0..519fef3f 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -25,8 +25,8 @@ export async function getSettings({ user }: GetSettingsProps): Promise { const fetchedData = await response.json(); return fetchedData; } catch (error) { - console.error("Error fetching settings", error); - return {}; + console.log("Error fetching settings", error); + return { temperature: "0", presencePenalty: "0", frequencyPenalty: "0" }; } } diff --git a/frontend/src/components/SettingsModal/SettingsModal.module.css b/frontend/src/components/SettingsModal/SettingsModal.module.css index d0fe3efd..93f57a11 100644 --- a/frontend/src/components/SettingsModal/SettingsModal.module.css +++ b/frontend/src/components/SettingsModal/SettingsModal.module.css @@ -18,7 +18,6 @@ .answerContainer { padding: 40px; background: rgb(249, 249, 249); - border-radius: 16px; box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); outline: transparent solid 1px; width: 300px; @@ -59,6 +58,7 @@ .closeButton:focus { outline: none; + border: none; } .header { @@ -71,4 +71,15 @@ font-weight: 500; padding-left: 10px; color: red; +} + +.infoIcon { + font-size: 50; + height: 50; + width: 50; + margin: '0 25px'; +} + +.w-100 { + width: 100%; } \ No newline at end of file diff --git a/frontend/src/components/SettingsModal/index.tsx b/frontend/src/components/SettingsModal/index.tsx index 66ef2411..3e2cafb1 100644 --- a/frontend/src/components/SettingsModal/index.tsx +++ b/frontend/src/components/SettingsModal/index.tsx @@ -1,9 +1,10 @@ -import React, { useState, useEffect } from "react"; -import { SettingsFilled, SaveFilled } from "@fluentui/react-icons"; +import React, { useState, useEffect, useCallback } from "react"; +import { SettingsFilled, SaveFilled, ErrorCircleFilled } from "@fluentui/react-icons"; +import { Checkbox } from "@fluentui/react/lib/Checkbox"; +import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Modal, Stack, Text, Spinner, Slider } from "@fluentui/react"; import styles from "./SettingsModal.module.css"; import { getSettings, postSettings } from "../../api/api"; - -import { DefaultButton, Modal, Stack, Text, TextField, Spinner } from "@fluentui/react"; +import { mergeStyles } from "@fluentui/react/lib/Styling"; interface Props { user: { @@ -12,21 +13,46 @@ interface Props { } | null; } +const iconClass = mergeStyles({ + fontSize: 25, + height: 25, + width: 25, + margin: "0", + padding: "5px 0 0 0px", + cursor: "pointer" +}); + +const itemClass = mergeStyles({ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "10px 0" +}); + const SettingsModal = ({ user }: Props) => { const [isModalOpen, setIsModalOpen] = useState(false); - const [temperature, setTemperature] = useState(""); - const [presencePenalty, setPresencePenalty] = useState(""); - const [frequencyPenalty, setFrequencyPenalty] = useState(""); + const [temperature, setTemperature] = useState("0"); + const [presencePenalty, setPresencePenalty] = useState("0"); + const [frequencyPenalty, setFrequencyPenalty] = useState("0"); const [loading, setLoading] = useState(true); + const temperatureDialog = + "It adjusts the balance between creativity and predictability in responses. Lower settings yield straightforward answers, while higher settings introduce originality and diversity, perfect for creative tasks and factual inquiries."; + const frequencyPenaltyDialog = + "Streamlines dialogue by minimizing repetition. Increase to boost variety and prevent redundancy; decrease to ensure key terms recur, enhancing focus on specific concepts. Use it to decrease excessive repetition."; + const presencePenaltyDialog = + "Promotes the introduction of new topics and ideas. Increase to discover varied concepts; decrease to maintain focus on current discussions. Ideal for brainstorming and exploration."; + useEffect(() => { const fetchData = async () => { - getSettings({ user }).then(data => { - setTemperature(data.temperature); - setPresencePenalty(data.presencePenalty); - setFrequencyPenalty(data.frequencyPenalty); - setLoading(false); - }); + getSettings({ user }) + .then(data => { + setTemperature(data.temperature); + setPresencePenalty(data.presencePenalty); + setFrequencyPenalty(data.frequencyPenalty); + setLoading(false); + }) + .catch(error => setLoading(false)); }; setLoading(true); @@ -42,7 +68,8 @@ const SettingsModal = ({ user }: Props) => { }; const handleSubmit = () => { - if (!temperature || !presencePenalty || !frequencyPenalty) { + if ((!temperature && temperature != "0") || (!presencePenalty && presencePenalty != "0") || (!frequencyPenalty && frequencyPenalty != "0")) { + console.error("Invalid settings are not submitted."); return; } @@ -74,22 +101,39 @@ const SettingsModal = ({ user }: Props) => { }; const handleSetTemperature = (val: any) => { - if (validateValue(val.target.value, setTemperature)) { - setTemperature(val.target.value); - } - }; - - const handleSetPresencePenalty = (val: any) => { - if (validateValue(val.target.value, setPresencePenalty)) { - setPresencePenalty(val.target.value); + const value = String(val); + if (validateValue(value, setTemperature)) { + setTemperature(value); } }; - const handleSetFrequencyPenalty = (val: any) => { - if (validateValue(val.target.value, setFrequencyPenalty)) { - setFrequencyPenalty(val.target.value); - } - }; + const handleSetPresencePenalty = useCallback((ev?: React.FormEvent, checked?: boolean): void => { + const presencePenalty = !!checked ? "1" : "0"; + setPresencePenalty(presencePenalty); + }, []); + + const handleSetFrequencyPenalty = useCallback((ev?: React.FormEvent, checked?: boolean): void => { + const frequencyPenalty = !!checked ? "1" : "0"; + setFrequencyPenalty(frequencyPenalty); + }, []); + + const onRenderLabel = (dialog: string, title: string) => ( + ( +
+

{title}

+ {dialog} +
+ ) + }} + delay={TooltipDelay.zero} + directionalHint={DirectionalHint.bottomCenter} + styles={{ root: { display: "inline-block" } }} + > + +
+ ); return (
@@ -100,7 +144,7 @@ const SettingsModal = ({ user }: Props) => { - +

Adjust your settings

@@ -119,10 +163,42 @@ const SettingsModal = ({ user }: Props) => {

Loading your settings

) : ( -
- handleSetTemperature(e)} /> - handleSetFrequencyPenalty(e)} /> - handleSetPresencePenalty(e)} /> +
+
+
+ Creativity Scale + {onRenderLabel(temperatureDialog, "Temperature")} +
+ handleSetTemperature(e)} + /> +
+
+ Variety Boost + onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")} + /> +
+
+ Topic Explorer + onRenderLabel(presencePenaltyDialog, "Presence Penalty")} + /> +
  Save From fa876ecdb1a6b6ca0591ef93390ba7146a8c60d5 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Wed, 3 Apr 2024 08:36:53 -0400 Subject: [PATCH 013/820] backend and frontend created, with new configurations in the pannel and new functinalities --- backend/app.py | 31 +++ frontend/src/api/api.ts | 42 +++- frontend/src/api/models.ts | 5 + .../ChatHistoryButton.module.css | 2 + .../HistoryPannel/ChatHistoryListItem.tsx | 72 ++++++- .../ChatHistoryPannel.module.css | 9 + .../SettingsModal/SettingsModal.module.css | 22 +- frontend/src/pages/chat/Chat.module.css | 8 + frontend/src/pages/chat/Chat.tsx | 194 ++++++++++++++---- frontend/src/pages/layout/Layout.module.css | 4 + frontend/src/pages/layout/Layout.tsx | 4 +- frontend/src/providers/AppProviders.tsx | 23 ++- 12 files changed, 357 insertions(+), 59 deletions(-) diff --git a/backend/app.py b/backend/app.py index 060500fa..454dc2aa 100644 --- a/backend/app.py +++ b/backend/app.py @@ -111,6 +111,37 @@ def getChatHistory(): except Exception as e: logging.exception("[webbackend] exception in /get-chat-history") return jsonify({"error": str(e)}), 500 + +@app.route("/api/get-chat-conversation", methods=["GET"]) +def getChatConversation(): + chat_id = request.args.get('id') + + if chat_id is None: + return jsonify({"error": "Missing chatId parameter"}), 400 + + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + try: + keySecretName = 'orchestrator-host--functionKey' + functionKey = get_secret(keySecretName) + except Exception as e: + return jsonify({"error": f"Error getting function key: {e}"}), 500 + + try: + url = f"{HISTORY_ENDPOINT}/?id={chat_id}" + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + payload = json.dumps({ + "user_id": client_principal_id + }) + response = requests.request("GET", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + + return response.text, response.status_code + except Exception as e: + logging.exception("[webbackend] exception in /get-chat-history") + return jsonify({"error": str(e)}), 500 # methods to provide access to speech services and blob storage account blobs diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 519fef3f..69043433 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -7,7 +7,9 @@ import { ChatRequestGpt, GetSettingsProps, PostSettingsProps, - ConversationHistoryItem + ConversationHistoryItem, + ConversationChatItem, + ChatTurn } from "./models"; export async function getSettings({ user }: GetSettingsProps): Promise { @@ -85,10 +87,46 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise 299 || !response.ok) { throw Error(parsedResponse.error || "Unknown error"); } - return parsedResponse; } +export async function getChatFromHistoryPannelById(chatId: string, userId: string): Promise { + const response = await fetch(`/api/get-chat-conversation?id=${chatId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId + } + }); + + const responseData = await response.json(); + const history = responseData.history; + + const conversationItems: ChatTurn[] = []; + let currentUserMessage = ''; + let currentBotMessage = ''; + + history.forEach((item: any) => { + if (item.role === 'user') { + currentUserMessage = item.content; + } else if (item.role === 'assistant') { + currentBotMessage = item.content; + if (currentUserMessage !== '' || currentBotMessage !== '') { + conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); + currentUserMessage = ''; + currentBotMessage = ''; + } + } + }); + + if (currentUserMessage !== '' || currentBotMessage !== '') { + conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); + } + + return conversationItems; +} + + export async function getChatHistory(userId: string): Promise { const response = await fetch("/api/get-chat-history", { method: "GET", diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 862cf474..2311c01e 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -10,6 +10,11 @@ export type ConversationHistoryItem = { content: string, }; +export type ConversationChatItem = { + role: string, + content: string +} + export type AskRequestOverrides = { semanticRanker?: boolean; semanticCaptions?: boolean; diff --git a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css index fe1dab18..c7a11c30 100644 --- a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css +++ b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.module.css @@ -9,6 +9,8 @@ padding: 4px 12px; transition: .2s; background-color: white; + width: 178px; + margin-right: 5px; } .container:hover{ diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 1f663527..106ba661 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -1,16 +1,19 @@ import styles from "./ChatHistoryPannel.module.css"; -import { getChatHistory } from "../../api"; +import { getChatHistory, getChatFromHistoryPannelById } from "../../api"; import { useEffect, useState } from "react"; import { useAppContext } from "../../providers/AppProviders"; import trash from "../../assets/trash.png" import pencil from "../../assets/pencil.png" +import { Spinner } from "@fluentui/react"; export const ChatHistoryPanelList = () => { const [hoveredItemIndex, setHoveredItemIndex] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [isLoading, setIsLoading] = useState(true); - const {dataHistory, setDataHistory, userId} = useAppContext() + const [chatSelected, setChatSelected] = useState(""); + const [conversationsIds, setConversationsIds] = useState([]); + const {dataHistory, setDataHistory, userId, dataConversation, setDataConversation, setConversationIsLoading, setChatId, chatId, refreshFetchHistorial, setRefreshFetchHistorial } = useAppContext() const handleMouseEnter = (index: string) => { @@ -26,8 +29,18 @@ export const ChatHistoryPanelList = () => { try { const data = await getChatHistory(userId); if(data.length > 0){ - setDataHistory(data) + const sortedData = data.sort((a, b) => { + // Convertir las fechas a objetos Date para compararlas + const dateA = new Date(a.start_date); + const dateB = new Date(b.start_date); + return dateB.getTime() - dateA.getTime(); // Orden descendente + }); + setDataHistory(sortedData) setIsLoading(false); + const ids = sortedData.map(data => (data.id)) + if(!ids.every(id => conversationsIds.includes(id))){ + setConversationsIds(ids) + } }else{ setIsLoading(false); setErrorMessage("There are not conversation history yet.") @@ -39,9 +52,47 @@ export const ChatHistoryPanelList = () => { } }; - useEffect(() => { - fetchData(); - }, [userId]); + const fetchConversation = async (chatConversationId: string) => { + try { + if(!chatSelected.includes(chatConversationId)){ + setChatSelected(chatConversationId) + setChatId(chatConversationId); + setConversationIsLoading(true) + const data = await getChatFromHistoryPannelById(chatConversationId, userId); + if(data.length > 0){ + setDataConversation(data) + setConversationIsLoading(false) + } + } + } catch (error) { + console.error('Error fetching data:', error); + setConversationIsLoading(false) + setErrorMessage(`Was an error fetching data: ${error}`) + } + }; + + const handleRefreshHistoial = async () => { + if(refreshFetchHistorial){ + await fetchData() + setRefreshFetchHistorial(false) + }else{ + return + } + } + + useEffect(() => { + + if(dataHistory.length <= 0 || !conversationsIds.every(id => dataHistory.some(item => item.id === id))){ + fetchData(); + }else{ + setIsLoading(false) + } + + if(refreshFetchHistorial){ + handleRefreshHistoial(); + } + + }, [userId, dataHistory, conversationsIds, refreshFetchHistorial]); const months = [ "January", "February", "March", "April", @@ -67,7 +118,7 @@ export const ChatHistoryPanelList = () => {
{isLoading && (
-
+
)} {errorMessage !== null ? ( @@ -82,19 +133,20 @@ export const ChatHistoryPanelList = () => { {data.map((conversation, index) => (
handleMouseEnter(`${monthIndex}-${index}`)} onMouseLeave={handleMouseLeave} + onClick={() => fetchConversation(conversation.id)} > - {hoveredItemIndex === `${monthIndex}-${index}` && ( + {hoveredItemIndex === `${monthIndex}-${index}` || chatSelected === conversation.id || chatId === conversation.id ? (
Destroy Edit
- )} + ):(<>)}
))} diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index d4244204..28bd9de2 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -75,6 +75,15 @@ .conversationContainer{ display: flex; border-radius: 5px; + margin-bottom: 4px; +} + +.conversationSelected{ + background-color: rgba(221, 221, 221, 0.349); + display: flex; + border-radius: 5px; + margin-bottom: 4px; + transition: .2s; } .conversationContainer:hover{ diff --git a/frontend/src/components/SettingsModal/SettingsModal.module.css b/frontend/src/components/SettingsModal/SettingsModal.module.css index 93f57a11..13df7878 100644 --- a/frontend/src/components/SettingsModal/SettingsModal.module.css +++ b/frontend/src/components/SettingsModal/SettingsModal.module.css @@ -1,9 +1,25 @@ .button { - color: rgb(63, 63, 63); + display: flex; + align-items: center; + margin-top: 5px; + gap: 6px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + border-radius: 5px; + padding: 4px 12px; + transition: .2s; + background-color: white; font-size: 24px; - border: none; width: 100%; - display: flex; + height: 35px; +} + +.button:hover { + background-color: rgb(243, 243, 243); +} + +.button:active { + background-color: white; } .buttonText { diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index a4968dfc..9d345dc0 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -62,6 +62,10 @@ } } +.conversationIsLoading{ + display: "none"; +} + .chatMessageStream { flex-grow: 1; max-height: 1024px; @@ -130,6 +134,10 @@ overflow-y: hidden; } +.spinnerStyles{ + position: absolute; +} + .commandsContainer::-webkit-scrollbar { width: 8px; } diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index df5f1166..93b19ab3 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -1,5 +1,5 @@ import { useRef, useState, useEffect } from "react"; -import { Checkbox, Panel, DefaultButton, TextField, SpinButton } from "@fluentui/react"; +import { Checkbox, Panel, DefaultButton, TextField, SpinButton, Spinner } from "@fluentui/react"; import { SparkleFilled, TabDesktopMultipleBottomRegular } from "@fluentui/react-icons"; import styles from "./Chat.module.css"; @@ -42,7 +42,9 @@ const Chat = () => { const [excludeCategory, setExcludeCategory] = useState(""); const [useSuggestFollowupQuestions, setUseSuggestFollowupQuestions] = useState(false); - const {showHistoryPanel} = useAppContext() + const chatContainerRef = useRef(null); + + const {showHistoryPanel, dataConversation, setDataConversation, chatId, conversationIsLoading, setRefreshFetchHistorial, setChatId} = useAppContext() const lastQuestionRef = useRef(""); @@ -61,7 +63,69 @@ const Chat = () => { const [userId, setUserId] = useState(""); const triggered = useRef(false); - const makeApiRequestGpt = async (question: string) => { + // const continueConversation = async (chatId: string, question: string) => { + // lastQuestionRef.current = question; + + // error && setError(undefined); + // setIsLoading(true); + // setActiveCitation(undefined); + // setActiveAnalysisPanelTab(undefined); + + // try { + // const history: ChatTurn[] = answers.map(a => ({ user: a[0], bot: a[1].answer })); + // const request: ChatRequestGpt = { + // history: [...history, { user: question, bot: undefined }], + // approach: Approaches.ReadRetrieveRead, + // conversation_id: chatId, + // query: question, + // overrides: { + // promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, + // excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, + // top: retrieveCount, + // semanticRanker: useSemanticRanker, + // semanticCaptions: useSemanticCaptions, + // suggestFollowupQuestions: useSuggestFollowupQuestions + // } + // }; + // const result = await chatApiGpt(request); + // console.log(result); + // console.log(result.answer); + // setAnswers([...answers, [question, result]]); + // setUserId(result.conversation_id); + + // // Voice Synthesis + // if (speechSynthesisEnabled) { + // const tokenObj = await getTokenOrRefresh(); + // const speechConfig = SpeechConfig.fromAuthorizationToken(tokenObj.authToken, tokenObj.region); + // const audioConfig = AudioConfig.fromDefaultSpeakerOutput(); + // speechConfig.speechSynthesisLanguage = tokenObj.speechSynthesisLanguage; + // speechConfig.speechSynthesisVoiceName = tokenObj.speechSynthesisVoiceName; + // const synthesizer = new SpeechSynthesizer(speechConfig, audioConfig); + + // synthesizer.speakTextAsync( + // result.answer.replace(/ *\[[^)]*\] */g, ""), + // function (result) { + // if (result.reason === ResultReason.SynthesizingAudioCompleted) { + // console.log("synthesis finished."); + // } else { + // console.error("Speech synthesis canceled, " + result.errorDetails + "\nDid you update the subscription info?"); + // } + // synthesizer.close(); + // }, + // function (err) { + // console.trace("err - " + err); + // synthesizer.close(); + // } + // ); + // } + // } catch (e) { + // setError(e); + // } finally { + // setIsLoading(false); + // } + // } + + const makeApiRequestGpt = async (question: string, chatId: string | null) => { lastQuestionRef.current = question; error && setError(undefined); @@ -70,11 +134,17 @@ const Chat = () => { setActiveAnalysisPanelTab(undefined); try { - const history: ChatTurn[] = answers.map(a => ({ user: a[0], bot: a[1].answer })); + let history: ChatTurn[] = [];; + if(dataConversation.length > 0){ + history.push(...dataConversation); + }else{ + history.push(...answers.map(a => ({ user: a[0], bot: a[1].answer }))); + } + history.push({ user: question, bot: undefined }); const request: ChatRequestGpt = { - history: [...history, { user: question, bot: undefined }], + history: history, approach: Approaches.ReadRetrieveRead, - conversation_id: userId, + conversation_id: chatId !== null ? chatId : userId, query: question, overrides: { promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, @@ -86,10 +156,22 @@ const Chat = () => { } }; const result = await chatApiGpt(request); - console.log(result); - console.log(result.answer); + const conditionOne = answers.map(a => ({ user: a[0]})) + if(conditionOne.length <= 0){ + setRefreshFetchHistorial(true); + setChatId(result.conversation_id) + }else{ + setRefreshFetchHistorial(false); + } setAnswers([...answers, [question, result]]); setUserId(result.conversation_id); + const response = { + answer: result.answer || "", + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse + setDataConversation([...dataConversation, {user: question, bot: response.answer}]) // Voice Synthesis if (speechSynthesisEnabled) { @@ -163,6 +245,9 @@ const Chat = () => { useEffect(() => { chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }); + if (dataConversation.length > 0) { + chatContainerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } if (triggered.current === false) { triggered.current = true; console.log(triggered.current); @@ -176,7 +261,7 @@ const Chat = () => { } else { setPlaceholderText("Write your question here"); } - }, [isLoading]); + }, [isLoading, dataConversation]); const onPromptTemplateChange = (_ev?: React.FormEvent, newValue?: string) => { setPromptTemplate(newValue || ""); @@ -202,9 +287,9 @@ const Chat = () => { setUseSuggestFollowupQuestions(!!checked); }; - const onExampleClicked = (example: string) => { - makeApiRequestGpt(example); - }; + // const onExampleClicked = (example: string) => { + // makeApiRequestGpt(example); + // }; const onShowCitation = async (citation: string, fileName: string, index: number) => { const response = await getPdf(fileName); @@ -263,38 +348,72 @@ const Chat = () => {
- {!lastQuestionRef.current ? ( -
-
0 ? styles.chatMessageStream : styles.chatEmptyState} style={conversationIsLoading ? {flexGrow: 1, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", maxHeight: "1024px", paddingTop: "60px"} : {}}> + {conversationIsLoading && } + +
+ justifyContent: 'center'} : {display: "none"}}>

Clew

Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver actionable insights.

+
+
-
) : ( -
- {answers.map((answer, index) => ( -
- -
- onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q)} - showFollowupQuestions={false} - showSources={true} - /> +
+ {conversationIsLoading && } + {dataConversation.length > 0 ? ( + dataConversation.map((item, index) => { + const response = { + answer: item.bot || "", + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse + return( +
+ +
+ onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} + showFollowupQuestions={false} + showSources={true} + /> +
+
+ ) + }) + ) : ( + answers.map((answer, index) => ( +
+ +
+ onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} + showFollowupQuestions={false} + showSources={true} + /> +
-
- ))} + )) + )} + {isLoading && ( <> @@ -307,16 +426,17 @@ const Chat = () => { <>
- makeApiRequestGpt(lastQuestionRef.current)} /> + makeApiRequestGpt(lastQuestionRef.current, null)} />
) : null}
+ )}
- makeApiRequestGpt(question)} /> + makeApiRequestGpt(question, chatId !== '' ? chatId : null)} />
diff --git a/frontend/src/pages/layout/Layout.module.css b/frontend/src/pages/layout/Layout.module.css index 86a4ad1d..1cae2d17 100644 --- a/frontend/src/pages/layout/Layout.module.css +++ b/frontend/src/pages/layout/Layout.module.css @@ -76,3 +76,7 @@ .githubLogo { height: 20px; } + +.layoutOptions{ + display: flex; +} \ No newline at end of file diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 14f206f8..c095349a 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -54,10 +54,10 @@ const Layout = () => { */} -
+
{/* needs an user to be sent ↓ */} - +
diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 880630b1..4cf80e7c 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,25 +1,38 @@ import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; -import { ConversationHistoryItem } from "../api"; +import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; + refreshFetchHistorial: boolean; + setRefreshFetchHistorial: Dispatch>; dataHistory: ConversationHistoryItem[]; setDataHistory: Dispatch>; - userId: string, - setUserId: Dispatch> + userId: string; + setUserId: Dispatch>; + chatId: string; + setChatId: Dispatch>; + dataConversation: ChatTurn[]; + setDataConversation: Dispatch>; + conversationIsLoading: boolean; + setConversationIsLoading: Dispatch> } const AppContext = createContext(undefined); export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [showHistoryPanel, setShowHistoryPanel] = useState(false); + const [showHistoryPanel, setShowHistoryPanel] = useState(true); + const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); const [dataHistory, setDataHistory] = useState([]); + const [dataConversation, setDataConversation] = useState([]); const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); + const [chatId, setChatId] = useState(""); + const [conversationIsLoading, setConversationIsLoading] = useState(false); + return ( - + {children} ); From d20199f21fcb108b99398565cccefff951edde99 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Wed, 3 Apr 2024 08:45:04 -0400 Subject: [PATCH 014/820] comments eliminated --- frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 106ba661..734895d0 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -30,10 +30,9 @@ export const ChatHistoryPanelList = () => { const data = await getChatHistory(userId); if(data.length > 0){ const sortedData = data.sort((a, b) => { - // Convertir las fechas a objetos Date para compararlas const dateA = new Date(a.start_date); const dateB = new Date(b.start_date); - return dateB.getTime() - dateA.getTime(); // Orden descendente + return dateB.getTime() - dateA.getTime(); }); setDataHistory(sortedData) setIsLoading(false); From 7a5c0fd6f2b63b3ac0c88ec9c6e043fc244675b1 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Wed, 3 Apr 2024 09:58:23 -0400 Subject: [PATCH 015/820] SFC-39 fixed default values for slider and checkboxes --- frontend/src/components/SettingsModal/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/SettingsModal/index.tsx b/frontend/src/components/SettingsModal/index.tsx index 3e2cafb1..f212f88f 100644 --- a/frontend/src/components/SettingsModal/index.tsx +++ b/frontend/src/components/SettingsModal/index.tsx @@ -175,7 +175,7 @@ const SettingsModal = ({ user }: Props) => { min={0} max={1} step={0.1} - defaultValue={0} + value={parseFloat(temperature)} showValue snapToStep onChange={e => handleSetTemperature(e)} @@ -185,7 +185,7 @@ const SettingsModal = ({ user }: Props) => { Variety Boost onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")} /> @@ -194,7 +194,7 @@ const SettingsModal = ({ user }: Props) => { Topic Explorer onRenderLabel(presencePenaltyDialog, "Presence Penalty")} /> From 44a3cfda51fae1c133e4c2bf3bc42a622257a482 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Wed, 3 Apr 2024 11:37:55 -0400 Subject: [PATCH 016/820] pedro's comments resolved --- frontend/src/pages/chat/Chat.module.css | 19 +- frontend/src/pages/chat/Chat.tsx | 235 +++++++++--------------- 2 files changed, 103 insertions(+), 151 deletions(-) diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 9d345dc0..63dc75df 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -62,8 +62,25 @@ } } +.noneDisplay{ + display: none; +} + +.flexDescription{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + .conversationIsLoading{ - display: "none"; + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + max-height: 1024px; + padding-top: 60px; } .chatMessageStream { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 93b19ab3..bc874fc8 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -18,7 +18,6 @@ import salesLogo from "../../img/logo.png"; import { useAppContext } from "../../providers/AppProviders"; import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; - const userLanguage = navigator.language; let error_message_text = ""; if (userLanguage.startsWith("pt")) { @@ -44,8 +43,7 @@ const Chat = () => { const chatContainerRef = useRef(null); - const {showHistoryPanel, dataConversation, setDataConversation, chatId, conversationIsLoading, setRefreshFetchHistorial, setChatId} = useAppContext() - + const { showHistoryPanel, dataConversation, setDataConversation, chatId, conversationIsLoading, setRefreshFetchHistorial, setChatId } = useAppContext(); const lastQuestionRef = useRef(""); const chatMessageStreamEnd = useRef(null); @@ -63,68 +61,6 @@ const Chat = () => { const [userId, setUserId] = useState(""); const triggered = useRef(false); - // const continueConversation = async (chatId: string, question: string) => { - // lastQuestionRef.current = question; - - // error && setError(undefined); - // setIsLoading(true); - // setActiveCitation(undefined); - // setActiveAnalysisPanelTab(undefined); - - // try { - // const history: ChatTurn[] = answers.map(a => ({ user: a[0], bot: a[1].answer })); - // const request: ChatRequestGpt = { - // history: [...history, { user: question, bot: undefined }], - // approach: Approaches.ReadRetrieveRead, - // conversation_id: chatId, - // query: question, - // overrides: { - // promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, - // excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, - // top: retrieveCount, - // semanticRanker: useSemanticRanker, - // semanticCaptions: useSemanticCaptions, - // suggestFollowupQuestions: useSuggestFollowupQuestions - // } - // }; - // const result = await chatApiGpt(request); - // console.log(result); - // console.log(result.answer); - // setAnswers([...answers, [question, result]]); - // setUserId(result.conversation_id); - - // // Voice Synthesis - // if (speechSynthesisEnabled) { - // const tokenObj = await getTokenOrRefresh(); - // const speechConfig = SpeechConfig.fromAuthorizationToken(tokenObj.authToken, tokenObj.region); - // const audioConfig = AudioConfig.fromDefaultSpeakerOutput(); - // speechConfig.speechSynthesisLanguage = tokenObj.speechSynthesisLanguage; - // speechConfig.speechSynthesisVoiceName = tokenObj.speechSynthesisVoiceName; - // const synthesizer = new SpeechSynthesizer(speechConfig, audioConfig); - - // synthesizer.speakTextAsync( - // result.answer.replace(/ *\[[^)]*\] */g, ""), - // function (result) { - // if (result.reason === ResultReason.SynthesizingAudioCompleted) { - // console.log("synthesis finished."); - // } else { - // console.error("Speech synthesis canceled, " + result.errorDetails + "\nDid you update the subscription info?"); - // } - // synthesizer.close(); - // }, - // function (err) { - // console.trace("err - " + err); - // synthesizer.close(); - // } - // ); - // } - // } catch (e) { - // setError(e); - // } finally { - // setIsLoading(false); - // } - // } - const makeApiRequestGpt = async (question: string, chatId: string | null) => { lastQuestionRef.current = question; @@ -134,10 +70,10 @@ const Chat = () => { setActiveAnalysisPanelTab(undefined); try { - let history: ChatTurn[] = [];; - if(dataConversation.length > 0){ + let history: ChatTurn[] = []; + if (dataConversation.length > 0) { history.push(...dataConversation); - }else{ + } else { history.push(...answers.map(a => ({ user: a[0], bot: a[1].answer }))); } history.push({ user: question, bot: undefined }); @@ -156,22 +92,22 @@ const Chat = () => { } }; const result = await chatApiGpt(request); - const conditionOne = answers.map(a => ({ user: a[0]})) - if(conditionOne.length <= 0){ + const conditionOne = answers.map(a => ({ user: a[0] })); + if (conditionOne.length <= 0) { setRefreshFetchHistorial(true); - setChatId(result.conversation_id) - }else{ + setChatId(result.conversation_id); + } else { setRefreshFetchHistorial(false); } setAnswers([...answers, [question, result]]); setUserId(result.conversation_id); const response = { - answer: result.answer || "", - conversation_id: chatId, - data_points: [""], - thoughts: null - } as AskResponse - setDataConversation([...dataConversation, {user: question, bot: response.answer}]) + answer: result.answer || "", + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse; + setDataConversation([...dataConversation, { user: question, bot: response.answer }]); // Voice Synthesis if (speechSynthesisEnabled) { @@ -246,7 +182,7 @@ const Chat = () => { useEffect(() => { chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }); if (dataConversation.length > 0) { - chatContainerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); + chatContainerRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); } if (triggered.current === false) { triggered.current = true; @@ -339,80 +275,72 @@ const Chat = () => { return (
-
- {showHistoryPanel && ( - - )} -
+
{showHistoryPanel && }
-
+
{!lastQuestionRef.current && dataConversation.length <= 0 ? ( -
0 ? styles.chatMessageStream : styles.chatEmptyState} style={conversationIsLoading ? {flexGrow: 1, display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", maxHeight: "1024px", paddingTop: "60px"} : {}}> - {conversationIsLoading && } - -
- -

Clew

-

Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver actionable insights.

-
- +
0 && !conversationIsLoading ? styles.chatMessageStream : styles.chatEmptyState}> + {conversationIsLoading && } + +
+ +

Clew

+

+ Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver + actionable insights. +

+
) : ( -
- {conversationIsLoading && } - {dataConversation.length > 0 ? ( - dataConversation.map((item, index) => { - const response = { - answer: item.bot || "", - conversation_id: chatId, - data_points: [""], - thoughts: null - } as AskResponse - return( -
- -
- onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} - showFollowupQuestions={false} - showSources={true} - /> -
-
- ) - }) - ) : ( - answers.map((answer, index) => ( -
- -
- onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} - showFollowupQuestions={false} - showSources={true} - /> -
-
- )) - )} +
+ {conversationIsLoading && } + {dataConversation.length > 0 + ? dataConversation.map((item, index) => { + const response = { + answer: item.bot || "", + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse; + return ( +
+ +
+ onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} + showFollowupQuestions={false} + showSources={true} + /> +
+
+ ); + }) + : answers.map((answer, index) => ( +
+ +
+ onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} + showFollowupQuestions={false} + showSources={true} + /> +
+
+ ))} {isLoading && ( <> @@ -426,17 +354,24 @@ const Chat = () => { <>
- makeApiRequestGpt(lastQuestionRef.current, null)} /> + makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null)} + />
) : null}
- )}
- makeApiRequestGpt(question, chatId !== '' ? chatId : null)} /> + makeApiRequestGpt(question, chatId !== "" ? chatId : null)} + />
From ba77548d12ca7c7c04ebaff366a1bccf8b9365b4 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Thu, 4 Apr 2024 13:19:55 -0400 Subject: [PATCH 017/820] start new chat button created --- .../HistoryPannel/ChatHistoryListItem.tsx | 266 +++++++++--------- frontend/src/pages/chat/Chat.module.css | 28 ++ frontend/src/pages/chat/Chat.tsx | 28 +- frontend/src/providers/AppProviders.tsx | 87 +++--- 4 files changed, 241 insertions(+), 168 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 734895d0..6e18e2a0 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -2,160 +2,164 @@ import styles from "./ChatHistoryPannel.module.css"; import { getChatHistory, getChatFromHistoryPannelById } from "../../api"; import { useEffect, useState } from "react"; import { useAppContext } from "../../providers/AppProviders"; -import trash from "../../assets/trash.png" -import pencil from "../../assets/pencil.png" +import trash from "../../assets/trash.png"; +import pencil from "../../assets/pencil.png"; import { Spinner } from "@fluentui/react"; export const ChatHistoryPanelList = () => { + const [hoveredItemIndex, setHoveredItemIndex] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [conversationsIds, setConversationsIds] = useState([]); + const { + dataHistory, + setDataHistory, + userId, + dataConversation, + setDataConversation, + setConversationIsLoading, + setChatId, + chatId, + refreshFetchHistorial, + setRefreshFetchHistorial, + chatSelected, + setChatSelected + } = useAppContext(); + + const handleMouseEnter = (index: string) => { + setHoveredItemIndex(index); + }; - const [hoveredItemIndex, setHoveredItemIndex] = useState(null); - const [errorMessage, setErrorMessage] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [chatSelected, setChatSelected] = useState(""); - const [conversationsIds, setConversationsIds] = useState([]); - const {dataHistory, setDataHistory, userId, dataConversation, setDataConversation, setConversationIsLoading, setChatId, chatId, refreshFetchHistorial, setRefreshFetchHistorial } = useAppContext() - - - const handleMouseEnter = (index: string) => { - setHoveredItemIndex(index); - }; - - const handleMouseLeave = () => { - setHoveredItemIndex(null); - }; - + const handleMouseLeave = () => { + setHoveredItemIndex(null); + }; - const fetchData = async () => { + const fetchData = async () => { try { const data = await getChatHistory(userId); - if(data.length > 0){ - const sortedData = data.sort((a, b) => { - const dateA = new Date(a.start_date); - const dateB = new Date(b.start_date); - return dateB.getTime() - dateA.getTime(); - }); - setDataHistory(sortedData) - setIsLoading(false); - const ids = sortedData.map(data => (data.id)) - if(!ids.every(id => conversationsIds.includes(id))){ - setConversationsIds(ids) - } - }else{ - setIsLoading(false); - setErrorMessage("There are not conversation history yet.") + if (data.length > 0) { + const sortedData = data.sort((a, b) => { + const dateA = new Date(a.start_date); + const dateB = new Date(b.start_date); + return dateB.getTime() - dateA.getTime(); + }); + setDataHistory(sortedData); + setIsLoading(false); + const ids = sortedData.map(data => data.id); + if (!ids.every(id => conversationsIds.includes(id))) { + setConversationsIds(ids); + } + } else { + setIsLoading(false); + setErrorMessage("There are not conversation history yet."); } } catch (error) { - console.error('Error fetching data:', error); + console.error("Error fetching data:", error); setIsLoading(false); - setErrorMessage(`Was an error fetching data: ${error}`) + setErrorMessage(`Was an error fetching data: ${error}`); } }; - const fetchConversation = async (chatConversationId: string) => { + const fetchConversation = async (chatConversationId: string) => { try { - if(!chatSelected.includes(chatConversationId)){ - setChatSelected(chatConversationId) - setChatId(chatConversationId); - setConversationIsLoading(true) - const data = await getChatFromHistoryPannelById(chatConversationId, userId); - if(data.length > 0){ - setDataConversation(data) - setConversationIsLoading(false) + if (!chatSelected.includes(chatConversationId)) { + setChatSelected(chatConversationId); + setChatId(chatConversationId); + setConversationIsLoading(true); + const data = await getChatFromHistoryPannelById(chatConversationId, userId); + if (data.length > 0) { + setDataConversation(data); + setConversationIsLoading(false); + } } - } } catch (error) { - console.error('Error fetching data:', error); - setConversationIsLoading(false) - setErrorMessage(`Was an error fetching data: ${error}`) + console.error("Error fetching data:", error); + setConversationIsLoading(false); + setErrorMessage(`Was an error fetching data: ${error}`); } }; const handleRefreshHistoial = async () => { - if(refreshFetchHistorial){ - await fetchData() - setRefreshFetchHistorial(false) - }else{ - return - } - } - - useEffect(() => { - - if(dataHistory.length <= 0 || !conversationsIds.every(id => dataHistory.some(item => item.id === id))){ - fetchData(); - }else{ - setIsLoading(false) - } + if (refreshFetchHistorial) { + await fetchData(); + setRefreshFetchHistorial(false); + } else { + return; + } + }; - if(refreshFetchHistorial){ - handleRefreshHistoial(); - } + useEffect(() => { + if (dataHistory.length <= 0 || !conversationsIds.every(id => dataHistory.some(item => item.id === id))) { + fetchData(); + } else { + setIsLoading(false); + } + if (refreshFetchHistorial) { + handleRefreshHistoial(); + } }, [userId, dataHistory, conversationsIds, refreshFetchHistorial]); - const months = [ - "January", "February", "March", "April", - "May", "June", "July", "August", - "September", "October", "November", "December" - ]; - - const sortedDataByMonth = dataHistory.sort((a, b) => { - const monthA = new Date(a.start_date).getMonth(); - const monthB = new Date(b.start_date).getMonth(); - return monthA - monthB; - }); - - const sortedDataListByMonth = months.map(month => { - const monthData = sortedDataByMonth.filter(item => { - return new Date(item.start_date).getMonth() === months.indexOf(month); - }); - return { month, data: monthData }; - }); - - - return ( -
- {isLoading && ( -
- -
- )} - {errorMessage !== null ? ( -

{errorMessage}

- ) : ( - <> - {sortedDataListByMonth.map(({ month, data }, monthIndex) => ( -
- {data.length > 0 && ( - <> -

{month}

- {data.map((conversation, index) => ( -
handleMouseEnter(`${monthIndex}-${index}`)} - onMouseLeave={handleMouseLeave} - onClick={() => fetchConversation(conversation.id)} - > - - {hoveredItemIndex === `${monthIndex}-${index}` || chatSelected === conversation.id || chatId === conversation.id ? ( -
- Destroy - Edit -
- ):(<>)} -
+ const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + + const sortedDataByMonth = dataHistory.sort((a, b) => { + const monthA = new Date(a.start_date).getMonth(); + const monthB = new Date(b.start_date).getMonth(); + return monthA - monthB; + }); + + const sortedDataListByMonth = months.map(month => { + const monthData = sortedDataByMonth.filter(item => { + return new Date(item.start_date).getMonth() === months.indexOf(month); + }); + return { month, data: monthData }; + }); + + return ( +
+ {isLoading && ( +
+ +
+ )} + {errorMessage !== null ? ( +

{errorMessage}

+ ) : ( + <> + {sortedDataListByMonth.map(({ month, data }, monthIndex) => ( +
+ {data.length > 0 && ( + <> +

{month}

+ {data.map((conversation, index) => ( +
handleMouseEnter(`${monthIndex}-${index}`)} + onMouseLeave={handleMouseLeave} + onClick={() => fetchConversation(conversation.id)} + > + + {hoveredItemIndex === `${monthIndex}-${index}` || chatSelected === conversation.id || chatId === conversation.id ? ( +
+ Destroy + Edit +
+ ) : ( + <> + )} +
+ ))} + + )} +
))} - - )} -
- ))} - - )} -
+ + )} +
); - - -} \ No newline at end of file +}; diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 63dc75df..1356c952 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -119,6 +119,34 @@ width: 100%; max-width: 1028px; background: #ffffff; + display: flex; + align-items: center; +} + +.newChatButton{ + color: #ffffff; + font-size: 24px; + border: none; + padding: 8px 7px 1px; + border-radius: 5px; + margin-right: 10px; + cursor: pointer; + background: radial-gradient(109.81% 107.82% at 100.1% 90.19%, #0F6CBD 33.63%, #2D87C3 70.31%, #8DDDD8 100%); + transition: .3s; +} + +.newChatButton:hover { + color: #8DDDD8; +} + +.newChatButtonDisabled{ + color: #a3a3a3; + font-size: 24px; + border: none; + padding: 8px 7px 1px; + border-radius: 5px; + margin-right: 10px; + transition: .3s; } .chatAnalysisPanel { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index bc874fc8..86a1b632 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -1,6 +1,6 @@ import { useRef, useState, useEffect } from "react"; import { Checkbox, Panel, DefaultButton, TextField, SpinButton, Spinner } from "@fluentui/react"; -import { SparkleFilled, TabDesktopMultipleBottomRegular } from "@fluentui/react-icons"; +import { AddRegular, SparkleFilled, TabDesktopMultipleBottomRegular } from "@fluentui/react-icons"; import styles from "./Chat.module.css"; @@ -43,7 +43,8 @@ const Chat = () => { const chatContainerRef = useRef(null); - const { showHistoryPanel, dataConversation, setDataConversation, chatId, conversationIsLoading, setRefreshFetchHistorial, setChatId } = useAppContext(); + const { showHistoryPanel, dataConversation, setDataConversation, chatId, conversationIsLoading, setRefreshFetchHistorial, setChatId, setChatSelected } = + useAppContext(); const lastQuestionRef = useRef(""); const chatMessageStreamEnd = useRef(null); @@ -151,6 +152,22 @@ const Chat = () => { setUserId(""); }; + const handleNewChat = () => { + if (lastQuestionRef.current || dataConversation.length > 0) { + lastQuestionRef.current = ""; + error && setError(undefined); + setActiveCitation(undefined); + setActiveAnalysisPanelTab(undefined); + setAnswers([]); + setDataConversation([]); + setChatId(""); + setUserId(""); + setChatSelected(""); + } else { + return; + } + }; + /**Get Pdf */ const getPdf = async (pdfName: string) => { /** get file type */ @@ -364,8 +381,13 @@ const Chat = () => {
)} -
+ >; - refreshFetchHistorial: boolean; - setRefreshFetchHistorial: Dispatch>; - dataHistory: ConversationHistoryItem[]; - setDataHistory: Dispatch>; - userId: string; - setUserId: Dispatch>; - chatId: string; - setChatId: Dispatch>; - dataConversation: ChatTurn[]; - setDataConversation: Dispatch>; - conversationIsLoading: boolean; - setConversationIsLoading: Dispatch> + showHistoryPanel: boolean; + setShowHistoryPanel: Dispatch>; + refreshFetchHistorial: boolean; + setRefreshFetchHistorial: Dispatch>; + dataHistory: ConversationHistoryItem[]; + setDataHistory: Dispatch>; + userId: string; + setUserId: Dispatch>; + chatSelected: string; + setChatSelected: Dispatch>; + chatId: string; + setChatId: Dispatch>; + dataConversation: ChatTurn[]; + setDataConversation: Dispatch>; + conversationIsLoading: boolean; + setConversationIsLoading: Dispatch>; } const AppContext = createContext(undefined); export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [showHistoryPanel, setShowHistoryPanel] = useState(true); - const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); - const [dataHistory, setDataHistory] = useState([]); - const [dataConversation, setDataConversation] = useState([]); - const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); - const [chatId, setChatId] = useState(""); - const [conversationIsLoading, setConversationIsLoading] = useState(false); + const [showHistoryPanel, setShowHistoryPanel] = useState(true); + const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); + const [dataHistory, setDataHistory] = useState([]); + const [dataConversation, setDataConversation] = useState([]); + const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); + const [chatId, setChatId] = useState(""); + const [conversationIsLoading, setConversationIsLoading] = useState(false); + const [chatSelected, setChatSelected] = useState(""); - - - - return ( - - {children} - - ); + return ( + + {children} + + ); }; export const useAppContext = (): AppContextType => { - const context = useContext(AppContext); - if (!context) { - throw new Error("useAppContext must be used inside AppProvider"); - } - return context; + const context = useContext(AppContext); + if (!context) { + throw new Error("useAppContext must be used inside AppProvider"); + } + return context; }; From 57c09043dd28f11c5633562664761765b47164c8 Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Fri, 5 Apr 2024 07:31:40 -0400 Subject: [PATCH 018/820] SFC-57 clean conversation button working (#15) * SFC-57 clean conversation button working * console.logs deleted --- frontend/src/pages/chat/Chat.module.css | 31 ++++++++++++- frontend/src/pages/chat/Chat.tsx | 62 ++++++++++++++++++------- frontend/src/providers/AppProviders.tsx | 7 ++- 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 1356c952..0105fdbc 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -129,7 +129,6 @@ border: none; padding: 8px 7px 1px; border-radius: 5px; - margin-right: 10px; cursor: pointer; background: radial-gradient(109.81% 107.82% at 100.1% 90.19%, #0F6CBD 33.63%, #2D87C3 70.31%, #8DDDD8 100%); transition: .3s; @@ -145,10 +144,38 @@ border: none; padding: 8px 7px 1px; border-radius: 5px; - margin-right: 10px; transition: .3s; } +.clearChatButtonDisabled{ + color: #a3a3a3; + font-size: 24px; + border: none; + padding: 8px 7px 1px; + border-radius: 5px; + transition: .3s; + margin-bottom: 5px; +} + +.buttonsActions{ + margin-right: -10px; +} + +.clearChatButton { + color: #ffffff; + font-size: 24px; + border: none; + padding: 8px 7px 1px; + border-radius: 5px; + cursor: pointer; + background: radial-gradient(109.81% 107.82% at 100.1% 90.19%, #0F6CBD 33.63%, #2D87C3 70.31%, #8DDDD8 100%); + transition: .3s; + margin-bottom: 5px; +} + +.clearChatButton:hover { + color: #8DDDD8; +} .chatAnalysisPanel { flex: 1; overflow-y: auto; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 86a1b632..4f50f7fb 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -1,6 +1,6 @@ import { useRef, useState, useEffect } from "react"; import { Checkbox, Panel, DefaultButton, TextField, SpinButton, Spinner } from "@fluentui/react"; -import { AddRegular, SparkleFilled, TabDesktopMultipleBottomRegular } from "@fluentui/react-icons"; +import { AddRegular, BroomRegular, SparkleFilled, TabDesktopMultipleBottomRegular } from "@fluentui/react-icons"; import styles from "./Chat.module.css"; @@ -43,8 +43,18 @@ const Chat = () => { const chatContainerRef = useRef(null); - const { showHistoryPanel, dataConversation, setDataConversation, chatId, conversationIsLoading, setRefreshFetchHistorial, setChatId, setChatSelected } = - useAppContext(); + const { + showHistoryPanel, + dataConversation, + setDataConversation, + chatId, + conversationIsLoading, + setRefreshFetchHistorial, + setChatId, + setChatSelected, + setChatIsCleaned, + chatIsCleaned + } = useAppContext(); const lastQuestionRef = useRef(""); const chatMessageStreamEnd = useRef(null); @@ -143,17 +153,22 @@ const Chat = () => { }; const clearChat = () => { - console.log("file is" + fileType); - lastQuestionRef.current = ""; - error && setError(undefined); - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - setAnswers([]); - setUserId(""); + if (lastQuestionRef.current || dataConversation.length > 0 || !chatIsCleaned) { + console.log("file is" + fileType); + lastQuestionRef.current = ""; + error && setError(undefined); + setActiveCitation(undefined); + setActiveAnalysisPanelTab(undefined); + setAnswers([]); + setDataConversation([]); + setChatIsCleaned(true); + } else { + return; + } }; const handleNewChat = () => { - if (lastQuestionRef.current || dataConversation.length > 0) { + if (lastQuestionRef.current || dataConversation.length > 0 || chatIsCleaned) { lastQuestionRef.current = ""; error && setError(undefined); setActiveCitation(undefined); @@ -163,6 +178,7 @@ const Chat = () => { setChatId(""); setUserId(""); setChatSelected(""); + setChatIsCleaned(false); } else { return; } @@ -382,12 +398,24 @@ const Chat = () => {
)}
- +
+ + +
>; refreshFetchHistorial: boolean; setRefreshFetchHistorial: Dispatch>; + chatIsCleaned: boolean; + setChatIsCleaned: Dispatch>; dataHistory: ConversationHistoryItem[]; setDataHistory: Dispatch>; userId: string; @@ -29,6 +31,7 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); + const [chatIsCleaned, setChatIsCleaned] = useState(false); const [chatSelected, setChatSelected] = useState(""); return ( @@ -49,7 +52,9 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => refreshFetchHistorial, setRefreshFetchHistorial, chatSelected, - setChatSelected + setChatSelected, + chatIsCleaned, + setChatIsCleaned }} > {children} From f1deec02fd42701e266431855974089b95b34828 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Fri, 5 Apr 2024 08:28:59 -0400 Subject: [PATCH 019/820] SFC-70 change fuente to source --- frontend/src/components/Answer/Answer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Answer/Answer.tsx b/frontend/src/components/Answer/Answer.tsx index cb0a3156..60b728a0 100644 --- a/frontend/src/components/Answer/Answer.tsx +++ b/frontend/src/components/Answer/Answer.tsx @@ -75,7 +75,7 @@ export const Answer = ({ {!!parsedAnswer.citations.length && showSources && ( - Fuentes: + Sources: {parsedAnswer.citations.map((x, i) => { const path = getCitationFilePath(x); return ( From 848caa7cf8452d053aae36d6a844326986c07572 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Fri, 5 Apr 2024 14:46:25 -0400 Subject: [PATCH 020/820] endpoint fixed and a console.log deleted --- backend/app.py | 7 +++---- frontend/src/api/api.ts | 2 +- frontend/src/pages/chat/Chat.tsx | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/app.py b/backend/app.py index 454dc2aa..628047cb 100644 --- a/backend/app.py +++ b/backend/app.py @@ -112,16 +112,15 @@ def getChatHistory(): logging.exception("[webbackend] exception in /get-chat-history") return jsonify({"error": str(e)}), 500 -@app.route("/api/get-chat-conversation", methods=["GET"]) -def getChatConversation(): - chat_id = request.args.get('id') +@app.route("/api/get-chat-conversation/", methods=["GET"]) +def getChatConversation(chat_id): if chat_id is None: return jsonify({"error": "Missing chatId parameter"}), 400 client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') try: - keySecretName = 'orchestrator-host--functionKey' + keySecretName = 'orchestrator-host--conversations' functionKey = get_secret(keySecretName) except Exception as e: return jsonify({"error": f"Error getting function key: {e}"}), 500 diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 69043433..b2b85639 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -91,7 +91,7 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise { - const response = await fetch(`/api/get-chat-conversation?id=${chatId}`, { + const response = await fetch(`/api/get-chat-conversation/${chatId}`, { method: "GET", headers: { "Content-Type": "application/json", diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 4f50f7fb..40ea9a20 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -154,7 +154,6 @@ const Chat = () => { const clearChat = () => { if (lastQuestionRef.current || dataConversation.length > 0 || !chatIsCleaned) { - console.log("file is" + fileType); lastQuestionRef.current = ""; error && setError(undefined); setActiveCitation(undefined); From a6afbd7821eaffcdfc24a01b41382c9ff6b7b084 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:56:41 -0400 Subject: [PATCH 021/820] SFC-35 close the preview window (#16) --- .../AnalysisPanel/AnalysisPanel.tsx | 84 ++++++++++++++----- frontend/src/pages/chat/Chat.tsx | 5 ++ 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx index 00763b96..03f65ebe 100644 --- a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx +++ b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -6,6 +6,8 @@ import { SupportingContent } from "../SupportingContent"; import { AskResponse } from "../../api"; import { AnalysisPanelTabs } from "./AnalysisPanelTabs"; import { getPage, getFileType } from "../../utils/functions"; +import { DismissCircleFilled } from "@fluentui/react-icons"; +import { mergeStyles } from "@fluentui/react/lib/Styling"; const LazyViewer = lazy(() => import("../DocView/DocView")); @@ -17,11 +19,25 @@ interface Props { citationHeight: string; answer: AskResponse; fileType: string; + onHideTab: () => void; } const pivotItemDisabledStyle = { disabled: true, style: { color: "grey" } }; -export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeight, className, onActiveTabChanged, fileType }: Props) => { +const closeButtonStyle = { + style: { + backgroundColor: "transparent", + color: "black", + borderColor: "transparent", + padding: "0px", + position: "absolute", + right: "0px", + top: "0px", + cursor: "pointer" + } +}; + +export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeight, className, onActiveTabChanged, fileType, onHideTab }: Props) => { const isDisabledThoughtProcessTab: boolean = !answer.thoughts; const isDisabledSupportingContentTab: boolean = !answer.data_points.length; const isDisabledCitationTab: boolean = !activeCitation; @@ -30,28 +46,52 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh const sanitizedThoughts = DOMPurify.sanitize(answer.thoughts!); return ( - pivotItem && onActiveTabChanged(pivotItem.props.itemKey! as AnalysisPanelTabs)} - > - + pivotItem && onActiveTabChanged(pivotItem.props.itemKey! as AnalysisPanelTabs)} > -
-
+ +
+
- - Cargando...

}> - -
-
-
+ + Cargando...

}> + +
+
+ ( +
+ +
+ )} + /> + + ); }; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 40ea9a20..99686500 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -304,6 +304,10 @@ const Chat = () => { setSelectedAnswer(index); }; + const hideTab = () => { + setActiveAnalysisPanelTab(undefined); + }; + return (
@@ -433,6 +437,7 @@ const Chat = () => { answer={answers[selectedAnswer][1]} activeTab={activeAnalysisPanelTab} fileType={fileType} + onHideTab={hideTab} /> )} From 221ddace012a51888cf266fb3d739cfaf0d05a02 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:38:26 -0400 Subject: [PATCH 022/820] SFC-69 migrate settings to a panel (#20) --- .../SettingsButton/SettingsButton.module.css | 23 +++ .../SettingsButton/SettingsButton.tsx | 15 +- .../SettingsModal.module.css | 38 ++++- .../index.tsx | 161 +++++++++--------- frontend/src/pages/chat/Chat.tsx | 7 +- frontend/src/pages/layout/Layout.tsx | 20 ++- frontend/src/providers/AppProviders.tsx | 19 ++- 7 files changed, 181 insertions(+), 102 deletions(-) rename frontend/src/components/{SettingsModal => SettingsPanel}/SettingsModal.module.css (72%) rename frontend/src/components/{SettingsModal => SettingsPanel}/index.tsx (56%) diff --git a/frontend/src/components/SettingsButton/SettingsButton.module.css b/frontend/src/components/SettingsButton/SettingsButton.module.css index fb533bfd..2138412e 100644 --- a/frontend/src/components/SettingsButton/SettingsButton.module.css +++ b/frontend/src/components/SettingsButton/SettingsButton.module.css @@ -4,3 +4,26 @@ gap: 6px; cursor: pointer; } + +.button { + display: flex; + align-items: center; + margin-top: 5px; + gap: 6px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + border-radius: 5px; + padding: 4px 12px; + transition: .2s; + background-color: white; + font-size: 24px; + height: 35px; +} + +.button:hover { + background-color: rgb(243, 243, 243); +} + +.button:active { + background-color: white; +} \ No newline at end of file diff --git a/frontend/src/components/SettingsButton/SettingsButton.tsx b/frontend/src/components/SettingsButton/SettingsButton.tsx index eacf03ce..e0210f21 100644 --- a/frontend/src/components/SettingsButton/SettingsButton.tsx +++ b/frontend/src/components/SettingsButton/SettingsButton.tsx @@ -1,18 +1,17 @@ -import { Text } from "@fluentui/react"; -import { Settings24Regular } from "@fluentui/react-icons"; +import { Text, DefaultButton } from "@fluentui/react"; +import { SettingsFilled } from "@fluentui/react-icons"; import styles from "./SettingsButton.module.css"; interface Props { - className?: string; onClick: () => void; } -export const SettingsButton = ({ className, onClick }: Props) => { +export const SettingsButton = ({ onClick }: Props) => { return ( -
- - {"Debug"} -
+ + + Settings + ); }; diff --git a/frontend/src/components/SettingsModal/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css similarity index 72% rename from frontend/src/components/SettingsModal/SettingsModal.module.css rename to frontend/src/components/SettingsPanel/SettingsModal.module.css index 13df7878..12cb9f9e 100644 --- a/frontend/src/components/SettingsModal/SettingsModal.module.css +++ b/frontend/src/components/SettingsPanel/SettingsModal.module.css @@ -32,7 +32,7 @@ } .answerContainer { - padding: 40px; + padding: 10px 40px 40px 40px; background: rgb(249, 249, 249); box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); outline: transparent solid 1px; @@ -77,7 +77,7 @@ border: none; } -.header { +.header2 { padding: 10px; display: flex; } @@ -98,4 +98,38 @@ .w-100 { width: 100%; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; +} + +.title { + font-weight: 600; + font-size: 24px; +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +.closeButton2{ + border: none; + font-size: 25px; + color: #5da3cc; + background-color: transparent; + cursor: pointer; + transform: rotate(45deg); + margin-top: -10px; + padding-left: 30px; } \ No newline at end of file diff --git a/frontend/src/components/SettingsModal/index.tsx b/frontend/src/components/SettingsPanel/index.tsx similarity index 56% rename from frontend/src/components/SettingsModal/index.tsx rename to frontend/src/components/SettingsPanel/index.tsx index f212f88f..d1f403b9 100644 --- a/frontend/src/components/SettingsModal/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -1,10 +1,11 @@ import React, { useState, useEffect, useCallback } from "react"; -import { SettingsFilled, SaveFilled, ErrorCircleFilled } from "@fluentui/react-icons"; +import { AddFilled, SaveFilled, ErrorCircleFilled } from "@fluentui/react-icons"; import { Checkbox } from "@fluentui/react/lib/Checkbox"; import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Modal, Stack, Text, Spinner, Slider } from "@fluentui/react"; import styles from "./SettingsModal.module.css"; import { getSettings, postSettings } from "../../api/api"; import { mergeStyles } from "@fluentui/react/lib/Styling"; +import { useAppContext } from "../../providers/AppProviders"; interface Props { user: { @@ -29,8 +30,14 @@ const itemClass = mergeStyles({ padding: "10px 0" }); -const SettingsModal = ({ user }: Props) => { - const [isModalOpen, setIsModalOpen] = useState(false); +export const SettingsPanel = () => { + const { userId, userName, setSettingsPanel } = useAppContext(); + + const user = { + id: userId, + name: userName + }; + const [temperature, setTemperature] = useState("0"); const [presencePenalty, setPresencePenalty] = useState("0"); const [frequencyPenalty, setFrequencyPenalty] = useState("0"); @@ -57,15 +64,7 @@ const SettingsModal = ({ user }: Props) => { setLoading(true); fetchData(); - }, [user]); - - const showModal = () => { - setIsModalOpen(true); - }; - - const hideModal = () => { - setIsModalOpen(false); - }; + }, []); const handleSubmit = () => { if ((!temperature && temperature != "0") || (!presencePenalty && presencePenalty != "0") || (!frequencyPenalty && frequencyPenalty != "0")) { @@ -83,8 +82,6 @@ const SettingsModal = ({ user }: Props) => { presence_penalty: parsedPresencePenalty, frequency_penalty: parsedFrequencyPenalty }); - - hideModal(); }; const validateValue = (val: any, func: any) => { @@ -135,81 +132,81 @@ const SettingsModal = ({ user }: Props) => { ); + const handleClosePanel = () => { + setSettingsPanel(false); + }; + return (
- - - Settings - - - - - -
-

Adjust your settings

- - ✖ - -
- {loading ? ( -
- -

Loading your settings

+ + +
+
Configuration
+
+
+
+
- ) : ( +
+
+ {loading ? ( +
+ +

Loading your settings

+
+ ) : ( +
-
-
- Creativity Scale - {onRenderLabel(temperatureDialog, "Temperature")} -
- handleSetTemperature(e)} - /> -
- Variety Boost - onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")} - /> + Creativity Scale + {onRenderLabel(temperatureDialog, "Temperature")}
-
- Topic Explorer - onRenderLabel(presencePenaltyDialog, "Presence Penalty")} - /> -
- - -   Save - + handleSetTemperature(e)} + />
- )} - - - +
+ Variety Boost + onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")} + /> +
+
+ Topic Explorer + onRenderLabel(presencePenaltyDialog, "Presence Penalty")} + /> +
+ + +   Save + +
+ )} +
+
); }; - -export default SettingsModal; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 99686500..52d7a5cd 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -17,6 +17,7 @@ import { getFileType } from "../../utils/functions"; import salesLogo from "../../img/logo.png"; import { useAppContext } from "../../providers/AppProviders"; import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; +import { SettingsPanel } from "../../components/SettingsPanel"; const userLanguage = navigator.language; let error_message_text = ""; @@ -53,7 +54,8 @@ const Chat = () => { setChatId, setChatSelected, setChatIsCleaned, - chatIsCleaned + chatIsCleaned, + settingsPanel, } = useAppContext(); const lastQuestionRef = useRef(""); @@ -313,6 +315,9 @@ const Chat = () => {
{showHistoryPanel && }
+
+
{settingsPanel && }
+
diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index c095349a..5c2b7d5a 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -7,16 +7,20 @@ import github from "../../assets/github.svg"; import styles from "./Layout.module.css"; import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; import { useAppContext } from "../../providers/AppProviders"; - -import SettingsModal from "../../components/SettingsModal"; +import { SettingsButton } from "../../components/SettingsButton"; const Layout = () => { - - const {showHistoryPanel, setShowHistoryPanel} = useAppContext() + const { showHistoryPanel, setShowHistoryPanel, settingsPanel, setSettingsPanel } = useAppContext(); const handleShowHistoryPanel = () => { - setShowHistoryPanel(!showHistoryPanel) - } + setShowHistoryPanel(!showHistoryPanel); + setSettingsPanel(false); + }; + + const handleShowSettings = () => { + setSettingsPanel(!settingsPanel); + setShowHistoryPanel(false); + }; return (
@@ -56,8 +60,8 @@ const Layout = () => {
{/* needs an user to be sent ↓ */} - - + +
diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 15e2d308..219fdef0 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,5 +1,12 @@ import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; + +interface SettingsType { + temperature: string; + presencePenalty: string; + frequencyPenalty: string; +} + interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; @@ -11,6 +18,8 @@ interface AppContextType { setDataHistory: Dispatch>; userId: string; setUserId: Dispatch>; + userName: string; + setUserName: Dispatch>; chatSelected: string; setChatSelected: Dispatch>; chatId: string; @@ -19,6 +28,8 @@ interface AppContextType { setDataConversation: Dispatch>; conversationIsLoading: boolean; setConversationIsLoading: Dispatch>; + settingsPanel: boolean; + setSettingsPanel: Dispatch>; } const AppContext = createContext(undefined); @@ -29,10 +40,12 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [dataHistory, setDataHistory] = useState([]); const [dataConversation, setDataConversation] = useState([]); const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); + const [userName, setUserName] = useState("anonymous"); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); const [chatIsCleaned, setChatIsCleaned] = useState(false); const [chatSelected, setChatSelected] = useState(""); + const [settingsPanel, setSettingsPanel] = useState(false); return ( = ({ children }) => setDataHistory, userId, setUserId, + userName, + setUserName, dataConversation, setDataConversation, chatId, @@ -54,7 +69,9 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => chatSelected, setChatSelected, chatIsCleaned, - setChatIsCleaned + setChatIsCleaned, + settingsPanel, + setSettingsPanel }} > {children} From dec977f87d630dc1ec85bed9f962381f004d1799 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:51:50 -0400 Subject: [PATCH 023/820] Sfc 76 77 78 feedback (#19) * SFC-76-77-78 feedback module * undo key change --- backend/app.py | 51 ++++++ frontend/src/api/api.ts | 39 ++++ .../FeedbackRating/FeedbackRating.module.css | 167 ++++++++++++++++++ .../FeedbackRating/FeedbackRating.tsx | 142 +++++++++++++++ .../FeedbackRatingButton.module.css | 35 ++++ .../FeedbackRating/FeedbackRatingButton.tsx | 22 +++ .../HistoryPannel/ChatHistoryListItem.tsx | 2 +- frontend/src/pages/chat/Chat.tsx | 5 + frontend/src/pages/layout/Layout.tsx | 13 +- frontend/src/providers/AppProviders.tsx | 5 + 10 files changed, 478 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/FeedbackRating/FeedbackRating.module.css create mode 100644 frontend/src/components/FeedbackRating/FeedbackRating.tsx create mode 100644 frontend/src/components/FeedbackRating/FeedbackRatingButton.module.css create mode 100644 frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx diff --git a/backend/app.py b/backend/app.py index 628047cb..071aaa2d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -18,6 +18,7 @@ ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI', default="") SETTINGS_ENDPOINT = ORCHESTRATOR_URI + "/settings" +FEEDBACK_ENDPOINT = ORCHESTRATOR_URI + "/feedback" HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/conversations" STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper() @@ -266,5 +267,55 @@ def setSettings(): logging.exception("[webbackend] exception in /api/settings") return jsonify({"error": str(e)}), 500 + +@app.route("/api/feedback", methods=["POST"]) +def setFeedback(): + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + + if not client_principal_id or not client_principal_name: + return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + + conversation_id = request.json["conversation_id"] + question = request.json["question"] + answer = request.json["answer"] + category = request.json["category"] + feedback = request.json["feedback"] + rating = request.json["rating"] + + if not conversation_id or not question or not answer or not category: + return jsonify({"error": "Missing required parameters, temperature, presence_penalty or frequency_penalty"}), 400 + + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = 'orchestrator-host--feedback' + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--feedback") + return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + + try: + url = FEEDBACK_ENDPOINT + payload = json.dumps({ + "conversation_id": conversation_id, + "question": question, + "answer": answer, + "category": category, + "feedback": feedback, + "rating": rating, + }) + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + response = requests.request("POST", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return(response.text) + except Exception as e: + logging.exception("[webbackend] exception in /api/feedback") + return jsonify({"error": str(e)}), 500 + + if __name__ == "__main__": app.run(host='0.0.0.0', port=8000) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index b2b85639..41b80fff 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -160,3 +160,42 @@ export function getCitationFilePath(citation: string): string { return `https://${storage_account}.blob.core.windows.net/documents/${citation}`; } + +export async function postFeedbackRating({ + user, + conversation_id, + feedback_message, + question, + answer, + rating, + category, + }: any): Promise { + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; + return new Promise(async (resolve, reject) => { + try { + const response = await fetch("/api/feedback", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name + }, + body: JSON.stringify({ + conversation_id: conversation_id, + feedback: feedback_message, + question: question, + answer:answer, + rating: rating, + category: category + }) + }); + + const fetchedData = await response.json(); + resolve(fetchedData); + } catch (error) { + console.error("Error posting feedback", error); + reject(error); + } + }); +} \ No newline at end of file diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css new file mode 100644 index 00000000..43f1080a --- /dev/null +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -0,0 +1,167 @@ + +.clearButton{ + border: none; + font-size: 25px; + color: #5da3cc; + background-color: transparent; + cursor: pointer; + padding: 10.2px 7.5px; + margin-top: -15px; +} + +.closeButton{ + border: none; + font-size: 25px; + color: #5da3cc; + background-color: transparent; + cursor: pointer; + transform: rotate(45deg); + margin-bottom: -5px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +.container { + margin-right: 10px; + margin-top: 5px; + width: 250px; + padding: 0 10px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + position: sticky; +} + +.title { + font-weight: 600; + font-size: 18px; +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} + +.content { + display: flex; + flex-direction: column; + max-width: 100%; +} + +.buttonConversation{ + border: none; + padding: 10px; + text-align: left; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + background-color: transparent; + cursor: pointer; +} + + +.actionsButtons{ + display: flex; +} + +.conversationContainer{ + display: flex; + border-radius: 5px; + margin-bottom: 4px; +} + +.conversationSelected{ + background-color: rgba(221, 221, 221, 0.349); + display: flex; + border-radius: 5px; + margin-bottom: 4px; + transition: .2s; +} + +.conversationContainer:hover{ + background-color: rgb(228, 228, 228); +} + +.actionButton{ + height: 28px; + justify-content: center; + align-items: center; + margin: auto; + background-color: white; + padding: 3px; + border-radius: 5px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + margin-right: 5px; + transition: .3s; +} + +.actionButton:hover{ + border: 1.5px solid rgb(212, 212, 212); + box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.34) inset; +} + +.loaderContainer{ + margin-top: 10px; + display: flex; + justify-content: center; + align-items: center; +} + +.customLoader { + width: 25px; + height: 25px; + border-radius: 50%; + border: 3px solid; + border-color: #E4E4ED; + border-right-color: #5da3cc; + animation: s2 1s infinite linear; +} + +@keyframes s2 { + to { + transform: rotate(1turn) + } +} + +.saveButton { + background: rgb(0, 0, 0); + color: white; + border-radius: 8px; + padding: 10px; + font-size: 20px; + width: 100%; + border: none; + cursor: pointer; + margin-top: 20px; +} + +.saveButton:hover { + background: rgb(0, 0, 0, 0.8); + color: white; +} + +.rating { + display: flex; + justify-content: space-around; + align-items: center; + margin-top: 10px; + font-size: 40px; + cursor: pointer; +} + +.error { + text-align: center; + font-weight: 400; + font-style: italic; + color: #ff3333; +} \ No newline at end of file diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx new file mode 100644 index 00000000..97c1f29f --- /dev/null +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -0,0 +1,142 @@ +import React, { useState } from "react"; +import { Dropdown, TextField, Button, Spinner, DefaultButton } from "@fluentui/react"; +import styles from "./FeedbackRating.module.css"; +import { useAppContext } from "../../providers/AppProviders"; +import { AddFilled, SaveFilled, ThumbLikeFilled, ThumbDislikeFilled } from "@fluentui/react-icons"; +import { ThumbLikeRegular, ThumbDislikeRegular } from "@fluentui/react-icons"; +import { postFeedbackRating } from "../../api/api"; + +const categoryOptions = [ + { key: "1", text: "Incorrect data" }, + { key: "2", text: "Lack of sources" }, + { key: "3", text: "Lack of context" }, + { key: "4", text: "Redundant information" }, + { key: "5", text: "Other" } +]; + +export const FeedbackRating = () => { + const { showFeedbackRatingPanel, setShowFeedbackRatingPanel, dataConversation, chatId, userId } = useAppContext(); + + const [category, setCategory] = useState(""); + const [feedback, setFeedback] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [selectedThumb, setSelectedThumb] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const handleCategoryChange = (event: any, selectedOption: any) => { + setErrorMessage(""); + setCategory(selectedOption.text); + }; + + const handleFeedbackChange = (event: any) => { + setErrorMessage(""); + setFeedback(event.target.value); + }; + + const handleClosePannel = () => { + setShowFeedbackRatingPanel(!showFeedbackRatingPanel); + }; + + const handleSubmitFeedback = async () => { + setIsLoading(true); + + // if no category is selected + if (category === "") { + setErrorMessage("Please select a category"); + setIsLoading(false); + return; + } + + // if no feedback message is provided + if (feedback === "") { + setErrorMessage("Please provide a message"); + setIsLoading(false); + return; + } + + // if not set chat id or dataConversation is empty + if (chatId === "" || dataConversation?.length === 0) { + setErrorMessage("Please select a chat or send a message first"); + setIsLoading(false); + return; + } + + await postFeedbackRating({ + user: { + id: userId, + name: "anonymous" // set name in provider + }, + conversation_id: chatId, + feedback_message: feedback, + question: dataConversation[0].user, + answer: dataConversation[0].bot, + rating: selectedThumb === "like" ? true : selectedThumb === "dislike" ? false : null, + category: category + }) + .then(() => { + setIsLoading(false); + // setCategory(""); + setFeedback(""); + setSelectedThumb(""); + setErrorMessage(""); + }) + .catch(error => { + setErrorMessage("Error sending feedback"); + setIsLoading(false); + }); + }; + + const handleSelectedThumb = (thumb: string) => { + if (selectedThumb === thumb) { + setSelectedThumb(""); + return; + } + setSelectedThumb(thumb); + }; + + return ( +
+
+
Feedback
+
+
+ +
+
+
+
+
+ {isLoading && ( +
+ +
+ )} + + + +
+ {selectedThumb === "like" ? ( + handleSelectedThumb("like")} /> + ) : ( + handleSelectedThumb("like")} /> + )} + {selectedThumb === "dislike" ? ( + handleSelectedThumb("dislike")} /> + ) : ( + handleSelectedThumb("dislike")} /> + )} +
+ + +   Send + +
+ {errorMessage !== null &&

{errorMessage}

} +
+
+ ); +}; + +export default FeedbackRating; diff --git a/frontend/src/components/FeedbackRating/FeedbackRatingButton.module.css b/frontend/src/components/FeedbackRating/FeedbackRatingButton.module.css new file mode 100644 index 00000000..9e24ba76 --- /dev/null +++ b/frontend/src/components/FeedbackRating/FeedbackRatingButton.module.css @@ -0,0 +1,35 @@ +.container { + display: flex; + align-items: center; + margin-top: 5px; + gap: 6px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + border-radius: 5px; + padding: 4px 12px; + transition: .2s; + background-color: white; + width: 200px; + margin-right: 5px; +} + +.container:hover{ + background-color: rgb(243, 243, 243); +} + +.container:active{ + background-color: white; +} + +.button{ + color: rgb(63, 63, 63); + font-size: 24px; +} + +.buttonText{ + font-weight: 500; +} + +.disabled { + opacity: 0.4; +} diff --git a/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx b/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx new file mode 100644 index 00000000..f7dd917c --- /dev/null +++ b/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx @@ -0,0 +1,22 @@ +import { Text } from "@fluentui/react"; +import { ChatBubblesQuestionFilled } from "@fluentui/react-icons"; + +import styles from "./FeedbackRatingButton.module.css"; +import { useAppContext } from "../../providers/AppProviders"; + +interface Props { + className?: string; + onClick: () => void; + disabled?: boolean; +} + +export const FeedbackRatingButton = ({ className, disabled, onClick }: Props) => { + const { showFeedbackRatingPanel } = useAppContext(); + const buttonContent = showFeedbackRatingPanel ? "Hide feedback panel" : "Show feedback panel"; + return ( + + ); +}; diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 6e18e2a0..b67cbc39 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -56,7 +56,7 @@ export const ChatHistoryPanelList = () => { } catch (error) { console.error("Error fetching data:", error); setIsLoading(false); - setErrorMessage(`Was an error fetching data: ${error}`); + setErrorMessage(`No history found`); } }; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 52d7a5cd..a407ddb4 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -17,6 +17,7 @@ import { getFileType } from "../../utils/functions"; import salesLogo from "../../img/logo.png"; import { useAppContext } from "../../providers/AppProviders"; import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; +import { FeedbackRating } from "../../components/FeedbackRating/FeedbackRating"; import { SettingsPanel } from "../../components/SettingsPanel"; const userLanguage = navigator.language; @@ -46,6 +47,7 @@ const Chat = () => { const { showHistoryPanel, + showFeedbackRatingPanel, dataConversation, setDataConversation, chatId, @@ -315,6 +317,9 @@ const Chat = () => {
{showHistoryPanel && }
+
+
{showFeedbackRatingPanel && }
+
{settingsPanel && }
diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 5c2b7d5a..2a09465b 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -6,20 +6,29 @@ import github from "../../assets/github.svg"; import styles from "./Layout.module.css"; import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; +import { FeedbackRatingButton } from "../../components/FeedbackRating/FeedbackRatingButton"; import { useAppContext } from "../../providers/AppProviders"; import { SettingsButton } from "../../components/SettingsButton"; const Layout = () => { - const { showHistoryPanel, setShowHistoryPanel, settingsPanel, setSettingsPanel } = useAppContext(); + const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); const handleShowHistoryPanel = () => { setShowHistoryPanel(!showHistoryPanel); + setShowFeedbackRatingPanel(false); setSettingsPanel(false); }; + const handleShowFeedbackRatingPanel = () => { + setShowFeedbackRatingPanel(!showFeedbackRatingPanel); + setSettingsPanel(false); + setShowHistoryPanel(false); + }; + const handleShowSettings = () => { setSettingsPanel(!settingsPanel); setShowHistoryPanel(false); + setShowFeedbackRatingPanel(false); }; return ( @@ -59,7 +68,7 @@ const Layout = () => { */}
- {/* needs an user to be sent ↓ */} +
diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 219fdef0..33c46099 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -10,6 +10,8 @@ interface SettingsType { interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; + showFeedbackRatingPanel: boolean; + setShowFeedbackRatingPanel: Dispatch>; refreshFetchHistorial: boolean; setRefreshFetchHistorial: Dispatch>; chatIsCleaned: boolean; @@ -36,6 +38,7 @@ const AppContext = createContext(undefined); export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [showHistoryPanel, setShowHistoryPanel] = useState(true); + const [showFeedbackRatingPanel, setShowFeedbackRatingPanel] = useState(false); const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); const [dataHistory, setDataHistory] = useState([]); const [dataConversation, setDataConversation] = useState([]); @@ -52,6 +55,8 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => value={{ showHistoryPanel, setShowHistoryPanel, + showFeedbackRatingPanel, + setShowFeedbackRatingPanel, dataHistory, setDataHistory, userId, From bb88d55ffc05d5cdb92c5ef0044e03814336c9c0 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Fri, 12 Apr 2024 09:29:58 -0400 Subject: [PATCH 024/820] Delete conversation working --- backend/app.py | 22 +++++ frontend/src/api/api.ts | 19 ++++ frontend/src/assets/check.png | Bin 0 -> 467 bytes frontend/src/assets/close.png | Bin 0 -> 548 bytes .../HistoryPannel/ChatHistoryListItem.tsx | 83 +++++++++++++++--- .../HistoryPannel/ChatHistoryPanel.tsx | 31 ++++--- .../ChatHistoryPannel.module.css | 13 +++ frontend/src/pages/chat/Chat.tsx | 4 +- frontend/src/providers/AppProviders.tsx | 7 +- 9 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 frontend/src/assets/check.png create mode 100644 frontend/src/assets/close.png diff --git a/backend/app.py b/backend/app.py index 071aaa2d..7acf5db5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -143,6 +143,28 @@ def getChatConversation(chat_id): logging.exception("[webbackend] exception in /get-chat-history") return jsonify({"error": str(e)}), 500 +@app.route("/api/delete-chat-conversation/", methods=["DELETE"]) +def deleteChatConversation(chat_id): + try: + client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + keySecretName = 'orchestrator-host--conversations' + functionKey = get_secret(keySecretName) + + url = f"{HISTORY_ENDPOINT}/?id={chat_id}" + headers = { + 'Content-Type': 'application/json', + 'x-functions-key': functionKey + } + payload = json.dumps({ + "user_id": client_principal_id + }) + + response = requests.delete(url, headers=headers, data=payload) + return response.text, response.status_code + except Exception as e: + logging.exception("[webbackend] exception in /delete-chat-conversation") + return jsonify({"error": str(e)}), 500 + # methods to provide access to speech services and blob storage account blobs @app.route("/api/get-speech-token", methods=["GET"]) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 41b80fff..20eddf3c 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -126,6 +126,25 @@ export async function getChatFromHistoryPannelById(chatId: string, userId: strin return conversationItems; } +export async function deleteChatConversation(chatId: string, userId: string): Promise { + try { + const response = await fetch(`/api/delete-chat-conversation/${chatId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-MS-CLIENT-PRINCIPAL-ID': userId + } + }); + if (!response.ok) { + throw new Error(`Failed to delete conversation. Status: ${response.status}`); + } + } catch (error) { + console.error('Error deleting conversation:', error); + throw new Error('Error deleting conversation'); + } +} + + export async function getChatHistory(userId: string): Promise { const response = await fetch("/api/get-chat-history", { diff --git a/frontend/src/assets/check.png b/frontend/src/assets/check.png new file mode 100644 index 0000000000000000000000000000000000000000..da826287edc442bbd591b99b93fe09bef7990c31 GIT binary patch literal 467 zcmV;^0WAKBP)UqK~zYI?UKDt0zni;&z(Um2wK@$nk?$Ny%d^gXG5%g0b?gV zgq5t%U}<4vf;RSABJyJ_Eop37cexfM1PIH5teszXXENv9nLF^Ggh<*zuSV|MnzE`{ z)uphKs$k=Tl}unCQFZ}Po^7h|u6+QCgGEE${23e&)vJY{$g7bHjIAQQ zf+RRB@QY@F1Blbe9EkRORKAR{XU*-Pkpz!>bC62g4KGGk3UBrkSnpSKi0on83+idW z0gQqUDrF5DJr!QFJ88fHj4_-7L6XF2laGP>ac}_B+KQ@TI0m}o5~rT^pg&NF?EN@g zk?Y}sPRigHZ8q(Z_P7Ir zI$1Mag9AutQs?RDKi~j6YBY7gUdv3h!~}a#_ zYnVO@!o{29^835nVm-{pGEO}rGvmeJvYM>mW|{`!v+5O_4KWhuY#9r`Bj}O{%sg{# zaBpMBpR}h1?q5k?i3c7OL`wp202i8d*iXQGbpx#7N7(O4=TThqtRLYhK@WJRmb zBG0aX{l4%>$&~|^+rj|`<^0sJ*PimyNje6uw}E#^uGP5Xznh$Aj|@HnyTCXptMgd+ mdKP*1QcxJwg3)eSefa}%U;5IbRN}S(0000 { +interface ChatHistoryPanelProps { + onDeleteChat: () => void; +} + +export const ChatHistoryPanelList: React.FC = ({ onDeleteChat }) => { const [hoveredItemIndex, setHoveredItemIndex] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [deletingIsLoading, setDeletingIsLoading] = useState(false); + const [confirmationDelete, setConfirmationDelete] = useState(null); const [conversationsIds, setConversationsIds] = useState([]); const { dataHistory, @@ -23,7 +31,8 @@ export const ChatHistoryPanelList = () => { refreshFetchHistorial, setRefreshFetchHistorial, chatSelected, - setChatSelected + setChatSelected, + setNewChatDeleted } = useAppContext(); const handleMouseEnter = (index: string) => { @@ -51,7 +60,7 @@ export const ChatHistoryPanelList = () => { } } else { setIsLoading(false); - setErrorMessage("There are not conversation history yet."); + setErrorMessage("There are not conversations yet."); } } catch (error) { console.error("Error fetching data:", error); @@ -79,6 +88,27 @@ export const ChatHistoryPanelList = () => { } }; + const handleDeleteConversation = async (chatConversationId: string) => { + try { + setDeletingIsLoading(true); + const data = await deleteChatConversation(chatConversationId, userId); + setDeletingIsLoading(false); + console.log("desde eliminar", chatId); + if (chatSelected === chatConversationId) { + setDataConversation([]); + } + if (chatId === chatConversationId) { + onDeleteChat(); + } + const updatedDataHistory = dataHistory.filter(item => item.id !== chatConversationId); + setDataHistory(updatedDataHistory); + } catch (error) { + console.error("Error deleting conversation:", error); + setDeletingIsLoading(false); + setErrorMessage(`Was an error deleting conversation: ${error}`); + } + }; + const handleRefreshHistoial = async () => { if (refreshFetchHistorial) { await fetchData(); @@ -89,7 +119,7 @@ export const ChatHistoryPanelList = () => { }; useEffect(() => { - if (dataHistory.length <= 0 || !conversationsIds.every(id => dataHistory.some(item => item.id === id))) { + if (dataHistory.length <= 0) { fetchData(); } else { setIsLoading(false); @@ -135,19 +165,50 @@ export const ChatHistoryPanelList = () => {
handleMouseEnter(`${monthIndex}-${index}`)} onMouseLeave={handleMouseLeave} - onClick={() => fetchConversation(conversation.id)} > - - {hoveredItemIndex === `${monthIndex}-${index}` || chatSelected === conversation.id || chatId === conversation.id ? ( + + {hoveredItemIndex === `${monthIndex}-${index}` || + chatSelected === conversation.id || + chatId === conversation.id || + confirmationDelete === conversation.id ? (
- Destroy - Edit + Destroy setConfirmationDelete(null) + : () => setConfirmationDelete(conversation.id) + } + /> + {deletingIsLoading && confirmationDelete === conversation.id ? ( + + ) : ( + Edit handleDeleteConversation(conversation.id) + : () => setConfirmationDelete(null) + } + /> + )}
) : ( <> diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index 81152df0..c71c9c46 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -1,32 +1,39 @@ -import { AddFilled } from "@fluentui/react-icons" +import { AddFilled } from "@fluentui/react-icons"; import styles from "./ChatHistoryPannel.module.css"; import { useAppContext } from "../../providers/AppProviders"; import { ChatHistoryPanelList } from "./ChatHistoryListItem"; -export const ChatHistoryPanel = () => { - const { showHistoryPanel, setShowHistoryPanel} = useAppContext() +interface ChatHistoryPanelProps { + functionDeleteChat: () => void; +} + +export const ChatHistoryPanel: React.FC = ({ functionDeleteChat }) => { + const { showHistoryPanel, setShowHistoryPanel } = useAppContext(); const handleClosePannel = () => { - setShowHistoryPanel(!showHistoryPanel) - } + setShowHistoryPanel(!showHistoryPanel); + }; return (
Chat history
- +
- -
+ +
- +
- - ) -} \ No newline at end of file + ); +}; diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 28bd9de2..102924fa 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -67,9 +67,21 @@ cursor: pointer; } +.buttonConversationSelected{ + border: none; + padding: 10px; + text-align: left; + width: 100%; + background-color: transparent; + cursor: pointer; + font-weight: bold; +} .actionsButtons{ display: flex; + flex-direction: row; + justify-self: center; + justify-content: center; } .conversationContainer{ @@ -102,6 +114,7 @@ border: 1.5px solid rgb(231, 231, 231); margin-right: 5px; transition: .3s; + width: 28px; } .actionButton:hover{ diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index a407ddb4..7712a984 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -57,7 +57,7 @@ const Chat = () => { setChatSelected, setChatIsCleaned, chatIsCleaned, - settingsPanel, + settingsPanel } = useAppContext(); const lastQuestionRef = useRef(""); @@ -315,7 +315,7 @@ const Chat = () => { return (
-
{showHistoryPanel && }
+
{showHistoryPanel && }
{showFeedbackRatingPanel && }
diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 33c46099..3362eb8e 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -10,6 +10,8 @@ interface SettingsType { interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; + newChatDeleted: boolean; + setNewChatDeleted: Dispatch>; showFeedbackRatingPanel: boolean; setShowFeedbackRatingPanel: Dispatch>; refreshFetchHistorial: boolean; @@ -49,6 +51,7 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [chatIsCleaned, setChatIsCleaned] = useState(false); const [chatSelected, setChatSelected] = useState(""); const [settingsPanel, setSettingsPanel] = useState(false); + const [newChatDeleted, setNewChatDeleted] = useState(false); return ( = ({ children }) => chatIsCleaned, setChatIsCleaned, settingsPanel, - setSettingsPanel + setSettingsPanel, + newChatDeleted, + setNewChatDeleted }} > {children} From c91d52ddc9d084b73f9cbe1b44b9585a885f9309 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Tue, 16 Apr 2024 13:07:05 -0400 Subject: [PATCH 025/820] SFC-90 Conversation History - Sources are not opening in the side panel --- frontend/src/pages/chat/Chat.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index a407ddb4..92b843b6 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -57,7 +57,7 @@ const Chat = () => { setChatSelected, setChatIsCleaned, chatIsCleaned, - settingsPanel, + settingsPanel } = useAppContext(); const lastQuestionRef = useRef(""); @@ -437,7 +437,30 @@ const Chat = () => { />
- + {dataConversation.length > 0 && + fileType !== "" && + activeAnalysisPanelTab && + dataConversation.map((data, index) => { + const response = { + answer: data.bot || "", + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse; + return ( + onToggleTab(x, index)} + citationHeight="810px" + answer={response} + activeTab={activeAnalysisPanelTab} + fileType={fileType} + onHideTab={hideTab} + /> + ); + })} {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( Date: Tue, 16 Apr 2024 15:18:06 -0400 Subject: [PATCH 026/820] SFC-90 Conversation History - Fix Sources are not opening in the side panel (#23) --- frontend/src/pages/chat/Chat.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index e8e29526..84c26af0 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -57,7 +57,7 @@ const Chat = () => { setChatSelected, setChatIsCleaned, chatIsCleaned, - settingsPanel, + settingsPanel } = useAppContext(); const lastQuestionRef = useRef(""); @@ -437,7 +437,30 @@ const Chat = () => { />
- + {dataConversation.length > 0 && + fileType !== "" && + activeAnalysisPanelTab && + dataConversation.map((data, index) => { + const response = { + answer: data.bot || "", + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse; + return ( + onToggleTab(x, index)} + citationHeight="810px" + answer={response} + activeTab={activeAnalysisPanelTab} + fileType={fileType} + onHideTab={hideTab} + /> + ); + })} {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( Date: Tue, 16 Apr 2024 15:55:23 -0400 Subject: [PATCH 027/820] comments resolved --- backend/app.py | 2 +- frontend/src/api/api.ts | 2 +- .../HistoryPannel/ChatHistoryListItem.tsx | 25 +++++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/backend/app.py b/backend/app.py index 7acf5db5..b84b8a5c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -143,7 +143,7 @@ def getChatConversation(chat_id): logging.exception("[webbackend] exception in /get-chat-history") return jsonify({"error": str(e)}), 500 -@app.route("/api/delete-chat-conversation/", methods=["DELETE"]) +@app.route("/api/conversations/", methods=["DELETE"]) def deleteChatConversation(chat_id): try: client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 20eddf3c..a3bcc681 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -128,7 +128,7 @@ export async function getChatFromHistoryPannelById(chatId: string, userId: strin export async function deleteChatConversation(chatId: string, userId: string): Promise { try { - const response = await fetch(`/api/delete-chat-conversation/${chatId}`, { + const response = await fetch(`/api/conversations/${chatId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index c74c962e..a644a2c1 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -93,7 +93,6 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete setDeletingIsLoading(true); const data = await deleteChatConversation(chatConversationId, userId); setDeletingIsLoading(false); - console.log("desde eliminar", chatId); if (chatSelected === chatConversationId) { setDataConversation([]); } @@ -145,6 +144,12 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete return { month, data: monthData }; }); + const isConfirmationDelete = (conversationId: string) => confirmationDelete === conversationId; + + const isChatId = (conversationId: string) => chatId === conversationId; + + const isChatSelected = (conversationId: string) => chatSelected === conversationId; + return (
{isLoading && ( @@ -165,7 +170,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete
= ({ onDelete > {hoveredItemIndex === `${monthIndex}-${index}` || chatSelected === conversation.id || chatId === conversation.id || - confirmationDelete === conversation.id ? ( + isConfirmationDelete(conversation.id) ? (
Destroy setConfirmationDelete(null) : () => setConfirmationDelete(conversation.id) } /> - {deletingIsLoading && confirmationDelete === conversation.id ? ( + {deletingIsLoading && isConfirmationDelete(conversation.id) ? ( ) : ( Edit handleDeleteConversation(conversation.id) : () => setConfirmationDelete(null) } From 65366f66c347cd06f80a59106dc423f3bbece4b5 Mon Sep 17 00:00:00 2001 From: KenyerRamirez Date: Tue, 16 Apr 2024 16:02:31 -0400 Subject: [PATCH 028/820] solution for the bug --- frontend/src/pages/chat/Chat.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 92b843b6..72717e3e 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -440,7 +440,7 @@ const Chat = () => { {dataConversation.length > 0 && fileType !== "" && activeAnalysisPanelTab && - dataConversation.map((data, index) => { + dataConversation.map(data => { const response = { answer: data.bot || "", conversation_id: chatId, @@ -449,10 +449,9 @@ const Chat = () => { } as AskResponse; return ( onToggleTab(x, index)} + onActiveTabChanged={x => onToggleTab(x, selectedAnswer)} citationHeight="810px" answer={response} activeTab={activeAnalysisPanelTab} From 3343030fb9953238084550a85dd1b3a2cfb0b9e0 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Tue, 16 Apr 2024 16:58:42 -0400 Subject: [PATCH 029/820] Revert "SFC-90 Conversation History Sources are not opening in the side panel" (#25) --- frontend/src/pages/chat/Chat.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index cea099e6..84c26af0 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -440,7 +440,7 @@ const Chat = () => { {dataConversation.length > 0 && fileType !== "" && activeAnalysisPanelTab && - dataConversation.map(data => { + dataConversation.map((data, index) => { const response = { answer: data.bot || "", conversation_id: chatId, @@ -449,9 +449,10 @@ const Chat = () => { } as AskResponse; return ( onToggleTab(x, selectedAnswer)} + onActiveTabChanged={x => onToggleTab(x, index)} citationHeight="810px" answer={response} activeTab={activeAnalysisPanelTab} From 89c0387fbe2afc576a9cebaafcd136c98bbb639b Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Tue, 16 Apr 2024 16:59:20 -0400 Subject: [PATCH 030/820] =?UTF-8?q?Revert=20"SFC-90=20Conversation=20Histo?= =?UTF-8?q?ry=20-=20Fix=20Sources=20are=20not=20opening=20in=20the=20side?= =?UTF-8?q?=E2=80=A6"=20(#26)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 4b38bc6342b7b64e5de35fdc70d5903deb206d0a. --- frontend/src/pages/chat/Chat.tsx | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 84c26af0..e8e29526 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -57,7 +57,7 @@ const Chat = () => { setChatSelected, setChatIsCleaned, chatIsCleaned, - settingsPanel + settingsPanel, } = useAppContext(); const lastQuestionRef = useRef(""); @@ -437,30 +437,7 @@ const Chat = () => { />
- {dataConversation.length > 0 && - fileType !== "" && - activeAnalysisPanelTab && - dataConversation.map((data, index) => { - const response = { - answer: data.bot || "", - conversation_id: chatId, - data_points: [""], - thoughts: null - } as AskResponse; - return ( - onToggleTab(x, index)} - citationHeight="810px" - answer={response} - activeTab={activeAnalysisPanelTab} - fileType={fileType} - onHideTab={hideTab} - /> - ); - })} + {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( Date: Wed, 17 Apr 2024 09:17:02 -0400 Subject: [PATCH 031/820] SFC-69 fix tooltip selecting checkboxes on click (#27) --- .../src/components/SettingsPanel/index.tsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index d1f403b9..5bbe3f76 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -182,22 +182,18 @@ export const SettingsPanel = () => { />
- Variety Boost - onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")} - /> +
+ Variety Boost + +
+ {onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")}
- Topic Explorer - onRenderLabel(presencePenaltyDialog, "Presence Penalty")} - /> +
+ Topic Explorer + +
+ {onRenderLabel(presencePenaltyDialog, "Presence Penalty")}
From 7403d657698412b1c5b614bf4c8d5b733a6b6415 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:26:34 -0400 Subject: [PATCH 032/820] SFC-78 fix latest feedback question and answer (#29) --- frontend/src/components/FeedbackRating/FeedbackRating.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index 97c1f29f..05c55cab 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -68,8 +68,8 @@ export const FeedbackRating = () => { }, conversation_id: chatId, feedback_message: feedback, - question: dataConversation[0].user, - answer: dataConversation[0].bot, + question: dataConversation[dataConversation.length - 1].user, + answer: dataConversation[dataConversation.length - 1].bot, rating: selectedThumb === "like" ? true : selectedThumb === "dislike" ? false : null, category: category }) From 1fef34387450bae8ed7f30cb8a0b1b0fd36126ce Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:51:07 -0400 Subject: [PATCH 033/820] SFC 90 conversation history sources are not opening in the side panel (#28) * bug resolved * (Solution)-SFC-90-Conversation-History-Sources-are-not-opening-in-the-side-panel --- frontend/src/pages/chat/Chat.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index e8e29526..25be7f3d 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -287,6 +287,15 @@ const Chat = () => { setSelectedAnswer(index); }; + const answerFromHistory = dataConversation.map(data => data.bot); + + const responseForPreviewPanel = { + answer: answerFromHistory.toString(), + conversation_id: chatId, + data_points: [""], + thoughts: null + } as AskResponse; + // const onShowCitation = (citation: string, index: number) => { // if (activeCitation === citation && activeAnalysisPanelTab === AnalysisPanelTabs.CitationTab && selectedAnswer === index) { // setActiveAnalysisPanelTab(undefined); @@ -437,7 +446,18 @@ const Chat = () => { />
- + {dataConversation.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( + onToggleTab(x, selectedAnswer)} + citationHeight="810px" + answer={responseForPreviewPanel} + activeTab={activeAnalysisPanelTab} + fileType={fileType} + onHideTab={hideTab} + /> + )} {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( Date: Fri, 26 Apr 2024 09:12:39 -0400 Subject: [PATCH 034/820] SFC-166-167 fix user id for settings and feedback (#30) * SFC-166-167 fix user id for settings and feedback * remove not required package --- backend/app.py | 11 ++--------- frontend/src/api/api.ts | 12 ------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/backend/app.py b/backend/app.py index b84b8a5c..5595f074 100644 --- a/backend/app.py +++ b/backend/app.py @@ -216,9 +216,6 @@ def getSettings(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') - if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 - try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. @@ -250,9 +247,6 @@ def setSettings(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') - if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 - temperature = request.json["temperature"] presence_penalty = request.json["presence_penalty"] frequency_penalty = request.json["frequency_penalty"] @@ -295,9 +289,6 @@ def setFeedback(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') - if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 - conversation_id = request.json["conversation_id"] question = request.json["question"] answer = request.json["answer"] @@ -326,6 +317,8 @@ def setFeedback(): "category": category, "feedback": feedback, "rating": rating, + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name }) headers = { 'Content-Type': 'application/json', diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a3bcc681..159ffb4a 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -13,15 +13,11 @@ import { } from "./models"; export async function getSettings({ user }: GetSettingsProps): Promise { - const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; - const user_name = user ? user.name : "anonymous"; try { const response = await fetch("/api/settings", { method: "GET", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": user_id, - "X-MS-CLIENT-PRINCIPAL-NAME": user_name } }); const fetchedData = await response.json(); @@ -33,15 +29,11 @@ export async function getSettings({ user }: GetSettingsProps): Promise { } export async function postSettings({ user, temperature, presence_penalty, frequency_penalty } : PostSettingsProps): Promise { - const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; - const user_name = user ? user.name : "anonymous"; try { const response = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": user_id, - "X-MS-CLIENT-PRINCIPAL-NAME": user_name }, body: JSON.stringify({ temperature, @@ -189,16 +181,12 @@ export async function postFeedbackRating({ rating, category, }: any): Promise { - const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; - const user_name = user ? user.name : "anonymous"; return new Promise(async (resolve, reject) => { try { const response = await fetch("/api/feedback", { method: "POST", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": user_id, - "X-MS-CLIENT-PRINCIPAL-NAME": user_name }, body: JSON.stringify({ conversation_id: conversation_id, From 4ae73e6f887c25e68f3917cb9c8f1d8a7e7dc54e Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:47:37 -0400 Subject: [PATCH 035/820] Revert "SFC-166-167 fix user id for settings and feedback (#30)" (#31) This reverts commit 799fb727893ce3325d2cabeac4c3539159dec2d4. --- backend/app.py | 11 +++++++++-- frontend/src/api/api.ts | 12 ++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index 5595f074..b84b8a5c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -216,6 +216,9 @@ def getSettings(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + if not client_principal_id or not client_principal_name: + return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. @@ -247,6 +250,9 @@ def setSettings(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + if not client_principal_id or not client_principal_name: + return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + temperature = request.json["temperature"] presence_penalty = request.json["presence_penalty"] frequency_penalty = request.json["frequency_penalty"] @@ -289,6 +295,9 @@ def setFeedback(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + if not client_principal_id or not client_principal_name: + return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + conversation_id = request.json["conversation_id"] question = request.json["question"] answer = request.json["answer"] @@ -317,8 +326,6 @@ def setFeedback(): "category": category, "feedback": feedback, "rating": rating, - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name }) headers = { 'Content-Type': 'application/json', diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 159ffb4a..a3bcc681 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -13,11 +13,15 @@ import { } from "./models"; export async function getSettings({ user }: GetSettingsProps): Promise { + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; try { const response = await fetch("/api/settings", { method: "GET", headers: { "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name } }); const fetchedData = await response.json(); @@ -29,11 +33,15 @@ export async function getSettings({ user }: GetSettingsProps): Promise { } export async function postSettings({ user, temperature, presence_penalty, frequency_penalty } : PostSettingsProps): Promise { + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; try { const response = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name }, body: JSON.stringify({ temperature, @@ -181,12 +189,16 @@ export async function postFeedbackRating({ rating, category, }: any): Promise { + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; return new Promise(async (resolve, reject) => { try { const response = await fetch("/api/feedback", { method: "POST", headers: { "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name }, body: JSON.stringify({ conversation_id: conversation_id, From 0d9f1be655ee74f7b3ab1d4f12a3835487e9bbcf Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:45:25 -0400 Subject: [PATCH 036/820] SFC-166-167-user-auth (#32) --- frontend/src/api/api.ts | 12 +++++- frontend/src/api/models.ts | 9 ++++ .../FeedbackRating/FeedbackRating.tsx | 6 +-- .../HistoryPannel/ChatHistoryListItem.tsx | 10 ++--- .../src/components/SettingsPanel/index.tsx | 14 +++---- frontend/src/pages/chat/Chat.tsx | 42 ++++++++++++++++++- frontend/src/providers/AppProviders.tsx | 23 +++++----- 7 files changed, 88 insertions(+), 28 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a3bcc681..efa86f85 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -9,9 +9,19 @@ import { PostSettingsProps, ConversationHistoryItem, ConversationChatItem, - ChatTurn + ChatTurn, + UserInfo } from "./models"; +export async function getUserInfo(): Promise { + const response = await fetch('/.auth/me'); + if (!response.ok) { + return []; + } + const payload = await response.json(); + return payload; +} + export async function getSettings({ user }: GetSettingsProps): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 2311c01e..bece5eb6 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -1,3 +1,12 @@ +export type UserInfo = { + access_token: string; + expires_on: string; + id_token: string; + provider_name: string; + user_claims: any[]; + user_id: string; +}; + export const enum Approaches { RetrieveThenRead = "rtr", ReadRetrieveRead = "rrr", diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index 05c55cab..fa523245 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -15,7 +15,7 @@ const categoryOptions = [ ]; export const FeedbackRating = () => { - const { showFeedbackRatingPanel, setShowFeedbackRatingPanel, dataConversation, chatId, userId } = useAppContext(); + const { showFeedbackRatingPanel, setShowFeedbackRatingPanel, dataConversation, chatId, user } = useAppContext(); const [category, setCategory] = useState(""); const [feedback, setFeedback] = useState(""); @@ -63,8 +63,8 @@ export const FeedbackRating = () => { await postFeedbackRating({ user: { - id: userId, - name: "anonymous" // set name in provider + id: user.id, + name: user.name }, conversation_id: chatId, feedback_message: feedback, diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index a644a2c1..ab689db6 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -22,7 +22,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const { dataHistory, setDataHistory, - userId, + user, dataConversation, setDataConversation, setConversationIsLoading, @@ -45,7 +45,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const fetchData = async () => { try { - const data = await getChatHistory(userId); + const data = await getChatHistory(user.id); if (data.length > 0) { const sortedData = data.sort((a, b) => { const dateA = new Date(a.start_date); @@ -75,7 +75,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete setChatSelected(chatConversationId); setChatId(chatConversationId); setConversationIsLoading(true); - const data = await getChatFromHistoryPannelById(chatConversationId, userId); + const data = await getChatFromHistoryPannelById(chatConversationId, user.id); if (data.length > 0) { setDataConversation(data); setConversationIsLoading(false); @@ -91,7 +91,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const handleDeleteConversation = async (chatConversationId: string) => { try { setDeletingIsLoading(true); - const data = await deleteChatConversation(chatConversationId, userId); + const data = await deleteChatConversation(chatConversationId, user.id); setDeletingIsLoading(false); if (chatSelected === chatConversationId) { setDataConversation([]); @@ -127,7 +127,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete if (refreshFetchHistorial) { handleRefreshHistoial(); } - }, [userId, dataHistory, conversationsIds, refreshFetchHistorial]); + }, [user.id, dataHistory, conversationsIds, refreshFetchHistorial]); const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 5bbe3f76..41120ba2 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -31,12 +31,7 @@ const itemClass = mergeStyles({ }); export const SettingsPanel = () => { - const { userId, userName, setSettingsPanel } = useAppContext(); - - const user = { - id: userId, - name: userName - }; + const { user, setSettingsPanel } = useAppContext(); const [temperature, setTemperature] = useState("0"); const [presencePenalty, setPresencePenalty] = useState("0"); @@ -52,7 +47,12 @@ export const SettingsPanel = () => { useEffect(() => { const fetchData = async () => { - getSettings({ user }) + getSettings({ + user: { + id: user.id, + name: user.name + } + }) .then(data => { setTemperature(data.temperature); setPresencePenalty(data.presencePenalty); diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 8ea0757b..13a7588d 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -4,7 +4,7 @@ import { AddRegular, BroomRegular, SparkleFilled, TabDesktopMultipleBottomRegula import styles from "./Chat.module.css"; -import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, ChatTurn } from "../../api"; +import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, ChatTurn, getUserInfo } from "../../api"; import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; import { QuestionInput } from "../../components/QuestionInput"; import { ExampleList } from "../../components/Example"; @@ -58,6 +58,7 @@ const Chat = () => { setChatIsCleaned, chatIsCleaned, settingsPanel, + setUser } = useAppContext(); const lastQuestionRef = useRef(""); @@ -73,7 +74,7 @@ const Chat = () => { const [selectedAnswer, setSelectedAnswer] = useState(0); const [answers, setAnswers] = useState<[user: string, response: AskResponse][]>([]); - const [userId, setUserId] = useState(""); + const [userId, setUserId] = useState(""); // this is more like a conversation id instead of a user id const triggered = useRef(false); const makeApiRequestGpt = async (question: string, chatId: string | null) => { @@ -215,6 +216,43 @@ const Chat = () => { } }; + useEffect(() => { + const getUserInfoList = async () => { + if (window.location.hostname !== "127.0.0.1") { + const userInfoList = await getUserInfo(); + if (userInfoList.length === 0) { + // setShowAuthMessage(true); + console.log("No user info found. Using anonymous user.", userInfoList); + } else { + // setShowAuthMessage(false); + console.log("User info found.", userInfoList); + + const keyId = "http://schemas.microsoft.com/identity/claims/objectidentifier"; + const keyName = "preferred_username"; + + const _user = userInfoList.find(obj => { + const _id = obj?.user_claims?.some(claim => claim.typ === keyId); + const _name = obj?.user_claims?.some(claim => claim.typ === keyName); + return _id && _name; + }); + + if (_user) { + const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; + const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; + + if (id && name) { + return setUser({ id, name }); + } + } + } + } else { + console.log("Local"); + } + }; + + getUserInfoList(); + }, []); + useEffect(() => { chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }); if (dataConversation.length > 0) { diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 3362eb8e..4131d1fe 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -7,6 +7,11 @@ interface SettingsType { frequencyPenalty: string; } +interface UserInfo { + id: string; + name: string; +} + interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; @@ -20,10 +25,8 @@ interface AppContextType { setChatIsCleaned: Dispatch>; dataHistory: ConversationHistoryItem[]; setDataHistory: Dispatch>; - userId: string; - setUserId: Dispatch>; - userName: string; - setUserName: Dispatch>; + user: UserInfo; + setUser: Dispatch>; chatSelected: string; setChatSelected: Dispatch>; chatId: string; @@ -44,8 +47,10 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); const [dataHistory, setDataHistory] = useState([]); const [dataConversation, setDataConversation] = useState([]); - const [userId, setUserId] = useState("00000000-0000-0000-0000-000000000000"); - const [userName, setUserName] = useState("anonymous"); + const [user, setUser] = useState({ + id: "00000000-0000-0000-0000-000000000000", + name: "anonymous" + }); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); const [chatIsCleaned, setChatIsCleaned] = useState(false); @@ -62,10 +67,8 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => setShowFeedbackRatingPanel, dataHistory, setDataHistory, - userId, - setUserId, - userName, - setUserName, + user, + setUser, dataConversation, setDataConversation, chatId, From 8795a5900b2d726bd48dc15ff5c63d5cd77c35f2 Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:35:44 -0400 Subject: [PATCH 037/820] changes to fix the bug that opens the AnalysisPanel more than once (#33) * changes to fix the bug that open the AnalysisPanel more than once * console.log deleted * console.log deleted --- frontend/src/pages/chat/Chat.tsx | 34 ++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 13a7588d..aa2cbe0a 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -484,30 +484,34 @@ const Chat = () => { />
- {dataConversation.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( + {(answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( onToggleTab(x, selectedAnswer)} - citationHeight="810px" - answer={responseForPreviewPanel} - activeTab={activeAnalysisPanelTab} - fileType={fileType} - onHideTab={hideTab} - /> - )} - {answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( - onToggleTab(x, selectedAnswer)} + onActiveTabChanged={x => { + onToggleTab(x, selectedAnswer); + }} citationHeight="810px" answer={answers[selectedAnswer][1]} activeTab={activeAnalysisPanelTab} fileType={fileType} onHideTab={hideTab} /> - )} + )) || + (dataConversation.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( + { + onToggleTab(x, selectedAnswer); + }} + citationHeight="810px" + answer={responseForPreviewPanel} + activeTab={activeAnalysisPanelTab} + fileType={fileType} + onHideTab={hideTab} + /> + ))} Date: Tue, 30 Apr 2024 16:28:41 -0400 Subject: [PATCH 038/820] SFC-22-RBAC-ui (#34) --- frontend/src/assets/person.png | Bin 0 -> 12849 bytes .../HistoryPannel/ChatHistoryPanel.tsx | 35 +++--- .../ChatHistoryPannel.module.css | 12 ++ .../src/components/Profile/Profile.module.css | 85 ++++++++++++++ frontend/src/components/Profile/index.tsx | 110 ++++++++++++++++++ 5 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 frontend/src/assets/person.png create mode 100644 frontend/src/components/Profile/Profile.module.css create mode 100644 frontend/src/components/Profile/index.tsx diff --git a/frontend/src/assets/person.png b/frontend/src/assets/person.png new file mode 100644 index 0000000000000000000000000000000000000000..b72cbcee33276860409fb7418e54a8af109a22d2 GIT binary patch literal 12849 zcmcJ0i91wp`1hF^W~^h&l6@IVc3BHi3PY61mc%rLjBSu5)XaR9I-x!Q!E6jWRe%JNBf5CfQ<}&A;=eeKzxu1LeoO_bbI@<8_Nb&#xz>l>( zi30#Q^c4Qa~w5ET`r z?H?Qve)$r~M>{0UuVC3s5&+}@?8)QK(S=K2uQy-xxxTp?f;@a&@!XpO@;z&s@WWhi zMfnp>Wy56+E^$j=#2OksUL*}SHWEb>d4^xP91uF7TBi1#a=1VKq-BDxfCAqoTN8y3 zf4goFPQ1tVjmIwCun4GJnQvKM1oGbKEHWEL+KbmK=3&@O%>U0n*C6Le0Q=Sm<}NTM zm~Z5MF1P#x=<#u9xA`@DB^p0?m|b-U%zxc+Q%pi)XHgYNM)U23Q~W5L@$A69u=O## z*5Cd)HEC^FO~idM4J?i@YkmDc?C6AYNwr1T_ul5g_gUh0r|&`XBxuED>}v=y#E z0j9xL_=TB*R|!VIPU=-;Ffv$jXWPN2Pv}Kjdc3R!wLyjW1|3~J z!;FMFiiC1=_EjEjjEa(-2TTh1`K}X)MO4Gk92;-uHzFD&nqgZ9NwTCPtP?~Lp|1hBrcJ}jKBpR4PO2_xF4L>*n3Q+2kwh_GH-))C$VQ0?;I!ykaMh4z5Jk@I; zWrEsLsa>Wx2N(fm^@h|@5#!zX)wPPfm!exoS?r`v%gmw6Pm(s*I;;XsTgU2z17u0a z)q5{sIVH0BaOpE`Ejc#^nkqzxm%v2IdO9Xz=?6HPbjq_%hm~tsC<(l>G0V-wq=4>> zGnDf89r+hrjU!}Ou6RQX*2)860?R+Ocl%3SSw$ixi`tNm*?k>De;1107AJmO@ag4r zeC_6<+%UNTvVs0A@}>ugHj;jQ)Y8NCh`+2Z1tfI>&+hoTz; z4Hbh6aJ&G9cD!x%!U&5fR4X)4Ws6G*Iu%&GaJIuIr9uoot+>&gUNYe>zI8x*IN%g&>o1Z^B zql~Mnq;A5`Q9mZR7?xf-m?LA^5XQ%n1AWJjW7_FfAc`8|hjKC>1Fo9*0^TsMfdQDWMAXcUo z9W73}5h<9{B?M@N7&2BU`pJ*T*!2b&YaUzxLz0|e+|Uf24Le4_mPhf>N&RG-m^>ep z(?MyNAZWr6q4$zK!o;xcdZN*)wS}yA^yNaLo(=HAoaKPuWXfm$@l|1oF*=#@-QT$^ zV^iR%s6Lnxwoj4P@f1$Bz6&sfDllbKU@4#-12YR z!DjA|z05a{=mGy!e?&jttAkxlCTRAPM(-iD9>v>o~$|7!yO#P^-b#zgLBmUtbF->O^nj1-LMLO$FxQdIe~ zlp}Kz7vPDI<}@pmIr*R-_PoiQDkl_s(;jE>6(OCx$T^f-4I3F908d4)&GYiusX_(r(3`iJ5aal=* z4yoN}MH|45BoVq+oDQw@=L%qw5Isx77y&Q6tODm3d%baA%uv#hVV7_i0oT#W32)lA z?TO_t>4f8Mekgw>DT*`-ut2EzLL0pjo-|q<(uW>2G6Hs+^hrXi9(OU^vYcp+n|Xy# zTmpeV0lb9k)9dx|wzx~ns@66u&{#2OY`sG0+O#ILCD8@P$J`|G*T-sL|D`ohy}v(| z$b6x$Pu-=c*2Nn<;DxPEW!~0-I2bGoO5=U9{w|*h@EaqyU?dV`f@K*FsKvzbAZHA_|FvFiRwQlKj^9@`v0@}mjBwQpHwyf3-R!@=23OC5i0%E_D7@mPO7}^Ei zB;|N=hD?cnAW+sMrT3kj0vX+qV0f}I!c(i}gb^@2A=nHbGlBRt?9qf`!M9oIly3o) z;d{1xe`DA3N)~!S{vmZA#MuXIj>vwr0+zo~l{L&1bohp8AhyYEorTsWQXeFD0i(Vs zn&~`nEyRN11`;7{pec(1)hZiqPKkj)OT8Y6UsnrSrlH3Ql#RC-hCxGv;Ux6bj`E?m zNH8k&cf;jA*ng95RHb?=$TtU_2yBkL8JT{x4)$M zlR3?~C)L5I$_xiwmrgQd#uYHww~eAK36e`>dgi!7<)k(pbrrH@>|jz4Y6BkJjmD`< zQ68lrs#J=Uv6&0=VvHXr^&kF#R=>*TXw!pMr(OO`8KxHdp$=sz0UC|QwY??$nV8`( ziVT1;$doUwg+%rqPVTyDPfNv>J&6Yg@-JEi>{tld?rTAkWNZgb&jo3NE0=r|Yd{iNewLu!AB zng+b-q}QZ>*|EdboAWb)$vhcMftA9yJNp7P%I7P*FY_2O7?Nh~jS>t^CKZ^fv_`*dX$C8Qb zK{X|4=kPqlvzDCNllz)z8utG870f{#UVyWX`;p_VXd_cdj zH+%%sm-m5AIKK!h0HhF~sSe3*kDqR?q$!C0*ag4TTRB%k`t&(jhV8k2CWr-)e&R1W z^u%6ov0+z6BpU}rBD`SI92K9RuydU=IqC0z?T#NW8=@l_;ox8B=*$nxHFNz>3L8tv z9iV^uPXT-c!c2ZWo%>_J+JqP!dV$3Akfs@DXorxLHEKOo&T0APk8@OGcnEUiEw-7M z`z*=ka%mB{G77%RH&l23Y?H{e$;gwNPZ6rZR!_&f2I|Vn1U|_-NWvr>JawDiqZK25 z;uzBuwzBro2qQ@o;Vr2dC#tjGrbswU>S^G8V+gdn{-7AL| zTJ<|}U<}m^Wxzu@Kf%y(h2OXi3_1FJ-BDUGJob|;3Z9ovp42MOYROHw+O0%+Kq-JW zsAP~8{d8-MDUVH6oSF!ob&OaeGTtx+zY?J#B6SE`C6+X+3DkbEIVZ4s6|c%DPBxY) zX^9Srq^KKQ;=l(H2%U7M{Gjd*JX(O{7McDgkhhShP2DuffDb{pyO#zjueFFZuy-J2 zPklO$@fN%kz2=>9AvuS0I`IiiecD8_#()G32v$rGD#d-}Xya^FHnnwNa>q%H9X3sR zA>1VNrB4S6MzoeRfO*@PoU_C?vu+%+_cqF~md=WTY*3??zlI!{6g<=pi;ju}{JD6O zW$1g^`M;lKT)!`2cqCbp{1*4UUBlGUcwvVN&(Anc;5|pfAiyt44S$E*jp5UP3X}}w z>I;lO%b9EucPthCHs~2hm^%6aqfxpQ?x=+k~Y){gU8re<>zX(Enhp zkhfhxX;m7qhcTtRxNF0jo7QO(y2JUkK<1=J=a)JJG+znm=!&}KlJJraH$wm29L)uN zz#*o*FiTfYy+POXz3osa2R@{2yBv};Uqr~>ZWOBaUV62A8OI_5btO@PcGL(!v&%W| zBt`9foj%r|0~XN6o3sr_QJ2;|d($G>ly&bE49?AlmB3 zZKnKNmnI=fJZr8xZo0F8j6H^kvoO8o5uzTNR+iFY2tdii!;x0*yocJo1cna9v_Ag% zI&od%p<^1*DO`P_@&QEIFwH%#JLN|A3#nCK@D@{^Sb2o5w*C)9+v%AQ1p(3aB&Qj( zGcO9n89=myWEkm8!M85Ykc{b2 z0^uIx9hA@>eL{I#XH{V*BT1QPJ4dtLR?y4xT#r1Ym#MUVVRa3#WBZy*MrXTw9$$F& zl_FJV^;{SQ7l(LWG&|mgc4r3XltPN1YD`|A)`9c{BMx(e7+b91Wah{aeC$F1={>iE zeqYqtAA3rs6c5^eT>wa0aUh`&vT{ysT_+vPg?{ktOIE#hoA$we5^)R7b?_Zh%yWz z{OL%+hR$$0n)+_(5N?4BT!svceDRhs+bAF5nFHzFVeT!KA{dM01>+E&rPa^-%qU7xaPxo}5SS0mTLo&DUQAp%Q0uT51Ib%bz=>gA zNr!@o8q1P4p#1^;@Ri5iSj6)VBfzL1UE3xsMInegoV|Q8SH|*2R751u;24?)vkL?w zKSCP4>ig-~Uf*y?s~^g}h@UbMEJO;AJRCsx_&)Et@k3N#p>shKTzJORl?Mk4<<{lEHeQ+2+p{v zv7l$^&u^LWQMqhj8RF5vh`iABSQe2z0&MMg@6LrYHd^$dRl0;IIW@4e9lemb89+ta z*+mn$?yXR|It6E84(-5ya@T3MQKt6m4Iw)s&?Hm*cd>}(e&XlAC z)ghke%=w_sN=gAfYQD0TSbj^riy0n1@}`tQvj>n-IQO(yl_2q7APJYwL+m@yzC0=) z_q~F0e_VnDZGdiu!^8GYhy{(__vo6F`}5x_5_6KrVcDJ7uI{rvUxG^wfB@Jc#Dazc zw7Nkmp!W6}kN+`yf5500;s&Z}b!^Dlmo#faBe~R`!SW$KD}O*#C$Br7hZdrQNm3jj zJ{#J&UyS~Ngy*4Ohp;@tZz=Rvfv9W|yX3R>O!I$lQPP0ycLUEhAcQAZaf(B&6vk4l z&`bqhjL!YPc@!m}o3J+SRduXP^VH;;-BrLECglT}wP?Lw;-BDFXs4AC&<^_cIs}$Eg_2HQH6$yDu`&5>Av_&r~g89oSc&XShecSCfp%u7ix&S zsc{A8d-0*07CBuuKz7D)k0}dC`+SaN%2;~9YygrII_KxvIG+mcjn3&O-LqVf+fpA@ zg#;K-+>Ms!e=P*i@PFYD8uBQ$ojckklFOg#C%+~4^eb%zl4Ot#!;3RUnDUaj zOO!=5e7&f@auH0$52^O8v(*HO#ysCC=MH_38|Y5qd_{%Q4V@bt4{z~K>_FnvZR6Q$ zWeR$ficrm=$o@8c3dm-0;f-p7A?75!Q%7sA#MjReW3$<#LS-kjNraUe$5P%@EUR{}HK^u6Czc|AfygMt66aC-Y zwcMmbB+JNjdg>1|?(xM5Y)Ke75uYa3BlblG*}(cI zCvX@H9zRaJ;>M6wq~jIQM6M!7`-YJR^4Qo-JFg_09mDha(E$2IRnskC?TKZv`_9GkJU^&Ib@NM1mt@G!9Q8N;c-;FDB-it}v; zRC&^!yCvd>2yprA^|s|yCXk6)ch?K~4SBZAAqyzDYC7Q%q*&;kOlAx-mkHQs8*q~h zXIw0!Y_l+i17`TctLpfcOlv?4fXY_|EH165B)z~Qdl&#Fvt=wZOJooEZpn2A0G?>~ z%pqqZKqK_>{}7vP8i(b&cpw-`02X&nbQs9~Ed~mw3EHRIWey#vKg67Jo!|mYc;vCf zqH>6hYN%E$+h@b8LzQS6JPHyG zhNf7r#+(!IF#;Hup>@I!81D%DFaq2l2MRqi0+bkB6lmJbEE)VMyFv~wUD;?N>lp?c zv94JoR{K53$xa3qEh~qx&0JW)@w9DQz2pbL+xsgo48Hr{+>BMu1aRVFh_yN^1JG>7 z-7dlh@rkOD?5B!*Fe$JjRl{G)>WZaYI43gv`68G|{R!TE-SJ98KYfTgulAdta@3^w zzdfz;*%-8mlnIZ-Tl*9w8rH`tCYs(f+rnXjmN2D8Oibkt9jptoVOabaq8MoWQ$)t|8WF> zKy>34ePlF0ON((2zUR4H<%dlB{^%N8ElhyHHxre$J6~7fvnYC?VkiR0mEbi)wn2!b z#|oCwLD3QJ-!3;0GLbO3AAPsR>m7R!XLi~Qxh=%9$KQmmQm7rs)UJSpU%ki?RoTBZ zE5D5CcjuQXTPY%x55K#vGIBuGq~W(kE3b=b-s@(1C)sDfsox=kt5+B|=uZjbT_7>! zhDgNH(kxrCa${I?W_#~nmG$+{7y|s8YVr}(nM`tELYXQ`|7Mu(T1j_W=li>yQ{0nQ zd$qL_ZXu6Ir511?-;Pfvcc=UD{qI7A6B9JL{*S@Ca3fTt>YM_TFOWMBd3W(#0&M2X z`UO$Edvsd-to%d zPQjI)kmCjjDPR#j;ylx7?873PLLZAutd)5Y@rsx6WQ)A9);Ob3SS(+3QuEJgY|ljX z_wZgr$|geY%Fo*Lm~VD%elBxQW(DTmdn@LXZpV&T2i2knm|Ew3KGl^~r9J9q+U@BK z5xsVANjSQh2Eht#_U1nky_T^2LqCK7`pF^(DE$zU<=BInn42d3OFF<(ZhQ4ya#N(Z z6e@vSOC3cPZ$e?ep$XXHt99%!CToRiL&5tzmkK#9UM$6pHQ9GjRF_Wd9Pm)AAi^#*n}~_nA6tQ{^TJc>VQ9mcQ;m+0jYL5uo|qWS8}M?}bXJO( ziJ~W5kyCv|u`S1y>(C;k#AFQFg^|c)Nw?n}5YHiHp` z9L@9t3ESgqyp}@h}X*&(MB&PQ&_CRk+Fy79pRCgbxtQAsT`m zNg+_(>Jtp4f(0G-FKg=wlN|OZ6%6gn3G_<9fH7F}y}6z16ALPYF|s4H5Nd?uCRS6S zBIe7z=J_EH2%7?eU%Sy4mcB z;rkGbf#O)v9f!GBh>#nrAuyr7W=JA&pst;|MT zdHvF0Gc?P&!3h-jU=dO|_WN1w{hLlx)G_>1TH4VCh8JN?Byq3w+Xdccu`x|k+dEnq z>&%eeebl)_Z|cNdxLOx{=H63dvj3GL`Mw8KD20tw;eWN=^e&PfUdVh@Vl;wET>YD2 zU@(uE&FzM6Fg?gd$qE((?cejvZGy3iXikm|132)9kJS4@>U<_Al%aa zd(?^eU}*c1%Z0m$U={XT55mi<3C@jandAp*wZWaExrV1>g|mot7B7^C$mJ7)xVkKcD2$f6<7Og>p+`HJ(3>jViO97^S)@m7opTT2Q$4ED__uao8r3c zG&8;#P#2H+%SEz+-tfF_lzdApJ^6C9X#t@`x^-fHk{-3l(yR*e64-9}*z_#}Ve`uiA zGo{Lx;9_j3E~fOz-7#I&zkd!7{+aiZ=A+43!=~lg7)k$jAnrM;%L>PSU}-LgEjs0C zRXX93;(lA>@0Ep;J`NREvMpx)bu?pf*4ySN&HH-{hM05+T%Rp=elU~+CJf67W6fU( zkwhUqa$HcsS1t%7XZs--!N1N_IWEBP#n5$$?3j3T`p%-Y>Uu1GcYi>L6ywsbGj)zc zU`zg3R+gP1&sOYA!%ODkDb)x$-6sq2iaA$Q8G66YG&(NA@$KDd|CR(-*th5XdElV= zSjBLv3j97>eC@%=4R9|?^rly;= zlT}*~1K;{wCUCbz^k{0dZ_$mZsMs}&66k1$2No&Yv&Gq8NA7_WqHBiz%r6>$ZXL~+Wbe)e?MifXJI^F8T#vc1P@)A_Z zkyW6*G~#enWw%w9$)dc@{}jrU3^pb3?=iEQ%3>162+@H#C@e9(mL9u%CWHLS)kOeq z>UKT7q;V`Erkd68xFyy7<>^6O?73+ftHiP|p+RJthSaAv-nSB-zuFw1zo@eyjVJin*bmCxRWxFJIQAjd#SQrD5>Zp~hKY?c?K{>V zt6kCsZ#NB%FPaxX1V;*;n@x3@O2Jll<)*v#|H1* zp|W8r&SA3{m$_)d3C9i2$8rtnW0&#McBzF1VX=Z~&nCk9wB$#Y(w(1MQEfhSxc{!# z=A7cXOJi(dHT01t=;p$m{u>sH$Ky`9mEBIE*uU?%`nz>m@ztY>@m+;%uPcZ3-b6}m z7q8tMH8k0@F3TCFKuoQ^f38Z^nbE$o04aRl)e-a!C0ey$_+6-nW&jkK^|q~BkR;r3 zF_s-9IUuLdo|b#_#^rPHkfjYvyY41YlG3(#PS1T*A;0wCk6P#OrQ^2L2?E;^w6VV#W|jy|K=!VLPm&Ve3Pg5<`1`u!HVB1kZHB4FyAb4?V-|O z1ta&c73#)VQncnZ?%s>VlJkCeGY{k{JhOM|S&0XiCr4YJ`$ebT!~9$J1mjmv zgG+|}FY=vU+4*EwR^UVraOlq@!_+Iitd&RK(*F(Vf(2ZQ%pXRHBx8%70h{UUmxo>kb431R>e>@Cxxr^w z`!k<@9jXV}@D)U4SIC)roq`?ln<5=q^Y7Vl*6@ z*Ev0i4m0{qU56WP5Fh(Cr{*(bgZ3K}k(L_zVo+kDM^I33nLo~OWo*1IZW5O2owRQ& zm^%4{>Oife2B881@TT~9x~IeM1A6~PsrT-h(d>-?$yA4A(KmZ)rnbC2O85WyLj9YR zz@WZv$&)zZo}U{G;rEL|rd>fN+b+CX#Ru^RTAem=33-6n;UBnSXH^ILWGju1gN<_c zzev9dUsiU#7WdECE@`~je(%r0nF5Az`1uJlv#!DU*Wb?{eWQ=euj^s^Ip8A*`lW`6 zTSs!5e^#f3oVwB?zNNvKgj!MV`x_F(HuPuviH({X_g|j*CMgPauZ7PPSZ}=Rubc3= z19e~8Xl4#Yp%9~4WW5&^M#Bx@9@IxVKtKGl{(Xlo)Ni_|HzQOSt~F$6akA_U0k6XH zrk^mWmRHHemUZgW3rxC%S~s=&Qk1Y7jk2qIwXwYJu2F^pTbec6na)c3l+QInH5C~n zCkP$!z3C;AUyvs8&Ch+j-61^7gCj1@UD+v9^0F!88iVtf&Wl_ZoEFBb{3`-Wta(7G z-=7L&kdsO?m1%}X2uE0vGW$f}`Q3{7LwSryq*9BDREBml-%hxGZe|dgfXnk^(L!HSxH9OnJhgPhAmiWnC4{`|8JA{7vXRe8s>#)(=^ODYM3U`$9{*u znAX2mYEc7AcMaC85hs1ZFIiPsJp5&b@PvA_bvU#UbI(*Vd3+J_DLw9ZDZ(wh1zrGe zYL<7{?4((1$F1g{-kf|TDw#z3o!p^zf4bUhFM4K8E8m{J%RG#MBuKDh7(9&ELnm>) z|F`eHl3Gd8LIr5xFHbc1?Znf= zTABn#g5nK%qZM>=hh+b!m0u`81j6u6YF=v7tBv{A8Bg}k8x@&HY-*j`1U3cs*4yLP zPkS82LhlEhRYj8L@G_4Ny2sx0*jP~ovXZ*^MNsUUZw`~qGu(d$FWc>1I0NHFvW?f0 z3jUm<3+`so!(N8xmXF7?cYYH0n*aa)$N_`x-eAK{zHkseKs+G|{oDh void; @@ -16,23 +16,28 @@ export const ChatHistoryPanel: React.FC = ({ functionDele }; return (
-
-
Chat history
-
-
- -
-
- +
+
+
Chat history
+
+
+ +
+
+ +
+
+ +
-
- +
+
); diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 102924fa..5b0128b1 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -143,4 +143,16 @@ to { transform: rotate(1turn) } +} + +.profileButtonContainer { + position: absolute; + bottom: 30px; + right: 20px; + width: 240px; + height: 80px; +} + +.listContainer { + margin-bottom: 80px; } \ No newline at end of file diff --git a/frontend/src/components/Profile/Profile.module.css b/frontend/src/components/Profile/Profile.module.css new file mode 100644 index 00000000..ccee1559 --- /dev/null +++ b/frontend/src/components/Profile/Profile.module.css @@ -0,0 +1,85 @@ +.container { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; +} + +.button { + display: flex; + align-items: center; + margin-top: 5px; + gap: 6px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + border-radius: 5px; + padding: 4px 12px; + transition: .2s; + background-color: white; + font-size: 24px; + height: 100%; + width: 100%; +} + +.button:hover { + background-color: rgb(243, 243, 243); +} + +.button:active { + background-color: white; +} + +.dropdown { + height: 100%; + width: 100%; +} + +.stackDropdown { + height: 100%; + width: 100%; +} + +.contactIcon { + font-size: 35px; +} + +.contactImage { + height: 60px; + width: 60px; + border-radius: 50%; +} + +.dropdownPlaceholder { + display: flex; + align-items: center; + cursor: pointer; + justify-content: space-between; +} + +.placeholderSeparator { + width: 10px; +} + +.dropdownPlaceholderTitle{ + display: flex; + flex: 1; + align-items: center; + cursor: pointer; + font-size: 18px; +} + +.dropdownTitle { + height: 100%; +} + +.stackDropdown div { + height: 100%; +} + +.stackDropdown span { + height: 100%; + background-color: #f2f2f2; + border: 0px; + padding: 0px; + margin: 0px; +} \ No newline at end of file diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx new file mode 100644 index 00000000..e8d900d7 --- /dev/null +++ b/frontend/src/components/Profile/index.tsx @@ -0,0 +1,110 @@ +import React, { useState, useEffect } from "react"; +import { Dropdown, DropdownMenuItemType, IDropdownOption, IDropdown } from "@fluentui/react/lib/Dropdown"; +import { Icon } from "@fluentui/react/lib/Icon"; +import { IStackTokens, Stack } from "@fluentui/react/lib/Stack"; +import { useAppContext } from "../../providers/AppProviders"; +import styles from "./Profile.module.css"; +import person from "../../assets/person.png"; + +const stackTokens: IStackTokens = { childrenGap: 20 }; +const iconStyles = { marginRight: "8px" }; + +const onRenderOption = (option: IDropdownOption): JSX.Element => { + return ( +
+ {option.data && option.data.icon &&
+ ); +}; + +// const onRenderTitle = (options: IDropdownOption[]): JSX.Element => { +// const option = options[0]; +// return ( +//
+// {option.data && option.data.icon &&
+// ); +// }; + +const onRenderCaretDown = (): JSX.Element => { + return <>; + // return ; +}; + +const onRenderPlaceholder = (props: any): JSX.Element => { + return ( +
+ +
   
+ {props.placeholder} +
+ ); +}; + +const placeholderPrepare = (placeholder: string) => { + // remove email domain + const email = placeholder.split("@")[0]; + // remove special characters + return email.replace(/[^a-zA-Z0-9]/g, ""); +}; + +export const ProfileButton: React.FunctionComponent = () => { + const { user } = useAppContext(); + + const placeholder = placeholderPrepare(user.name); + + const headerTitle = user.name; + + const options: IDropdownOption[] = [ + { key: "Header", text: headerTitle || "Options", itemType: DropdownMenuItemType.Header }, + { key: "Admin", text: "Admin Panel", data: { icon: "SecurityGroup" } }, + { key: "Logout", text: "Logout", data: { icon: "SkypeArrow" } } + ]; + + const dropdownRef = React.createRef(); + const onSetFocus = () => dropdownRef.current!.focus(true); + const [selectedOption, setSelectedOption] = useState(null); + + const clearOption = () => { + setSelectedOption(null); + onSetFocus(); + }; + + const onSelectOption = (event: React.FormEvent, option?: IDropdownOption, index?: number) => { + const selOption = option?.key.toString() ?? null; + setSelectedOption(selOption); + + if (selOption === "Logout") { + // retrieve all cookies + var Cookies = document.cookie.split(";"); + // set past expiry to all cookies + for (var i = 0; i < Cookies.length; i++) { + document.cookie = Cookies[i] + "=; expires=" + new Date(0).toUTCString(); + } + window.location.reload(); + } else if (selOption === "Admin") { + // go to admin panel + console.log("Admin Panel"); + } + }; + + return ( + + + + ); +}; From 09cb8dd9affa31c50bc6e1479a1d54d405dd9166 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Tue, 30 Apr 2024 16:42:05 -0400 Subject: [PATCH 039/820] SFC-22-RBAC-ui-fix --- frontend/src/components/Profile/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index e8d900d7..40ce923d 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -9,7 +9,7 @@ import person from "../../assets/person.png"; const stackTokens: IStackTokens = { childrenGap: 20 }; const iconStyles = { marginRight: "8px" }; -const onRenderOption = (option: IDropdownOption): JSX.Element => { +const onRenderOption = (option: any): JSX.Element => { return (
{option.data && option.data.icon &&
); }; From 9a63e825d41fb7685fdd6d4d775c7b5634ac1e2e Mon Sep 17 00:00:00 2001 From: Kenyer Ramirez <124545052+KenyerRamirez@users.noreply.github.com> Date: Tue, 7 May 2024 18:11:07 -0400 Subject: [PATCH 045/820] SFC 175 History retrieval working (#38) * history retrieval working * comments resolved * comment resolved --- backend/app.py | 8 ++++---- frontend/src/api/api.ts | 33 ++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/backend/app.py b/backend/app.py index b84b8a5c..22eccfaa 100644 --- a/backend/app.py +++ b/backend/app.py @@ -86,7 +86,7 @@ def chatgpt(): logging.exception("[webbackend] exception in /chatgpt") return jsonify({"error": str(e)}), 500 -@app.route("/api/get-chat-history", methods=["GET"]) +@app.route("/api/chat-history", methods=["GET"]) def getChatHistory(): client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') try: @@ -110,10 +110,10 @@ def getChatHistory(): logging.info(f"[webbackend] response: {response.text[:500]}...") return(response.text) except Exception as e: - logging.exception("[webbackend] exception in /get-chat-history") + logging.exception("[webbackend] exception in /chat-history") return jsonify({"error": str(e)}), 500 -@app.route("/api/get-chat-conversation/", methods=["GET"]) +@app.route("/api/chat-conversation/", methods=["GET"]) def getChatConversation(chat_id): if chat_id is None: @@ -143,7 +143,7 @@ def getChatConversation(chat_id): logging.exception("[webbackend] exception in /get-chat-history") return jsonify({"error": str(e)}), 500 -@app.route("/api/conversations/", methods=["DELETE"]) +@app.route("/api/chat-conversations/", methods=["DELETE"]) def deleteChatConversation(chat_id): try: client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 6a2cdc22..74202b8d 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -101,7 +101,7 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise { - const response = await fetch(`/api/get-chat-conversation/${chatId}`, { + const response = await fetch(`/api/chat-conversation/${chatId}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -110,24 +110,26 @@ export async function getChatFromHistoryPannelById(chatId: string, userId: strin }); const responseData = await response.json(); - const history = responseData.history; + const messages = responseData.messages; const conversationItems: ChatTurn[] = []; let currentUserMessage = ""; let currentBotMessage = ""; - history.forEach((item: any) => { - if (item.role === "user") { - currentUserMessage = item.content; - } else if (item.role === "assistant") { - currentBotMessage = item.content; - if (currentUserMessage !== "" || currentBotMessage !== "") { - conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); - currentUserMessage = ""; - currentBotMessage = ""; + if (messages) { + messages.forEach((item: any) => { + if (item.role === "user") { + currentUserMessage = item.content; + } else if (item.role === "assistant") { + currentBotMessage = item.content; + if (currentUserMessage !== "" || currentBotMessage !== "") { + conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); + currentUserMessage = ""; + currentBotMessage = ""; + } } - } - }); + }); + } if (currentUserMessage !== "" || currentBotMessage !== "") { conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); @@ -136,9 +138,10 @@ export async function getChatFromHistoryPannelById(chatId: string, userId: strin return conversationItems; } + export async function deleteChatConversation(chatId: string, userId: string): Promise { try { - const response = await fetch(`/api/conversations/${chatId}`, { + const response = await fetch(`/api/chat-conversations/${chatId}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -155,7 +158,7 @@ export async function deleteChatConversation(chatId: string, userId: string): Pr } export async function getChatHistory(userId: string): Promise { - const response = await fetch("/api/get-chat-history", { + const response = await fetch("/api/chat-history", { method: "GET", headers: { "Content-Type": "application/json", From ac9f8f1084f07b03d10427156829116b8a0463e3 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Tue, 14 May 2024 14:55:01 -0400 Subject: [PATCH 046/820] SFC-193 fix web app crash on source click (#39) --- frontend/src/pages/chat/Chat.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 35320161..5fa7cb0a 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -270,6 +270,18 @@ const Chat = () => { } else { setPlaceholderText("Write your question here"); } + + // fill answers with data from dataConversation + setAnswers( + dataConversation.map(data => [ + data.user, + { + answer: data.bot || "", + data_points: [], + thoughts: null + } + ]) + ); }, [isLoading, dataConversation]); const onPromptTemplateChange = (_ev?: React.FormEvent, newValue?: string) => { @@ -483,7 +495,7 @@ const Chat = () => { />
- {(answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( + {(answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && answers[selectedAnswer] && ( { onToggleTab(x, selectedAnswer); }} citationHeight="810px" - answer={answers[selectedAnswer][1]} + answer={answers[selectedAnswer]?.[1]} activeTab={activeAnalysisPanelTab} fileType={fileType} onHideTab={hideTab} From 3fd9c4dda353fd7fc7a30e24cae20aae404ce1cd Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Thu, 23 May 2024 14:18:02 -0400 Subject: [PATCH 047/820] SFC-84 handle error messages on conversation delete (#40) --- .../HistoryPannel/ChatHistoryListItem.tsx | 8 +- package-lock.json | 84 +++++++++++++++++++ package.json | 5 ++ 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 package-lock.json create mode 100644 package.json diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index ab689db6..c4c03564 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -7,6 +7,8 @@ import pencil from "../../assets/pencil.png"; import yes from "../../assets/check.png"; import no from "../../assets/close.png"; import { Spinner } from "@fluentui/react"; +import { ToastContainer, toast } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; interface ChatHistoryPanelProps { onDeleteChat: () => void; @@ -101,10 +103,13 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete } const updatedDataHistory = dataHistory.filter(item => item.id !== chatConversationId); setDataHistory(updatedDataHistory); + + toast("Conversation deleted successfully", { type: "success" }); } catch (error) { console.error("Error deleting conversation:", error); setDeletingIsLoading(false); - setErrorMessage(`Was an error deleting conversation: ${error}`); + setErrorMessage(`We run into an error deleting the conversation, please contact the system administrator.`); + toast("Conversation could not be deleted", { type: "error" }); } }; @@ -152,6 +157,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete return (
+ {isLoading && (
diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..8342cbf7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,84 @@ +{ + "name": "gpt-rag-frontend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "react-toastify": "^10.0.5" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "peer": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..e423c7ef --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "react-toastify": "^10.0.5" + } +} From f9efceee40c7efcda3296030b37140847ed1318c Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 23 May 2024 14:22:25 -0400 Subject: [PATCH 048/820] SFC-208 adapt sources to new format --- frontend/src/api/api.ts | 3 +++ frontend/src/components/Answer/Answer.tsx | 10 +--------- frontend/src/pages/chat/Chat.tsx | 3 +++ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 74202b8d..09d8a7a8 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -192,6 +192,9 @@ export function getCitationFilePath(citation: string): string { } export function getFilePath(fileUrl: string) { + if(!fileUrl.endsWith(".pdf") || !fileUrl.endsWith(".docx") || fileUrl.endsWith(".doc")) { + return fileUrl; + } const regex = /documents\/(.*)/; const match = fileUrl.match(regex); let filepath = ""; diff --git a/frontend/src/components/Answer/Answer.tsx b/frontend/src/components/Answer/Answer.tsx index a15e7f02..939be10d 100644 --- a/frontend/src/components/Answer/Answer.tsx +++ b/frontend/src/components/Answer/Answer.tsx @@ -66,14 +66,6 @@ export const Answer = ({ onClick={() => onThoughtProcessClicked()} disabled={!answer.thoughts} /> - {/* onSupportingContentClicked()} - disabled={!answer.data_points.length} - /> */}
@@ -84,7 +76,7 @@ export const Answer = ({ {!!parsedAnswer.citations.length && showSources && ( - + {citation_label_text}: {parsedAnswer.citations.map((url, i) => { const path = getFilePath(url); diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 5fa7cb0a..94500ebe 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -313,6 +313,9 @@ const Chat = () => { // }; const onShowCitation = async (citation: string, fileName: string, index: number) => { + if(!citation.endsWith(".pdf") && !citation.endsWith(".doc") && !citation.endsWith(".docx")){ + return window.open(citation, '_blank'); + } const response = await getPdf(fileName); if (activeCitation === citation && activeAnalysisPanelTab === AnalysisPanelTabs.CitationTab && selectedAnswer === index) { setActiveAnalysisPanelTab(undefined); From 02e20fd834e18cc2e7a36fe68f4bcbcf3f53bbe3 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Thu, 23 May 2024 17:20:00 -0400 Subject: [PATCH 049/820] SFC-186 hide presence and frequency penalty from settings (#42) * SFC-186 hide presence and frequency penalty from settings * removed comments * fix typo --- backend/app.py | 16 +++---- frontend/src/api/api.ts | 4 +- frontend/src/api/models.ts | 2 - .../HistoryPannel/ChatHistoryListItem.tsx | 2 +- .../src/components/SettingsPanel/index.tsx | 46 ++----------------- 5 files changed, 13 insertions(+), 57 deletions(-) diff --git a/backend/app.py b/backend/app.py index 22eccfaa..cda73878 100644 --- a/backend/app.py +++ b/backend/app.py @@ -253,13 +253,11 @@ def setSettings(): if not client_principal_id or not client_principal_name: return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 - temperature = request.json["temperature"] - presence_penalty = request.json["presence_penalty"] - frequency_penalty = request.json["frequency_penalty"] - - if not temperature or not presence_penalty or not frequency_penalty: - return jsonify({"error": "Missing required parameters, temperature, presence_penalty or frequency_penalty"}), 400 - + try: + temperature = float(request.json["temperature"]) + except: + temperature = 0 + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. @@ -275,8 +273,6 @@ def setSettings(): "client_principal_id": client_principal_id, "client_principal_name": client_principal_name, "temperature": temperature, - "presence_penalty": presence_penalty, - "frequency_penalty": frequency_penalty }) headers = { 'Content-Type': 'application/json', @@ -306,7 +302,7 @@ def setFeedback(): rating = request.json["rating"] if not conversation_id or not question or not answer or not category: - return jsonify({"error": "Missing required parameters, temperature, presence_penalty or frequency_penalty"}), 400 + return jsonify({"error": "Missing required parameters conversation_id, question, answer or category"}), 400 try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 09d8a7a8..076cdf0d 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -42,7 +42,7 @@ export async function getSettings({ user }: GetSettingsProps): Promise { } } -export async function postSettings({ user, temperature, presence_penalty, frequency_penalty }: PostSettingsProps): Promise { +export async function postSettings({ user, temperature }: PostSettingsProps): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; try { @@ -55,8 +55,6 @@ export async function postSettings({ user, temperature, presence_penalty, freque }, body: JSON.stringify({ temperature, - presence_penalty, - frequency_penalty }) }); const fetchedData = await response.json(); diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index bece5eb6..345bb960 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -97,6 +97,4 @@ export type PostSettingsProps = { name: string; } | null; temperature: number; - presence_penalty: number; - frequency_penalty: number; } diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index c4c03564..2a23f08b 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -108,7 +108,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete } catch (error) { console.error("Error deleting conversation:", error); setDeletingIsLoading(false); - setErrorMessage(`We run into an error deleting the conversation, please contact the system administrator.`); + setErrorMessage(`We ran into an error deleting the conversation, please contact the system administrator.`); toast("Conversation could not be deleted", { type: "error" }); } }; diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index ac9f06fa..4fe58698 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -35,16 +35,10 @@ export const SettingsPanel = () => { const { user, setSettingsPanel } = useAppContext(); const [temperature, setTemperature] = useState("0"); - const [presencePenalty, setPresencePenalty] = useState("0"); - const [frequencyPenalty, setFrequencyPenalty] = useState("0"); const [loading, setLoading] = useState(true); const temperatureDialog = "It adjusts the balance between creativity and predictability in responses. Lower settings yield straightforward answers, while higher settings introduce originality and diversity, perfect for creative tasks and factual inquiries."; - const frequencyPenaltyDialog = - "Streamlines dialogue by minimizing repetition. Increase to boost variety and prevent redundancy; decrease to ensure key terms recur, enhancing focus on specific concepts. Use it to decrease excessive repetition."; - const presencePenaltyDialog = - "Promotes the introduction of new topics and ideas. Increase to discover varied concepts; decrease to maintain focus on current discussions. Ideal for brainstorming and exploration."; useEffect(() => { const fetchData = async () => { @@ -56,8 +50,6 @@ export const SettingsPanel = () => { }) .then(data => { setTemperature(data.temperature); - setPresencePenalty(data.presencePenalty); - setFrequencyPenalty(data.frequencyPenalty); setLoading(false); }) .catch(error => setLoading(false)); @@ -68,20 +60,16 @@ export const SettingsPanel = () => { }, []); const handleSubmit = () => { - if ((!temperature && temperature != "0") || (!presencePenalty && presencePenalty != "0") || (!frequencyPenalty && frequencyPenalty != "0")) { - console.error("Invalid settings are not submitted."); + const parsedTemperature = parseFloat(temperature); + + if (parsedTemperature < 0 || parsedTemperature > 1) { + console.error("Invalid, settings are not submitted."); return; } - const parsedTemperature = parseFloat(temperature); - const parsedPresencePenalty = parseFloat(presencePenalty); - const parsedFrequencyPenalty = parseFloat(frequencyPenalty); - postSettings({ user, - temperature: parsedTemperature, - presence_penalty: parsedPresencePenalty, - frequency_penalty: parsedFrequencyPenalty + temperature: parsedTemperature }); }; @@ -105,16 +93,6 @@ export const SettingsPanel = () => { } }; - const handleSetPresencePenalty = useCallback((ev?: React.FormEvent, checked?: boolean): void => { - const presencePenalty = !!checked ? "1" : "0"; - setPresencePenalty(presencePenalty); - }, []); - - const handleSetFrequencyPenalty = useCallback((ev?: React.FormEvent, checked?: boolean): void => { - const frequencyPenalty = !!checked ? "1" : "0"; - setFrequencyPenalty(frequencyPenalty); - }, []); - const onRenderLabel = (dialog: string, title: string) => ( { onChange={e => handleSetTemperature(e)} />
-
-
- Variety Boost - -
- {onRenderLabel(frequencyPenaltyDialog, "Frequency Penalty")} -
-
-
- Topic Explorer - -
- {onRenderLabel(presencePenaltyDialog, "Presence Penalty")} -
  Save From 2d07bf9146e2ce75033e2751c26c918c6505e7e0 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Fri, 24 May 2024 15:35:08 -0400 Subject: [PATCH 050/820] SFC-207 add thought process --- frontend/src/api/api.ts | 7 +++++-- frontend/src/api/models.ts | 5 ++++- frontend/src/pages/chat/Chat.tsx | 25 +++++++++++++------------ 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 076cdf0d..bd9c0f3b 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -113,6 +113,7 @@ export async function getChatFromHistoryPannelById(chatId: string, userId: strin const conversationItems: ChatTurn[] = []; let currentUserMessage = ""; let currentBotMessage = ""; + let currentBotThoughts: string[] = []; if (messages) { messages.forEach((item: any) => { @@ -120,17 +121,19 @@ export async function getChatFromHistoryPannelById(chatId: string, userId: strin currentUserMessage = item.content; } else if (item.role === "assistant") { currentBotMessage = item.content; + currentBotThoughts = item.thoughts; if (currentUserMessage !== "" || currentBotMessage !== "") { - conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); + conversationItems.push({ user: currentUserMessage, bot: { message: currentBotMessage, thoughts: currentBotThoughts } }); currentUserMessage = ""; currentBotMessage = ""; + currentBotThoughts = []; } } }); } if (currentUserMessage !== "" || currentBotMessage !== "") { - conversationItems.push({ user: currentUserMessage, bot: currentBotMessage }); + conversationItems.push({ user: currentUserMessage, bot: { message: currentBotMessage, thoughts: currentBotThoughts } }); } return conversationItems; diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 345bb960..4f526af2 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -67,7 +67,10 @@ export type AskResponseGpt= { export type ChatTurn = { user: string; - bot?: string; + bot?: { + message: string; + thoughts: any; + } | null; }; export type ChatRequest = { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 94500ebe..b626dbf1 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -90,7 +90,7 @@ const Chat = () => { if (dataConversation.length > 0) { history.push(...dataConversation); } else { - history.push(...answers.map(a => ({ user: a[0], bot: a[1].answer }))); + history.push(...answers.map(a => ({ user: a[0], bot: { message: a[1]?.answer, thoughts: a[1]?.thoughts || [] } }))); } history.push({ user: question, bot: undefined }); const request: ChatRequestGpt = { @@ -121,9 +121,9 @@ const Chat = () => { answer: result.answer || "", conversation_id: chatId, data_points: [""], - thoughts: null + thoughts: result.thoughts || [] } as AskResponse; - setDataConversation([...dataConversation, { user: question, bot: response.answer }]); + setDataConversation([...dataConversation, { user: question, bot: { message: response.answer, thoughts: response.thoughts } }]); // Voice Synthesis if (speechSynthesisEnabled) { @@ -276,9 +276,9 @@ const Chat = () => { dataConversation.map(data => [ data.user, { - answer: data.bot || "", + answer: data?.bot?.message || "", data_points: [], - thoughts: null + thoughts: data?.bot?.thoughts || [] } ]) ); @@ -313,8 +313,8 @@ const Chat = () => { // }; const onShowCitation = async (citation: string, fileName: string, index: number) => { - if(!citation.endsWith(".pdf") && !citation.endsWith(".doc") && !citation.endsWith(".docx")){ - return window.open(citation, '_blank'); + if (!citation.endsWith(".pdf") && !citation.endsWith(".doc") && !citation.endsWith(".docx")) { + return window.open(citation, "_blank"); } const response = await getPdf(fileName); if (activeCitation === citation && activeAnalysisPanelTab === AnalysisPanelTabs.CitationTab && selectedAnswer === index) { @@ -339,13 +339,14 @@ const Chat = () => { setSelectedAnswer(index); }; - const answerFromHistory = dataConversation.map(data => data.bot); + const answerFromHistory = dataConversation.map(data => data.bot?.message); + const thoughtsFromHistory = dataConversation.map(data => data.bot?.thoughts); const responseForPreviewPanel = { answer: answerFromHistory.toString(), conversation_id: chatId, data_points: [""], - thoughts: null + thoughts: thoughtsFromHistory.toString() } as AskResponse; // const onShowCitation = (citation: string, index: number) => { @@ -406,10 +407,10 @@ const Chat = () => { {dataConversation.length > 0 ? dataConversation.map((item, index) => { const response = { - answer: item.bot || "", + answer: item.bot?.message || "", conversation_id: chatId, data_points: [""], - thoughts: null + thoughts: item.bot?.thoughts || [] } as AskResponse; return (
@@ -498,7 +499,7 @@ const Chat = () => { />
- {(answers.length > 0 && fileType !== "" && activeAnalysisPanelTab && answers[selectedAnswer] && ( + {(answers.length > 0 && activeAnalysisPanelTab && answers[selectedAnswer] && ( Date: Tue, 28 May 2024 17:37:49 -0400 Subject: [PATCH 051/820] SFC-218-confirmation-message --- .../src/components/SettingsPanel/index.tsx | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 4fe58698..ef2bae7f 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -7,6 +7,7 @@ import { getSettings, postSettings } from "../../api/api"; import { mergeStyles } from "@fluentui/react/lib/Styling"; import { useAppContext } from "../../providers/AppProviders"; import { ProfileButton } from "../Profile"; +import { Dialog, DialogContent, PrimaryButton } from "@fluentui/react"; interface Props { user: { @@ -31,12 +32,51 @@ const itemClass = mergeStyles({ padding: "10px 0" }); +const ConfirmationDialog = ({ isOpen, onDismiss, onConfirm }: { isOpen: boolean; onDismiss: () => void; onConfirm: () => void }) => { + return ( + + ); +}; + export const SettingsPanel = () => { const { user, setSettingsPanel } = useAppContext(); const [temperature, setTemperature] = useState("0"); const [loading, setLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const temperatureDialog = "It adjusts the balance between creativity and predictability in responses. Lower settings yield straightforward answers, while higher settings introduce originality and diversity, perfect for creative tasks and factual inquiries."; @@ -51,6 +91,7 @@ export const SettingsPanel = () => { .then(data => { setTemperature(data.temperature); setLoading(false); + setIsDialogOpen(false); }) .catch(error => setLoading(false)); }; @@ -117,6 +158,15 @@ export const SettingsPanel = () => { return (
+ { + setIsDialogOpen(false); + }} + onConfirm={() => { + handleSubmit(); + }} + />
@@ -160,7 +210,7 @@ export const SettingsPanel = () => { onChange={e => handleSetTemperature(e)} />
- + setIsDialogOpen(true)}>   Save From 5a1fa1023d96881e52fc0ef10c007f9c0844968c Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 29 May 2024 12:52:23 -0400 Subject: [PATCH 052/820] SFC-218-fix --- .../src/components/SettingsPanel/index.tsx | 51 ++++++++++++------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index ef2bae7f..6a1c885a 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -32,7 +32,7 @@ const itemClass = mergeStyles({ padding: "10px 0" }); -const ConfirmationDialog = ({ isOpen, onDismiss, onConfirm }: { isOpen: boolean; onDismiss: () => void; onConfirm: () => void }) => { +const ConfirmationDialog = ({ loading, isOpen, onDismiss, onConfirm }: { loading: boolean; isOpen: boolean; onDismiss: () => void; onConfirm: () => void }) => { return ( ); }; @@ -74,6 +85,7 @@ export const SettingsPanel = () => { const [temperature, setTemperature] = useState("0"); const [loading, setLoading] = useState(true); + const [isLoadingSettings, setIsLoadingSettings] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -91,11 +103,9 @@ export const SettingsPanel = () => { .then(data => { setTemperature(data.temperature); setLoading(false); - setIsDialogOpen(false); }) .catch(error => setLoading(false)); }; - setLoading(true); fetchData(); }, []); @@ -111,6 +121,9 @@ export const SettingsPanel = () => { postSettings({ user, temperature: parsedTemperature + }).then(() => { + setIsDialogOpen(false); + setIsLoadingSettings(false); }); }; @@ -159,11 +172,13 @@ export const SettingsPanel = () => { return (
{ setIsDialogOpen(false); }} onConfirm={() => { + setIsLoadingSettings(true); handleSubmit(); }} /> From ab08de87e409672a5e40c77615cec5a45a74e73c Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 29 May 2024 14:43:13 -0400 Subject: [PATCH 053/820] SFC-218-fix-get-settings --- frontend/src/api/api.ts | 1 - frontend/src/components/SettingsPanel/index.tsx | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index bd9c0f3b..261a5e12 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -58,7 +58,6 @@ export async function postSettings({ user, temperature }: PostSettingsProps): Pr }) }); const fetchedData = await response.json(); - console.log("Settings posted", fetchedData); return fetchedData; } catch (error) { console.error("Error posting settings", error); diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 6a1c885a..ec6626d7 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -108,7 +108,7 @@ export const SettingsPanel = () => { }; setLoading(true); fetchData(); - }, []); + }, [user.id]); const handleSubmit = () => { const parsedTemperature = parseFloat(temperature); @@ -121,7 +121,8 @@ export const SettingsPanel = () => { postSettings({ user, temperature: parsedTemperature - }).then(() => { + }).then((data) => { + setTemperature(data.temperature); setIsDialogOpen(false); setIsLoadingSettings(false); }); From 34460a0a30325eeb2e82d8564718e1be42553449 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 29 May 2024 17:43:37 -0400 Subject: [PATCH 054/820] change useEffect --- frontend/src/components/SettingsPanel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index ec6626d7..dd7b1cbb 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -108,7 +108,7 @@ export const SettingsPanel = () => { }; setLoading(true); fetchData(); - }, [user.id]); + }, []); const handleSubmit = () => { const parsedTemperature = parseFloat(temperature); From 8e6c5369dd1c7171681e1ab6192b60f9da9305b3 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 5 Jun 2024 09:22:43 -0400 Subject: [PATCH 055/820] SFC-237-change-loading-message --- frontend/src/components/DocView/DocView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/DocView/DocView.tsx b/frontend/src/components/DocView/DocView.tsx index 8a12d71a..5771def5 100644 --- a/frontend/src/components/DocView/DocView.tsx +++ b/frontend/src/components/DocView/DocView.tsx @@ -40,7 +40,7 @@ const DocView: React.FC = ({ base64Doc, page, fileType }) => ) : ( -
Cargando Documento...
+
Loading Document...
)}
); From 3aab08015df212cee602cd9a4792815ecd420361 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 6 Jun 2024 09:41:07 -0400 Subject: [PATCH 056/820] SFC-238-fix-input-text-box --- frontend/src/components/QuestionInput/QuestionInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index f829b4cf..4f751b1e 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -71,7 +71,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr const onQuestionChange = (_ev: React.FormEvent, newValue?: string) => { if (!newValue) { setQuestion(""); - } else if (newValue.length <= 1000) { + } else { setQuestion(newValue); } }; From 150c9c62e6f75f030bf2f4494a907ce2768f7a70 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 6 Jun 2024 14:04:12 -0400 Subject: [PATCH 057/820] SFC-fix-loading --- frontend/src/components/AnalysisPanel/AnalysisPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx index 03f65ebe..d5207391 100644 --- a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx +++ b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -65,7 +65,7 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh headerText="Citation" headerButtonProps={isDisabledCitationTab ? pivotItemDisabledStyle : undefined} > - Cargando...

}> + Loading...

}>
From d4218d7ea05fe2ecd007def3c953c56c73756a98 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Tue, 11 Jun 2024 11:01:21 -0400 Subject: [PATCH 058/820] SFC-243 fix username not showing --- frontend/src/components/Profile/index.tsx | 4 ++-- frontend/src/pages/chat/Chat.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index e28606b1..4df0c1f7 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -47,7 +47,7 @@ const placeholderPrepare = (placeholder: string) => { // remove email domain const email = placeholder.split("@")[0]; // remove special characters - return email.replace(/[^a-zA-Z0-9]/g, ""); + return email.replace(/[^a-zA-Z0-9\s]/g, ""); }; export const ProfileButton: React.FunctionComponent = () => { @@ -77,7 +77,7 @@ export const ProfileButton: React.FunctionComponent = () => { setSelectedOption(selOption); if (selOption === "Logout") { - window.location.href = "/.auth/logout"; + window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; } else if (selOption === "Admin") { // go to admin panel console.log("Admin Panel"); diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index b626dbf1..325279d6 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -226,8 +226,8 @@ const Chat = () => { // setShowAuthMessage(false); console.log("User info found.", userInfoList); - const keyId = "http://schemas.microsoft.com/identity/claims/objectidentifier"; - const keyName = "preferred_username"; + const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; + const keyName = "name"; const _user = userInfoList.find(obj => { const _id = obj?.user_claims?.some(claim => claim.typ === keyId); From f1256f42c4070b22b647707f1151f4f75063125f Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 17 Jun 2024 13:59:29 -0400 Subject: [PATCH 059/820] Sfc 158 admin user interface (#52) * SFC-158-admin-user-interface start admin page * Modify userList --- frontend/src/index.tsx | 5 + frontend/src/pages/admin/Admin.module.css | 20 +++ frontend/src/pages/admin/Admin.tsx | 168 ++++++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 frontend/src/pages/admin/Admin.module.css create mode 100644 frontend/src/pages/admin/Admin.tsx diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index f68d468f..2dc38654 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,6 +8,7 @@ import "./index.css"; import Layout from "./pages/layout/Layout"; import NoPage from "./pages/NoPage"; import Chat from "./pages/chat/Chat"; +import Admin from "./pages/admin/Admin"; import { AppProvider } from "./providers/AppProviders"; initializeIcons(); @@ -21,6 +22,10 @@ export default function App() { } /> } /> + }> + } /> + } /> + diff --git a/frontend/src/pages/admin/Admin.module.css b/frontend/src/pages/admin/Admin.module.css new file mode 100644 index 00000000..d5bbffc5 --- /dev/null +++ b/frontend/src/pages/admin/Admin.module.css @@ -0,0 +1,20 @@ +.page_container{ + display: flex; + flex-direction: column; + width: 100%; + padding: 20px; +} + +.separator{ + font-size: '24px'; + height: '24px'; + width: '24px'; +} + +.row{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items:center +} \ No newline at end of file diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx new file mode 100644 index 00000000..97143a62 --- /dev/null +++ b/frontend/src/pages/admin/Admin.tsx @@ -0,0 +1,168 @@ +import React, { ReactNode } from "react"; +import { PrimaryButton, IconButton } from "@fluentui/react"; +import { Announced } from "@fluentui/react/lib/Announced"; +import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; +import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from "@fluentui/react/lib/DetailsList"; +import { MarqueeSelection } from "@fluentui/react/lib/MarqueeSelection"; +import { mergeStyles } from "@fluentui/react/lib/Styling"; + +import styles from "./Admin.module.css"; + +const exampleChildClass = mergeStyles({ + display: "block", + marginBottom: "10px" +}); + +const textFieldStyles: Partial = { root: { maxWidth: "300px" } }; + +export interface IUserListItem { + key: number; + name: string; + email: string; + role: string; + actions: ReactNode; + value: number; +} + +export interface IUserListState { + items: IUserListItem[]; + selectionDetails: string; +} + +export class UserList extends React.Component<{}, IUserListState> { + private _selection: Selection; + private _allItems: IUserListItem[]; + private _columns: IColumn[]; + + + constructor(props: {}) { + super(props); + + this._selection = new Selection({ + onSelectionChanged: () => this.setState({ selectionDetails: this._getSelectionDetails() }) + }); + + // Populate with items for demos. + this._allItems = []; + for (let i = 0; i < 10; i++) { + this._allItems.push({ + key: i, + name: "User " + i, + email: "example@mail.com", + role: "User", + actions: actions({}), + value: i + }); + } + + this._columns = [ + { key: "column1", name: "Name", fieldName: "name", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "column2", name: "Email", fieldName: "email", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "column3", name: "Role", fieldName: "role", minWidth: 100, maxWidth: 200, isResizable: true }, + { key: "column4", name: "Actions", fieldName: "actions", minWidth: 300, maxWidth: 400, isResizable: true } + ]; + + this.state = { + items: this._allItems, + selectionDetails: this._getSelectionDetails() + }; + } + + public render(): JSX.Element { + const { items, selectionDetails } = this.state; + + return ( +
+
{selectionDetails}
+ + + + + + +
+ ); + } + + private _getSelectionDetails(): string { + const selectionCount = this._selection.getSelectedCount(); + + switch (selectionCount) { + case 0: + return "No items selected"; + case 1: + return "1 item selected: " + (this._selection.getSelection()[0] as IUserListItem).name; + default: + return `${selectionCount} items selected`; + } + } + + private _onFilter = (ev: React.FormEvent, text: string | undefined): void => { + this.setState({ + items: text ? this._allItems.filter(i => i.name.toLowerCase().indexOf(text.toLowerCase()) > -1) : this._allItems + }); + }; + + private _onItemInvoked = (item: IUserListItem): void => { + alert(`Item invoked: ${item.name}`); + }; +} +const iconStyle = { + icon: { color: "black" }, + root: { + selectors: { + ":hover .ms-Button-icon": { + color: "rgb(0, 120, 212);" + } + } + } +}; + +const actions: React.FC = () => { + return ( +
+ {}} /> + {}} + /> + {}} + /> +
+ ); +}; + +const Admin: React.FC = () => { + return ( +
+
+

Roles and access

+ +
+
+ +
+
+ ); +}; + +export default Admin; From cb36001d2c80f94323682fceb3884670e5420117 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 18 Jun 2024 23:15:40 -0400 Subject: [PATCH 060/820] SFC-143-add-keyboard-shortcuts --- .../ChatHistoryButton/ChatHistoryButton.tsx | 5 +- .../FeedbackRating/FeedbackRating.tsx | 6 +- .../FeedbackRating/FeedbackRatingButton.tsx | 5 +- .../HistoryPannel/ChatHistoryListItem.tsx | 6 +- .../HistoryPannel/ChatHistoryPanel.tsx | 5 +- frontend/src/components/Profile/index.tsx | 6 +- .../src/components/SettingsPanel/index.tsx | 6 +- frontend/src/pages/chat/Chat.tsx | 33 +++++----- frontend/src/pages/layout/Layout.tsx | 35 ++-------- frontend/src/providers/AppProviders.tsx | 66 +++++++++++++++---- 10 files changed, 96 insertions(+), 77 deletions(-) diff --git a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx index 94d53410..8e1c4292 100644 --- a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx +++ b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx @@ -2,7 +2,8 @@ import { Text } from "@fluentui/react"; import { HistoryFilled } from "@fluentui/react-icons"; import styles from "./ChatHistoryButton.module.css"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; +import { useContext } from "react"; interface Props { className?: string; @@ -11,7 +12,7 @@ interface Props { } export const ChatHistoryButton = ({ className, disabled, onClick }: Props) => { - const {showHistoryPanel} = useAppContext() + const {showHistoryPanel} = useContext(AppContext) const buttonContent = showHistoryPanel ? "Hide chat history" : 'Show chat history' return (
+
diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index 2fc672a8..5f848950 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -17,6 +17,7 @@ display: flex; flex-direction: column; justify-content: flex-end; + padding-left: 4px; } .questionInputSendButton { diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 4f751b1e..e77e2e02 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -93,15 +93,29 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr
{ + if (ev.key === "Enter") { + ev.preventDefault(); + sendQuestion(); + } + }} + tabIndex={0} >
{ + if (ev.key === "Enter") { + ev.preventDefault(); + sttFromMic(); + } + }} + tabIndex={0} >
diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 21e283c4..fa294ffc 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -406,7 +406,7 @@ const Chat = () => { {conversationIsLoading && }
- + Sales Factory logo

Clew

Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver @@ -415,7 +415,7 @@ const Chat = () => {

) : ( -
+
{conversationIsLoading && } {dataConversation.length > 0 ? dataConversation.map((item, index) => { @@ -490,6 +490,7 @@ const Chat = () => { @@ -500,6 +501,7 @@ const Chat = () => { : styles.newChatButtonDisabled } onClick={handleNewChat} + aria-label="Start a new chat" > diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 18f59e4d..83787fc9 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -37,8 +37,8 @@ const Layout = () => {
- - + + Sales Factory logo

From bbd0b94d9fa16ae79432317d6f96063b1dd943ca Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Tue, 25 Jun 2024 09:06:27 -0400 Subject: [PATCH 073/820] SFC 271 Tab name and welcome message should change to FreddAid (#61) Co-authored-by: Manuel Castro --- frontend/index.html | 2 +- frontend/src/pages/chat/Chat.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index c2bc9a4b..bc0b53ab 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Clew + FreddAid
diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index fa294ffc..a66aba9e 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -406,8 +406,10 @@ const Chat = () => { {conversationIsLoading && }
+ Sales Factory logo -

Clew

+

FreddAid

+

Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver actionable insights. From 248434e8fc1c9929b829f72cc333e0c80a4f44fd Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 25 Jun 2024 11:54:52 -0400 Subject: [PATCH 074/820] add email configuration --- backend/app.py | 564 ++++++++++++++++++++++++++++------------ frontend/src/api/api.ts | 20 ++ 2 files changed, 415 insertions(+), 169 deletions(-) diff --git a/backend/app.py b/backend/app.py index 0d4eaa3a..fe2019f1 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,4 +1,5 @@ import os +import re import mimetypes import time import logging @@ -12,49 +13,66 @@ from azure.storage.blob import BlobServiceClient from urllib.parse import unquote +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + load_dotenv() -SPEECH_REGION = os.getenv('SPEECH_REGION') -ORCHESTRATOR_ENDPOINT = os.getenv('ORCHESTRATOR_ENDPOINT') -ORCHESTRATOR_URI = os.getenv('ORCHESTRATOR_URI', default="") +SPEECH_REGION = os.getenv("SPEECH_REGION") +ORCHESTRATOR_ENDPOINT = os.getenv("ORCHESTRATOR_ENDPOINT") +ORCHESTRATOR_URI = os.getenv("ORCHESTRATOR_URI", default="") SETTINGS_ENDPOINT = ORCHESTRATOR_URI + "/settings" FEEDBACK_ENDPOINT = ORCHESTRATOR_URI + "/feedback" HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/conversations" CHECK_USER_ENDPOINT = ORCHESTRATOR_URI + "/checkUser" -STORAGE_ACCOUNT = os.getenv('STORAGE_ACCOUNT') -LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper() +STORAGE_ACCOUNT = os.getenv("STORAGE_ACCOUNT") + +# email +EMAIL_HOST = os.getenv("EMAIL_HOST") +EMAIL_PASS = os.getenv("EMAIL_PASS") +EMAIL_USER = os.getenv("EMAIL_USER") +EMAIL_PORT = os.getenv("EMAIL_PORT") + +INVITATION_LINK = os.getenv("INVITATION_LINK") + +LOGLEVEL = os.environ.get("LOGLEVEL", "INFO").upper() logging.basicConfig(level=LOGLEVEL) + def get_secret(secretName): keyVaultName = os.environ["AZURE_KEY_VAULT_NAME"] KVUri = f"https://{keyVaultName}.vault.azure.net" credential = DefaultAzureCredential() client = SecretClient(vault_url=KVUri, credential=credential) - logging.info(f"[webbackend] retrieving {secretName} secret from {keyVaultName}.") + logging.info(f"[webbackend] retrieving {secretName} secret from {keyVaultName}.") retrieved_secret = client.get_secret(secretName) return retrieved_secret.value -SPEECH_KEY = get_secret('speechKey') -SPEECH_RECOGNITION_LANGUAGE = os.getenv('SPEECH_RECOGNITION_LANGUAGE') -SPEECH_SYNTHESIS_LANGUAGE = os.getenv('SPEECH_SYNTHESIS_LANGUAGE') -SPEECH_SYNTHESIS_VOICE_NAME = os.getenv('SPEECH_SYNTHESIS_VOICE_NAME') +SPEECH_KEY = get_secret("speechKey") + +SPEECH_RECOGNITION_LANGUAGE = os.getenv("SPEECH_RECOGNITION_LANGUAGE") +SPEECH_SYNTHESIS_LANGUAGE = os.getenv("SPEECH_SYNTHESIS_LANGUAGE") +SPEECH_SYNTHESIS_VOICE_NAME = os.getenv("SPEECH_SYNTHESIS_VOICE_NAME") app = Flask(__name__) CORS(app) + @app.route("/", defaults={"path": "index.html"}) @app.route("/") def static_file(path): return app.send_static_file(path) + @app.route("/chatgpt", methods=["POST"]) def chatgpt(): conversation_id = request.json["conversation_id"] question = request.json["query"] - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') - logging.info("[webbackend] conversation_id: " + conversation_id) + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") + logging.info("[webbackend] conversation_id: " + conversation_id) logging.info("[webbackend] question: " + question) logging.info(f"[webbackend] User principal: {client_principal_id}") logging.info(f"[webbackend] User name: {client_principal_name}") @@ -62,103 +80,108 @@ def chatgpt(): try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--functionKey' + keySecretName = "orchestrator-host--functionKey" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 - + logging.exception( + "[webbackend] exception in /api/orchestrator-host--functionKey" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + try: url = ORCHESTRATOR_ENDPOINT - payload = json.dumps({ - "conversation_id": conversation_id, - "question": question, - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name - }) - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } + payload = json.dumps( + { + "conversation_id": conversation_id, + "question": question, + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + } + ) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} response = requests.request("GET", url, headers=headers, data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /chatgpt") return jsonify({"error": str(e)}), 500 - + + @app.route("/api/chat-history", methods=["GET"]) def getChatHistory(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--conversations' + keySecretName = "orchestrator-host--conversations" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + logging.exception( + "[webbackend] exception in /api/orchestrator-host--functionKey" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) try: url = HISTORY_ENDPOINT - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } - payload = json.dumps({ - "user_id": client_principal_id - }) - response = requests.request("GET",url,headers=headers,data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + payload = json.dumps({"user_id": client_principal_id}) + response = requests.request("GET", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /chat-history") return jsonify({"error": str(e)}), 500 - + + @app.route("/api/chat-conversation/", methods=["GET"]) def getChatConversation(chat_id): if chat_id is None: return jsonify({"error": "Missing chatId parameter"}), 400 - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") try: - keySecretName = 'orchestrator-host--conversations' + keySecretName = "orchestrator-host--conversations" functionKey = get_secret(keySecretName) except Exception as e: return jsonify({"error": f"Error getting function key: {e}"}), 500 try: url = f"{HISTORY_ENDPOINT}/?id={chat_id}" - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } - payload = json.dumps({ - "user_id": client_principal_id - }) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + payload = json.dumps({"user_id": client_principal_id}) response = requests.request("GET", url, headers=headers, data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text, response.status_code except Exception as e: logging.exception("[webbackend] exception in /get-chat-history") return jsonify({"error": str(e)}), 500 + @app.route("/api/chat-conversations/", methods=["DELETE"]) def deleteChatConversation(chat_id): try: - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - keySecretName = 'orchestrator-host--conversations' + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + keySecretName = "orchestrator-host--conversations" functionKey = get_secret(keySecretName) url = f"{HISTORY_ENDPOINT}/?id={chat_id}" - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } - payload = json.dumps({ - "user_id": client_principal_id - }) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + payload = json.dumps({"user_id": client_principal_id}) response = requests.delete(url, headers=headers, data=payload) return response.text, response.status_code @@ -166,93 +189,132 @@ def deleteChatConversation(chat_id): logging.exception("[webbackend] exception in /delete-chat-conversation") return jsonify({"error": str(e)}), 500 + # methods to provide access to speech services and blob storage account blobs + @app.route("/api/get-speech-token", methods=["GET"]) def getGptSpeechToken(): try: - fetch_token_url = f"https://{SPEECH_REGION}.api.cognitive.microsoft.com/sts/v1.0/issueToken" + fetch_token_url = ( + f"https://{SPEECH_REGION}.api.cognitive.microsoft.com/sts/v1.0/issueToken" + ) headers = { - 'Ocp-Apim-Subscription-Key': SPEECH_KEY, - 'Content-Type': 'application/x-www-form-urlencoded' + "Ocp-Apim-Subscription-Key": SPEECH_KEY, + "Content-Type": "application/x-www-form-urlencoded", } response = requests.post(fetch_token_url, headers=headers) access_token = str(response.text) - return json.dumps({'token': access_token, 'region': SPEECH_REGION, 'speechRecognitionLanguage': SPEECH_RECOGNITION_LANGUAGE, 'speechSynthesisLanguage': SPEECH_SYNTHESIS_LANGUAGE, 'speechSynthesisVoiceName': SPEECH_SYNTHESIS_VOICE_NAME}) + return json.dumps( + { + "token": access_token, + "region": SPEECH_REGION, + "speechRecognitionLanguage": SPEECH_RECOGNITION_LANGUAGE, + "speechSynthesisLanguage": SPEECH_SYNTHESIS_LANGUAGE, + "speechSynthesisVoiceName": SPEECH_SYNTHESIS_VOICE_NAME, + } + ) except Exception as e: logging.exception("[webbackend] exception in /api/get-speech-token") return jsonify({"error": str(e)}), 500 + @app.route("/api/get-storage-account", methods=["GET"]) def getStorageAccount(): - if STORAGE_ACCOUNT is None or STORAGE_ACCOUNT == '': + if STORAGE_ACCOUNT is None or STORAGE_ACCOUNT == "": return jsonify({"error": "Add STORAGE_ACCOUNT to frontend app settings"}), 500 try: - return json.dumps({'storageaccount': STORAGE_ACCOUNT}) + return json.dumps({"storageaccount": STORAGE_ACCOUNT}) except Exception as e: logging.exception("[webbackend] exception in /api/get-storage-account") return jsonify({"error": str(e)}), 500 + @app.route("/api/get-blob", methods=["POST"]) def getBlob(): - logging.exception ("------------------ENTRA ------------") + logging.exception("------------------ENTRA ------------") blob_name = unquote(request.json["blob_name"]) try: client_credential = DefaultAzureCredential() blob_service_client = BlobServiceClient( - f"https://{STORAGE_ACCOUNT}.blob.core.windows.net", - client_credential + f"https://{STORAGE_ACCOUNT}.blob.core.windows.net", client_credential + ) + blob_client = blob_service_client.get_blob_client( + container="documents", blob=blob_name ) - blob_client = blob_service_client.get_blob_client(container='documents', blob=blob_name) blob_data = blob_client.download_blob() blob_text = blob_data.readall() - return Response(blob_text, content_type='application/octet-stream') + return Response(blob_text, content_type="application/octet-stream") except Exception as e: logging.exception("[webbackend] exception in /api/get-blob") logging.exception(blob_name) return jsonify({"error": str(e)}), 500 - + + @app.route("/api/settings", methods=["GET"]) def getSettings(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--settings' + keySecretName = "orchestrator-host--settings" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + logging.exception( + "[webbackend] exception in /api/orchestrator-host--functionKey" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) try: url = SETTINGS_ENDPOINT - payload = json.dumps({ - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name - }) - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } + payload = json.dumps( + { + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + } + ) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} response = requests.request("GET", url, headers=headers, data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /api/settings") return jsonify({"error": str(e)}), 500 + @app.route("/api/settings", methods=["POST"]) def setSettings(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) try: temperature = float(request.json["temperature"]) @@ -262,38 +324,53 @@ def setSettings(): try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--settings' + keySecretName = "orchestrator-host--settings" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--functionKey") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + logging.exception( + "[webbackend] exception in /api/orchestrator-host--functionKey" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) try: url = SETTINGS_ENDPOINT - payload = json.dumps({ - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name, - "temperature": temperature, - }) - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } + payload = json.dumps( + { + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + "temperature": temperature, + } + ) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} response = requests.request("POST", url, headers=headers, data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /api/settings") return jsonify({"error": str(e)}), 500 - + @app.route("/api/feedback", methods=["POST"]) def setFeedback(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) conversation_id = request.json["conversation_id"] question = request.json["question"] @@ -301,38 +378,51 @@ def setFeedback(): category = request.json["category"] feedback = request.json["feedback"] rating = request.json["rating"] - + if not conversation_id or not question or not answer or not category: - return jsonify({"error": "Missing required parameters conversation_id, question, answer or category"}), 400 - + return ( + jsonify( + { + "error": "Missing required parameters conversation_id, question, answer or category" + } + ), + 400, + ) + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--feedback' + keySecretName = "orchestrator-host--feedback" functionKey = get_secret(keySecretName) except Exception as e: logging.exception("[webbackend] exception in /api/orchestrator-host--feedback") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) try: url = FEEDBACK_ENDPOINT - payload = json.dumps({ - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name, - "conversation_id": conversation_id, - "question": question, - "answer": answer, - "category": category, - "feedback": feedback, - "rating": rating, - }) - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } + payload = json.dumps( + { + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + "conversation_id": conversation_id, + "question": question, + "answer": answer, + "category": category, + "feedback": feedback, + "rating": rating, + } + ) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} response = requests.request("POST", url, headers=headers, data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /api/feedback") return jsonify({"error": str(e)}), 500 @@ -340,73 +430,209 @@ def setFeedback(): @app.route("/api/getusers", methods=["GET"]) def getUsers(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 - + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--checkuser' + keySecretName = "orchestrator-host--checkuser" functionKey = get_secret(keySecretName) except Exception as e: logging.exception("[webbackend] exception in /api/orchestrator-host--checkuser") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) try: url = CHECK_USER_ENDPOINT - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} response = requests.request("GET", url, headers=headers) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /api/checkUser") return jsonify({"error": str(e)}), 500 - + + +@app.route("/api/inviteUser", methods=["POST"]) +def sendEmail(): + if ( + not request.json + or "username" not in request.json + or "email" not in request.json + ): + return jsonify({"error": "Missing username or email"}), 400 + + username = request.json["username"] + email = request.json["email"] + + # Validate email format + if not re.match(r"[^@]+@[^@]+\.[^@]+", email): + return jsonify({"error": "Invalid email format"}), 400 + + try: + # Email account credentials + gmail_user = EMAIL_USER + gmail_password = EMAIL_PASS + + # Email details + sent_from = gmail_user + to = [email] + subject = "SalesFactory Chatbot Invitation" + body = """ + + + + + Welcome to FreddAid - Your Marketing Powerhouse + + + +

+

Dear [Recipient's Name],

+

Congratulations!

+

You now have exclusive access to FreddAid, your new marketing powerhouse. Get ready to transform your approach to marketing and take your strategies to the next level.

+

Ready to Get Started?

+

Click the link below and follow the easy steps to create your FreddAid account:

+ Activate Your FreddAid Account Now +

Unlock FreddAid's full potential and start enjoying unparalleled insights, real-time data, and a high-speed advantage in all your marketing efforts.

+

If you need any assistance, our support team is here to help you every step of the way.

+

Welcome to the future of marketing. Welcome to FreddAid.

+ +
+ + + """.replace( + "[Recipient's Name]", username + ).replace( + "[link to activate account]", INVITATION_LINK + ) + + # Create a multipart message and set headers + message = MIMEMultipart() + message["From"] = sent_from + message["To"] = ", ".join(to) + message["Subject"] = subject + + # Add body to email + message.attach(MIMEText(body, "html")) + + # Connect to Gmail's SMTP server + server = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT) + server.ehlo() + server.login(gmail_user, gmail_password) + + # Send email + server.sendmail(sent_from, to, message.as_string()) + server.close() + + logging.error("Email sent!") + return jsonify({"message": "Email sent!"}) + except Exception as e: + logging.error("Something went wrong...", e) + return jsonify({"error": str(e)}), 500 + @app.route("/api/checkuser", methods=["POST"]) def checkUser(): - client_principal_id = request.headers.get('X-MS-CLIENT-PRINCIPAL-ID') - client_principal_name = request.headers.get('X-MS-CLIENT-PRINCIPAL-NAME') + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") if not client_principal_id or not client_principal_name: - return jsonify({"error": "Missing required parameters, client_principal_id or client_principal_name"}), 400 - + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = 'orchestrator-host--checkuser' + keySecretName = "orchestrator-host--checkuser" functionKey = get_secret(keySecretName) except Exception as e: logging.exception("[webbackend] exception in /api/orchestrator-host--checkuser") - return jsonify({"error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)"}), 500 + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) try: email = request.json["email"] url = CHECK_USER_ENDPOINT - payload = json.dumps({ - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name, - "id": client_principal_id, - "name": client_principal_name, - "email": email, - }) - headers = { - 'Content-Type': 'application/json', - 'x-functions-key': functionKey - } + payload = json.dumps( + { + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + "id": client_principal_id, + "name": client_principal_name, + "email": email, + } + ) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} response = requests.request("POST", url, headers=headers, data=payload) - logging.info(f"[webbackend] response: {response.text[:500]}...") - return(response.text) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text except Exception as e: logging.exception("[webbackend] exception in /api/checkUser") return jsonify({"error": str(e)}), 500 - + if __name__ == "__main__": - app.run(host='0.0.0.0', port=8000) + app.run(host="0.0.0.0", port=8000) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 8a62238e..8215f76f 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -288,3 +288,23 @@ export async function postFeedbackRating({ user, conversation_id, feedback_messa } }); } + +export async function inviteUser ({ username, email }: any): Promise { + try { + const response = await fetch("/api/inviteUser", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + username, + email + }) + }); + const fetchedData = await response.json(); + return fetchedData; + } catch (error) { + console.error("Error inviting user", error); + return {}; + } +} From 32bf45ac5d59a537a4ab82b87cf48bbf96ba09c6 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 25 Jun 2024 13:57:33 -0400 Subject: [PATCH 075/820] finish integration --- frontend/src/api/api.ts | 2 +- frontend/src/pages/admin/Admin.tsx | 45 +++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 8215f76f..a46cd8df 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -305,6 +305,6 @@ export async function inviteUser ({ username, email }: any): Promise { return fetchedData; } catch (error) { console.error("Error inviting user", error); - return {}; + return {error : error}; } } diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 8370beb9..dc1f2251 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -6,8 +6,9 @@ import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from "@fluentu import { MarqueeSelection } from "@fluentui/react/lib/MarqueeSelection"; import { mergeStyles } from "@fluentui/react/lib/Styling"; import { AppContext } from "../../providers/AppProviders"; +import DOMPurify from "dompurify"; -import { checkUser, getUsers } from "../../api"; +import { checkUser, getUsers, inviteUser } from "../../api"; import styles from "./Admin.module.css"; @@ -153,9 +154,12 @@ export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOp const [errorMessage, setErrorMessage] = useState(""); const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); const isValidated = () => { - if (!username || !email) { + const sanitizedUsername = DOMPurify.sanitize(username); + const sanitizedEmail = DOMPurify.sanitize(email); + if (!sanitizedUsername || !sanitizedEmail) { setErrorMessage("Please fill in all fields"); return false; } else if (!/\S+@\S+\.\S+/.test(email)) { @@ -168,9 +172,15 @@ export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOp const handleSubmit = () => { if (!isValidated()) return; setLoading(true); - alert("form submitted!"); - setLoading(false); - onDismiss(); + inviteUser({ username, email, role }).then(res => { + if (res.error) { + setErrorMessage(res.error); + } else { + setErrorMessage(""); + setLoading(false); + setSuccess(true); + } + }); }; const onUserNameChange = (_ev: React.FormEvent, newValue?: string) => { @@ -204,6 +214,7 @@ export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOp setErrorMessage(""); setLoading(false); setIsOpen(false); + setSuccess(false); }; const onConfirm = () => { @@ -228,7 +239,7 @@ export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOp styles: { main: { maxWidth: 450 } } }} > - {loading ? ( + {loading && ( - ) : ( + )} + {!success && !loading && (
)} + {success && ( + +
+

Invitation sent

+

+ An invitation has been sent to {email}. They will receive an email with a link to create an account. +

+
+
+ +
+
+ )} ); }; From b713faa73760f0a95a356615a7c555408a460981 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 25 Jun 2024 21:25:42 -0400 Subject: [PATCH 076/820] SFC-25-email-validation --- frontend/src/pages/admin/Admin.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index dc1f2251..294aac6e 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -147,7 +147,7 @@ const actions: React.FC = () => { ); }; -export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: React.Dispatch> }) => { +export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; setIsOpen: React.Dispatch>; users: never[] }) => { const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [role, setRole] = useState("user"); @@ -156,9 +156,7 @@ export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOp const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); - const isValidated = () => { - const sanitizedUsername = DOMPurify.sanitize(username); - const sanitizedEmail = DOMPurify.sanitize(email); + const isValidated = (sanitizedUsername: string, sanitizedEmail: string) => { if (!sanitizedUsername || !sanitizedEmail) { setErrorMessage("Please fill in all fields"); return false; @@ -169,8 +167,15 @@ export const CreateUserForm = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOp return true; }; + const alreadyExists = (sanitizedEmail: string) => { + return users.some((user: any) => user.data.email === sanitizedEmail); + }; + const handleSubmit = () => { - if (!isValidated()) return; + const sanitizedUsername = DOMPurify.sanitize(username); + const sanitizedEmail = DOMPurify.sanitize(email); + if (!isValidated(sanitizedUsername, sanitizedEmail)) return; + if (alreadyExists(sanitizedEmail)) return setErrorMessage("User with this email already exists"); setLoading(true); inviteUser({ username, email, role }).then(res => { if (res.error) { @@ -364,7 +369,7 @@ const Admin = () => { }} />
- +
{loading ? ( Date: Wed, 26 Jun 2024 09:44:43 -0400 Subject: [PATCH 077/820] SFC-279-link-invitation-color --- backend/app.py | 16 ++++++++++++++-- frontend/src/pages/layout/Layout.tsx | 14 ++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/backend/app.py b/backend/app.py index fe2019f1..5a8a8f4e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -524,12 +524,24 @@ def sendEmail(): } .cta-button { background-color: #337ab7; - color: #fff; + color: #fff !important; padding: 10px 20px; border-radius: 5px; text-align: center; display: inline-block; } + .cta-button:hover { + background-color: #23527c; + } + .cta-button a { + color: #fff !important; + } + .cta-button a:visited { + color: #fff !important; + } + .ii a[href] { + color: #fff !important; + } .footer { text-align: center; margin-top: 20px; @@ -546,7 +558,7 @@ def sendEmail(): Activate Your FreddAid Account Now

Unlock FreddAid's full potential and start enjoying unparalleled insights, real-time data, and a high-speed advantage in all your marketing efforts.

If you need any assistance, our support team is here to help you every step of the way.

-

Welcome to the future of marketing. Welcome to FreddAid.

+

Welcome to the future of marketing. Welcome to FreddAid.

diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 83787fc9..7bcf8650 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useState } from "react"; -import { Outlet, NavLink, Link } from "react-router-dom"; +import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; @@ -15,6 +15,8 @@ const Layout = () => { const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useContext(AppContext); + const { pathname } = useLocation(); + const handleShowHistoryPanel = () => { setShowHistoryPanel(!showHistoryPanel); setShowFeedbackRatingPanel(false); @@ -43,9 +45,13 @@ const Layout = () => {
- - - + {pathname === "/" && ( + <> + + + + + )}
From 541f666dca001422abe2d81059c12be191d7b968 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 26 Jun 2024 16:53:46 -0400 Subject: [PATCH 078/820] SFC-261-RBAC-admin-page --- frontend/src/index.tsx | 15 +++++++- frontend/src/pages/AccesDenied.tsx | 20 ++++++++++ frontend/src/pages/admin/Admin.tsx | 53 ++++++++++++++------------ frontend/src/pages/chat/Chat.tsx | 1 - frontend/src/router/ProtectedRoute.tsx | 20 ++++++++++ 5 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 frontend/src/pages/AccesDenied.tsx create mode 100644 frontend/src/router/ProtectedRoute.tsx diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 2dc38654..8017472b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,12 +1,14 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { HashRouter, Routes, Route } from "react-router-dom"; +import ProtectedRoute from "./router/ProtectedRoute"; import { initializeIcons } from "@fluentui/react"; import "./index.css"; import Layout from "./pages/layout/Layout"; import NoPage from "./pages/NoPage"; +import AccessDenied from "./pages/AccesDenied"; import Chat from "./pages/chat/Chat"; import Admin from "./pages/admin/Admin"; import { AppProvider } from "./providers/AppProviders"; @@ -23,7 +25,18 @@ export default function App() { } /> }> - } /> + + + + } + /> + } /> + + }> + } /> } /> diff --git a/frontend/src/pages/AccesDenied.tsx b/frontend/src/pages/AccesDenied.tsx new file mode 100644 index 00000000..33a35581 --- /dev/null +++ b/frontend/src/pages/AccesDenied.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const AccessDenied: React.FC = () => { + return ( +
+

Access Denied

+

You do not have permission to access this page.

+
+ ); +}; + +export default AccessDenied; \ No newline at end of file diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 294aac6e..5e8843e0 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -359,30 +359,35 @@ const Admin = () => { return (
-
-

Roles and access

- { - setIsOpen(true); - }} - /> -
- -
- {loading ? ( - - ) : ( - - )} -
+ {user.role !== "admin" &&

Access denied

} + {user.role === "admin" && ( + <> +
+

Roles and access

+ { + setIsOpen(true); + }} + /> +
+ +
+ {loading ? ( + + ) : ( + + )} +
+ + )}
); }; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index a66aba9e..d0f4c230 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -273,7 +273,6 @@ const Chat = () => { } if (triggered.current === false) { triggered.current = true; - console.log(triggered.current); } const language = navigator.language; if (language.startsWith("pt")) { diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx new file mode 100644 index 00000000..9e4123a7 --- /dev/null +++ b/frontend/src/router/ProtectedRoute.tsx @@ -0,0 +1,20 @@ +import React, { useContext, ReactNode } from 'react'; +import { AppContext } from "../providers/AppProviders"; +import { Navigate } from 'react-router-dom'; + +interface ProtectedRouteProps { + children: ReactNode; + allowedRoles: string[]; +} + +const ProtectedRoute = ({ children, allowedRoles }: ProtectedRouteProps) => { + const { user } = useContext(AppContext); + if (!user.role || !allowedRoles.includes(user.role)) { + console.log("Access Denied"); + return ; + } + console.log("Access Granted"); + return children; +}; + +export default ProtectedRoute; From acfab83689347ba63c6646044f44ccfe1e464c98 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 27 Jun 2024 08:31:19 -0400 Subject: [PATCH 079/820] delete console --- frontend/src/router/ProtectedRoute.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index 9e4123a7..fa1f7394 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -10,10 +10,8 @@ interface ProtectedRouteProps { const ProtectedRoute = ({ children, allowedRoles }: ProtectedRouteProps) => { const { user } = useContext(AppContext); if (!user.role || !allowedRoles.includes(user.role)) { - console.log("Access Denied"); return ; } - console.log("Access Granted"); return children; }; From 0e3b251d1f96464486cd5a20a62cee6ead35af99 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 27 Jun 2024 09:15:27 -0400 Subject: [PATCH 080/820] fix build command --- frontend/src/router/ProtectedRoute.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index fa1f7394..e42a9011 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -12,7 +12,7 @@ const ProtectedRoute = ({ children, allowedRoles }: ProtectedRouteProps) => { if (!user.role || !allowedRoles.includes(user.role)) { return ; } - return children; + return (<>{children}); }; export default ProtectedRoute; From 3405a0a84906fed5cbe76cfc44346ec5cee7455f Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 27 Jun 2024 09:53:22 -0400 Subject: [PATCH 081/820] resolve confilcts --- frontend/src/pages/admin/Admin.module.css | 18 ++++++++++++++++++ frontend/src/pages/admin/Admin.tsx | 14 ++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/frontend/src/pages/admin/Admin.module.css b/frontend/src/pages/admin/Admin.module.css index 753a5a01..083ee1b9 100644 --- a/frontend/src/pages/admin/Admin.module.css +++ b/frontend/src/pages/admin/Admin.module.css @@ -18,6 +18,24 @@ justify-content: space-between; justify-items:center } +.closeButton{ + position: fixed; + top: 0; + right: 0px; + border: none; + font-size: 25px; + color: black; + background-color: transparent; + cursor: pointer; + transform: rotate(45deg); + margin-bottom: -5px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} .questionInputContainer { border-radius: 8px; box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 5e8843e0..38ad2293 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, ReactNode, useContext } from "react"; import { PrimaryButton, IconButton, Spinner, Dialog, DialogContent, Label, Dropdown, DefaultButton, MessageBar } from "@fluentui/react"; import { Announced } from "@fluentui/react/lib/Announced"; import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; +import { AddFilled } from "@fluentui/react-icons"; import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from "@fluentui/react/lib/DetailsList"; import { MarqueeSelection } from "@fluentui/react/lib/MarqueeSelection"; import { mergeStyles } from "@fluentui/react/lib/Styling"; @@ -362,6 +363,19 @@ const Admin = () => { {user.role !== "admin" &&

Access denied

} {user.role === "admin" && ( <> +
+
+ +
+

Roles and access

Date: Fri, 28 Jun 2024 13:43:11 -0400 Subject: [PATCH 082/820] SFC-fix-citations-url --- backend/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index 5a8a8f4e..7a33874b 100644 --- a/backend/app.py +++ b/backend/app.py @@ -240,7 +240,7 @@ def getBlob(): f"https://{STORAGE_ACCOUNT}.blob.core.windows.net", client_credential ) blob_client = blob_service_client.get_blob_client( - container="documents", blob=blob_name + container="documents", blob=blob_name[1:] ) blob_data = blob_client.download_blob() blob_text = blob_data.readall() From 2f2daf01b923e4f53a79b75977685c19fd4edc6d Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Fri, 28 Jun 2024 14:27:33 -0400 Subject: [PATCH 083/820] SFC-282 enhance chat history --- .../HistoryPannel/ChatHistoryListItem.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 8f6fe02f..74490a0d 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -136,17 +136,26 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; - const sortedDataByMonth = dataHistory.sort((a, b) => { - const monthA = new Date(a.start_date).getMonth(); - const monthB = new Date(b.start_date).getMonth(); - return monthA - monthB; - }); - - const sortedDataListByMonth = months.map(month => { - const monthData = sortedDataByMonth.filter(item => { - return new Date(item.start_date).getMonth() === months.indexOf(month); + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + const sortedDataByDate = dataHistory.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); + + const sortedDataListByDate = [ + { label: "Today", date: today }, + { label: "Yesterday", date: yesterday }, + ...months.map(month => ({ label: month, monthIndex: months.indexOf(month) })) + ].map(({ label, date, monthIndex }: any) => { + const filteredData = sortedDataByDate.filter(item => { + const itemDate = new Date(item.start_date); + if (date) { + return itemDate.toDateString() === date.toDateString(); + } else { + return itemDate.getMonth() === monthIndex; + } }); - return { month, data: monthData }; + return { label, data: filteredData }; }); const isConfirmationDelete = (conversationId: string) => confirmationDelete === conversationId; @@ -167,11 +176,11 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete

{errorMessage}

) : ( <> - {sortedDataListByMonth.map(({ month, data }, monthIndex) => ( + {sortedDataListByDate.map(({ label, data }, monthIndex) => (
{data.length > 0 && ( <> -

{month}

+

{label}

{data.map((conversation, index) => (
Date: Fri, 28 Jun 2024 14:51:14 -0400 Subject: [PATCH 084/820] SFC-282 not repeating conversations --- .../HistoryPannel/ChatHistoryListItem.tsx | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 74490a0d..15681c2b 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -54,6 +54,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const dateB = new Date(b.start_date); return dateB.getTime() - dateA.getTime(); }); + sortedData.splice(100); setDataHistory(sortedData); setIsLoading(false); const ids = sortedData.map(data => data.id); @@ -134,26 +135,34 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete } }, [user.id, dataHistory, conversationsIds, refreshFetchHistorial]); - const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; - const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); // Set to the start of the week (Sunday) + + const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); + const sortedDataByDate = dataHistory.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); + const uniqueItems = new Set(); + const sortedDataListByDate = [ - { label: "Today", date: today }, - { label: "Yesterday", date: yesterday }, - ...months.map(month => ({ label: month, monthIndex: months.indexOf(month) })) - ].map(({ label, date, monthIndex }: any) => { + { label: "Today", filter: (itemDate: any) => itemDate.toDateString() === today.toDateString() }, + { label: "Yesterday", filter: (itemDate: any) => itemDate.toDateString() === yesterday.toDateString() }, + { label: "This Week", filter: (itemDate: any) => itemDate >= startOfWeek && itemDate <= today }, + { label: "This Month", filter: (itemDate: any) => itemDate >= startOfMonth && itemDate <= today }, + { label: "Previous Months", filter: (itemDate: any) => itemDate < startOfMonth } + ].map(({ label, filter }) => { const filteredData = sortedDataByDate.filter(item => { const itemDate = new Date(item.start_date); - if (date) { - return itemDate.toDateString() === date.toDateString(); - } else { - return itemDate.getMonth() === monthIndex; + if (!uniqueItems.has(item)) { + const matches = filter(itemDate); + if (matches) uniqueItems.add(item); + return matches; } + return false; }); return { label, data: filteredData }; }); From 743e7d774ceba9d27eab9faf48473f240a3458bd Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 2 Jul 2024 15:33:49 -0400 Subject: [PATCH 085/820] SFC-286-add-attachment-icon --- .../QuestionInput/QuestionInput.module.css | 7 +++++ .../QuestionInput/QuestionInput.tsx | 31 ++++++++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index 5f848950..8feda090 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -8,6 +8,13 @@ border-bottom: 5px solid #a2e700; } +.attachmentContainer { + height: '100%'; + justify-content: 'center'; + align-items: center; + display: flex; +} + .questionInputTextArea { width: 100%; line-height: 40px; diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index e77e2e02..8dbc44fe 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,8 +1,8 @@ import { useState } from "react"; -import { Stack, TextField } from "@fluentui/react"; -import { getTokenOrRefresh } from './token_util'; -import { Send28Filled, BookOpenMicrophone28Filled, SlideMicrophone32Filled } from "@fluentui/react-icons"; -import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from 'microsoft-cognitiveservices-speech-sdk'; +import { Stack, TextField, IconButton } from "@fluentui/react"; +import { getTokenOrRefresh } from "./token_util"; +import { Send28Filled, Attach32Filled, BookOpenMicrophone28Filled, SlideMicrophone32Filled } from "@fluentui/react-icons"; +import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from "microsoft-cognitiveservices-speech-sdk"; import styles from "./QuestionInput.module.css"; interface Props { @@ -31,18 +31,18 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr const tokenObj = await getTokenOrRefresh(); const speechConfig = SpeechConfig.fromAuthorizationToken(tokenObj.authToken, tokenObj.region); speechConfig.speechRecognitionLanguage = tokenObj.speechRecognitionLanguage; - + const audioConfig = AudioConfig.fromDefaultMicrophoneInput(); const recognizer = new SpeechRecognizer(speechConfig, audioConfig); const userLanguage = navigator.language; - let reiniciar_text = ''; - if (userLanguage.startsWith('pt')) { - reiniciar_text = 'Pode falar usando seu microfone...'; - } else if (userLanguage.startsWith('es')) { - reiniciar_text = 'Puedes hablar usando su micrófono...'; + let reiniciar_text = ""; + if (userLanguage.startsWith("pt")) { + reiniciar_text = "Pode falar usando seu microfone..."; + } else if (userLanguage.startsWith("es")) { + reiniciar_text = "Puedes hablar usando su micrófono..."; } else { - reiniciar_text = 'You can talk using your microphone...'; + reiniciar_text = "You can talk using your microphone..."; } setQuestion(reiniciar_text); @@ -54,7 +54,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr //setQuestion(displayText); //onSend(question); } else { - displayText = 'ERROR: Voice recognition was canceled or the voice cannot be recognized. Make sure your microphone is working properly.'; + displayText = "ERROR: Voice recognition was canceled or the voice cannot be recognized. Make sure your microphone is working properly."; //setQuestion(displayText); } setQuestion(displayText); @@ -80,6 +80,9 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr return ( +
+ {}} /> +
{ + onKeyDown={ev => { if (ev.key === "Enter") { ev.preventDefault(); sendQuestion(); @@ -109,7 +112,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr className={`${styles.questionInputSendButton}`} aria-label="Button to talk" onClick={sttFromMic} - onKeyDown={(ev) => { + onKeyDown={ev => { if (ev.key === "Enter") { ev.preventDefault(); sttFromMic(); From eff0a3ff75100914f69cf83e9f1312e499367379 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 2 Jul 2024 15:36:03 -0400 Subject: [PATCH 086/820] fix sources validation --- backend/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index 7a33874b..5a8a8f4e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -240,7 +240,7 @@ def getBlob(): f"https://{STORAGE_ACCOUNT}.blob.core.windows.net", client_credential ) blob_client = blob_service_client.get_blob_client( - container="documents", blob=blob_name[1:] + container="documents", blob=blob_name ) blob_data = blob_client.download_blob() blob_text = blob_data.readall() From 0909a03fa38d4b8da3c5af65353c9c476b671c6c Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Tue, 2 Jul 2024 23:10:53 -0400 Subject: [PATCH 087/820] SFC-282 enhance chat history recovery experience --- frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 15681c2b..de1b6835 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -136,8 +136,6 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete }, [user.id, dataHistory, conversationsIds, refreshFetchHistorial]); const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); const startOfWeek = new Date(today); startOfWeek.setDate(today.getDate() - today.getDay()); // Set to the start of the week (Sunday) @@ -150,7 +148,6 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const sortedDataListByDate = [ { label: "Today", filter: (itemDate: any) => itemDate.toDateString() === today.toDateString() }, - { label: "Yesterday", filter: (itemDate: any) => itemDate.toDateString() === yesterday.toDateString() }, { label: "This Week", filter: (itemDate: any) => itemDate >= startOfWeek && itemDate <= today }, { label: "This Month", filter: (itemDate: any) => itemDate >= startOfMonth && itemDate <= today }, { label: "Previous Months", filter: (itemDate: any) => itemDate < startOfMonth } From ca3b9fffdf20b4259adbefb3fa9fb5ffaaf9d34c Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 3 Jul 2024 17:51:28 -0400 Subject: [PATCH 088/820] SFC-266-add-upload-component --- .../QuestionInput/QuestionInput.tsx | 135 +++++++++++++++++- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 8dbc44fe..d9062590 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Stack, TextField, IconButton } from "@fluentui/react"; +import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; import { getTokenOrRefresh } from "./token_util"; import { Send28Filled, Attach32Filled, BookOpenMicrophone28Filled, SlideMicrophone32Filled } from "@fluentui/react-icons"; import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from "microsoft-cognitiveservices-speech-sdk"; @@ -12,6 +12,135 @@ interface Props { clearOnSend?: boolean; } +import { useFilePicker } from "use-file-picker"; + +export const FileAttachmentInput = () => { + const [files, setFiles] = useState([]); + const { openFilePicker, filesContent, loading, errors } = useFilePicker({ + readAs: "DataURL", + accept: ["xls", "xlsx", "csv"], + multiple: false, + onFilesSelected: ({ plainFiles, filesContent, errors }) => { + // this callback is always called, even if there are errors + console.log("onFilesSelected", plainFiles, filesContent, errors); + }, + onFilesRejected: ({ errors }) => { + // this callback is called when there were validation errors + console.log("onFilesRejected", errors); + }, + onFilesSuccessfullySelected: ({ plainFiles, filesContent }) => { + // this callback is called when the files are successfully selected + console.log("onFilesSuccessfullySelected", plainFiles, filesContent); + setFiles(plainFiles); + } + }); + + if (loading) { + return ( +
+ +
+ ); + } + + if (errors.length) { + return ( +
+
Error with the file picker
+
+ ); + } + + return ( + <> +
+ {files.map((file, index) => ( +
+ + {}} + /> +
{file.name}
+
+ ))} +
+
+
+ +
+ + ); +}; + export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { const [question, setQuestion] = useState(""); @@ -80,9 +209,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr return ( -
- {}} /> -
+ Date: Fri, 5 Jul 2024 08:20:41 -0400 Subject: [PATCH 089/820] HOTFIX install missing dependency (#73) --- frontend/package-lock.json | 955 ++++++++----------------------------- frontend/package.json | 3 +- 2 files changed, 204 insertions(+), 754 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d3df2529..581ce792 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,8 @@ "react-doc-viewer": "^0.1.5", "react-dom": "^18.2.0", "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" + "universal-cookie": "^4.0.4", + "use-file-picker": "^2.1.2" }, "devDependencies": { "@types/dompurify": "^2.4.0", @@ -388,23 +389,22 @@ } }, "node_modules/@cyntler/react-doc-viewer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.14.1.tgz", - "integrity": "sha512-1LiYewtiLM6FZgkJmlAiibv3zeiDinII+WKjViLeaD7O9yP+F9TqYyYSTR05crZODltzHenn/Tcx9YesV9tKtA==", - "dependencies": { - "@types/mustache": "^4.2.3", - "@types/papaparse": "^5.3.9", + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/@cyntler/react-doc-viewer/-/react-doc-viewer-1.16.5.tgz", + "integrity": "sha512-C1hLPJIO+rE8TR/foidej47xnil5bmh7F/NdqCMNRE8EHWyhP5UKog4GALAiRScznw4ybgHaTDk1bIXYBhQnIw==", + "dependencies": { + "@types/mustache": "^4.2.5", + "@types/papaparse": "^5.3.14", + "ajv": "^7.2.4", + "core-js": "^3.37.1", "mustache": "^4.2.0", "papaparse": "^5.4.1", - "react-pdf": "7.5.0", - "styled-components": "^6.0.8" - }, - "engines": { - "node": ">=12.0.0" + "react-pdf": "^9.0.0", + "styled-components": "^6.1.11" }, "peerDependencies": { - "react": ">=16.13.1", - "react-dom": ">=16.13.1" + "react": ">=17.0.0", + "react-dom": ">=17.0.0" } }, "node_modules/@emotion/hash": { @@ -413,9 +413,9 @@ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", - "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "dependencies": { "@emotion/memoize": "^0.8.1" } @@ -431,9 +431,9 @@ "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", @@ -1062,6 +1062,8 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -1478,37 +1480,6 @@ "@types/trusted-types": "*" } }, - "node_modules/@types/eslint": { - "version": "8.56.5", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", - "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "peer": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, "node_modules/@types/mustache": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", @@ -1559,9 +1530,9 @@ "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-n4sx2bqL0mW1tvDf/loQ+aMX7GQD3lc3fkCMC55VFNDu/vBOabO+LTIeXKM14xK0ppk5TUGcWRjiSpIlUpghKw==" + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==" }, "node_modules/@types/trusted-types": { "version": "2.0.7", @@ -1593,152 +1564,6 @@ "vite": "^4.1.0-beta.0" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", - "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", - "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", - "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", - "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-opt": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6", - "@webassemblyjs/wast-printer": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", - "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", - "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-buffer": "1.11.6", - "@webassemblyjs/wasm-gen": "1.11.6", - "@webassemblyjs/wasm-parser": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", - "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", - "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.11.6", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -1747,18 +1572,6 @@ "node": ">=10.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "peer": true - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1769,6 +1582,8 @@ "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "optional": true, "peer": true, "bin": { "acorn": "bin/acorn" @@ -1777,15 +1592,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", - "peer": true, - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1798,13 +1604,13 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.2.4.tgz", + "integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==", "dependencies": { "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" }, "funding": { @@ -1812,14 +1618,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1931,14 +1729,6 @@ "is-stream": "^2.0.0" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "engines": { - "node": "*" - } - }, "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", @@ -1989,6 +1779,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "optional": true, "peer": true }, "node_modules/bytesish": { @@ -2065,19 +1857,10 @@ "node": ">=10" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } @@ -2108,6 +1891,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, "peer": true }, "node_modules/concat-map": { @@ -2135,6 +1920,16 @@ "node": ">= 0.6" } }, + "node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2197,6 +1992,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", @@ -2235,33 +2038,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "optional": true }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", - "peer": true - }, "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", @@ -2315,67 +2091,21 @@ "node": ">=0.8.0" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "peer": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "peer": true, - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "node_modules/file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } }, "node_modules/fs-minipass": { "version": "2.1.0", @@ -2475,12 +2205,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "peer": true - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2489,12 +2213,6 @@ "node": ">=4" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "peer": true - }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -2587,44 +2305,6 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2641,16 +2321,10 @@ "node": ">=4" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "peer": true - }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json5": { "version": "2.2.3", @@ -2682,28 +2356,6 @@ "immediate": "~3.0.5" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "peer": true, - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2804,23 +2456,15 @@ "node": ">=12.0.0" } }, - "node_modules/merge-class-names": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz", - "integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-class-names?sponsor=1" - } - }, "node_modules/merge-refs": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", - "integrity": "sha512-RwcT7GsQR3KbuLw1rRuodq4Nt547BKEBkliZ0qqsrpyNne9bGTFtsFIsIpx82huWhcl3kOlOlH4H0xkPk/DqVw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", + "integrity": "sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==", "funding": { "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -2828,12 +2472,6 @@ } } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "peer": true - }, "node_modules/microsoft-cognitiveservices-speech-sdk": { "version": "1.36.0", "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", @@ -2847,27 +2485,6 @@ "ws": "^7.5.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "peer": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", @@ -2980,12 +2597,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "peer": true - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -3042,6 +2653,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -3078,11 +2690,21 @@ "node": ">=0.10.0" } }, + "node_modules/path2d": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", + "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/path2d-polyfill": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path2d-polyfill/-/path2d-polyfill-2.0.1.tgz", "integrity": "sha512-ad/3bsalbbWhmBo0D6FZ4RNMwsLsPpL6gnvhuSaU5Vm7b06Kr5ubSltQQ0T7YKsiJQO+g22zJ4dJKNTXIyOXtA==", "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -3091,6 +2713,7 @@ "version": "3.11.174", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", "integrity": "sha512-TdTZPf1trZ8/UFu5Cx/GXB7GZM30LT+wWUNfsi6Bq8ePLnb+woNKtDymI2mxZYBpMbonNFqKmiz684DIfnd8dA==", + "peer": true, "engines": { "node": ">=18" }, @@ -3116,9 +2739,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -3134,9 +2757,9 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" @@ -3167,21 +2790,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3190,15 +2798,6 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -3211,13 +2810,12 @@ } }, "node_modules/react-doc-viewer": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.5.tgz", - "integrity": "sha512-hLhjSlc0Ffe/PUjfgvEhM/SEgZ9ql1ujFYnkOMlJquBLj7iHlSM0cGAENXPbI2VK03+r92nM2Re+vT0Dwfyifg==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/react-doc-viewer/-/react-doc-viewer-0.1.13.tgz", + "integrity": "sha512-f7q8ymQULiV1jKKK9kbjZQVbuTrre3/2y5Cpv78/yqXMWz7Ornasle4d+Cy3eXW5ZMfTIVzCqQSNyFQLgs8yzw==", "dependencies": { - "pdfjs-dist": "2.4.456", - "react-pdf": "5.0.0", - "styled-components": "^5.1.1", + "react-pdf": "9.0.0", + "styled-components": "^5.3.11", "wl-msg-reader": "^0.2.0" } }, @@ -3227,68 +2825,43 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "node_modules/react-doc-viewer/node_modules/pdfjs-dist": { - "version": "2.4.456", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.4.456.tgz", - "integrity": "sha512-yckJEHq3F48hcp6wStEpbN9McOj328Ib09UrBlGAKxvN2k+qYPN5iq6TH6jD1C0pso7zTep+g/CKsYgdrQd5QA==" - }, - "node_modules/react-doc-viewer/node_modules/react": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", - "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" - }, + "version": "4.3.136", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.3.136.tgz", + "integrity": "sha512-gzfnt1qc4yA+U46golPGYtU4WM2ssqP2MvFjKga8GEKOrEnzRPrA/9jogLLPYHiA3sGBPJ+p7BdAq+ytmw3jEg==", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-doc-viewer/node_modules/react-dom": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", - "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "node": ">=18" }, - "peerDependencies": { - "react": "^16.14.0" + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" } }, "node_modules/react-doc-viewer/node_modules/react-pdf": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.0.0.tgz", - "integrity": "sha512-VpqZjpZGEevmotLYl6acU6GYQeJ0dxn9+5sth5QjWLFhKu0xy3zSZgt3U3m97zW6UWzQ/scvw5drfPyun5l4eA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.0.0.tgz", + "integrity": "sha512-J+pza8R2p9oNEOJOHIQJI4o5rFK7ji7bBl2IvsHvz1OOyphvuzVDo5tOJwWAFAbxYauCH3Kt8jOvcMJUOpxYZQ==", "dependencies": { - "@babel/runtime": "^7.0.0", - "make-cancellable-promise": "^1.0.0", - "make-event-props": "^1.1.0", - "merge-class-names": "^1.1.1", - "pdfjs-dist": "2.4.456", - "prop-types": "^15.6.2", - "worker-loader": "^3.0.0" + "clsx": "^2.0.0", + "dequal": "^2.0.3", + "make-cancellable-promise": "^1.3.1", + "make-event-props": "^1.6.0", + "merge-refs": "^1.3.0", + "pdfjs-dist": "4.3.136", + "tiny-invariant": "^1.0.0", + "warning": "^4.0.0" }, "funding": { "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" }, "peerDependencies": { - "react": "^16.3.0", - "react-dom": "^16.3.0" - } - }, - "node_modules/react-doc-viewer/node_modules/scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/react-doc-viewer/node_modules/styled-components": { @@ -3339,26 +2912,26 @@ "peer": true }, "node_modules/react-pdf": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.5.0.tgz", - "integrity": "sha512-hX7SfQGd9T6pdd3H5HcR1VzrRCehkhnBh/tsyz9GO9cXrYHgoxupboVL2VCQpBBSak+/UQSMCj+3JTOdheuwwQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.0.tgz", + "integrity": "sha512-KhPDQE3QshkLdS3b48S5Bldv0N5flob6qwvsiADWdZOS5TMDaIrkRtEs+Dyl6ubRf2jTf9jWmFb6RjWu46lSSg==", "dependencies": { "clsx": "^2.0.0", + "dequal": "^2.0.3", "make-cancellable-promise": "^1.3.1", "make-event-props": "^1.6.0", - "merge-refs": "^1.2.1", - "pdfjs-dist": "3.11.174", - "prop-types": "^15.6.2", + "merge-refs": "^1.3.0", + "pdfjs-dist": "4.4.168", "tiny-invariant": "^1.0.0", - "tiny-warning": "^1.0.0" + "warning": "^4.0.0" }, "funding": { "url": "https://github.com/wojtekmaj/react-pdf?sponsor=1" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3366,6 +2939,18 @@ } } }, + "node_modules/react-pdf/node_modules/pdfjs-dist": { + "version": "4.4.168", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", + "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -3424,6 +3009,14 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -3476,23 +3069,6 @@ "loose-envify": "^1.1.0" } }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -3501,15 +3077,6 @@ "semver": "bin/semver.js" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -3567,15 +3134,17 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -3584,6 +3153,8 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "optional": true, "peer": true, "dependencies": { "buffer-from": "^1.0.0", @@ -3630,19 +3201,19 @@ } }, "node_modules/styled-components": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.8.tgz", - "integrity": "sha512-PQ6Dn+QxlWyEGCKDS71NGsXoVLKfE1c3vApkvDYS5KAK+V8fNWGhbSUEo9Gg2iaID2tjLXegEW3bZDUGpofRWw==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", + "integrity": "sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==", "dependencies": { - "@emotion/is-prop-valid": "1.2.1", - "@emotion/unitless": "0.8.0", - "@types/stylis": "4.2.0", + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", - "csstype": "3.1.2", - "postcss": "8.4.31", + "csstype": "3.1.3", + "postcss": "8.4.38", "shallowequal": "1.1.0", - "stylis": "4.3.1", - "tslib": "2.5.0" + "stylis": "4.3.2", + "tslib": "2.6.2" }, "engines": { "node": ">= 16" @@ -3656,20 +3227,10 @@ "react-dom": ">= 16.8.0" } }, - "node_modules/styled-components/node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" - }, - "node_modules/styled-components/node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" - }, "node_modules/stylis": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", - "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" }, "node_modules/supports-color": { "version": "5.5.0", @@ -3682,19 +3243,10 @@ "node": ">=4" } }, - "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "optional": true, "dependencies": { "chownr": "^2.0.0", @@ -3718,6 +3270,8 @@ "version": "5.29.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "dev": true, + "optional": true, "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -3732,50 +3286,11 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", - "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -3864,6 +3379,20 @@ "punycode": "^2.1.0" } }, + "node_modules/use-file-picker": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/use-file-picker/-/use-file-picker-2.1.2.tgz", + "integrity": "sha512-ZEIzRi1wXeIXDWr5i55gRBVER8rTkSGskDUY94bciTTAZJHlBnOTRLL/LDYjgz6d+US3yELHnRvtBhLxFGtB0A==", + "dependencies": { + "file-selector": "0.2.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -3882,9 +3411,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -3936,17 +3465,12 @@ } } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "peer": true, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" + "loose-envify": "^1.0.0" } }, "node_modules/webidl-conversions": { @@ -3955,62 +3479,6 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "optional": true }, - "node_modules/webpack": { - "version": "5.90.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", - "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.3", - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.11.5", - "@webassemblyjs/wasm-edit": "^1.11.5", - "@webassemblyjs/wasm-parser": "^1.11.5", - "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", - "browserslist": "^4.21.10", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.15.0", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.9", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.0", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -4035,25 +3503,6 @@ "resolved": "https://registry.npmjs.org/wl-msg-reader/-/wl-msg-reader-0.2.1.tgz", "integrity": "sha512-PFK8vjdaGUmj0EqBKL/ECSeSgxI/QBy2njuxX+UaCKjDaN6H0UYVLmmizmMJsrzkQ9QmDvsJiSE0H1o7wY4Zfg==" }, - "node_modules/worker-loader": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-3.0.8.tgz", - "integrity": "sha512-XQyQkIFeRVC7f7uRhFdNMe/iJOdO6zxAaR3EWbDp45v3mDhrTi+++oswKNxShUNjPC/1xUp5DB29YKLhFo129g==", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4061,9 +3510,9 @@ "optional": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, diff --git a/frontend/package.json b/frontend/package.json index f7cb880b..d8977ab7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,7 +23,8 @@ "react-doc-viewer": "^0.1.5", "react-dom": "^18.2.0", "react-router-dom": "^6.8.1", - "universal-cookie": "^4.0.4" + "universal-cookie": "^4.0.4", + "use-file-picker": "^2.1.2" }, "devDependencies": { "@types/dompurify": "^2.4.0", From ecda572027320e9147149351aa67f0dcc52002f9 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 5 Jul 2024 08:32:14 -0400 Subject: [PATCH 090/820] HOTFIX install mising react toastify (#74) --- frontend/package-lock.json | 13 ++++++ frontend/package.json | 1 + package-lock.json | 84 -------------------------------------- package.json | 5 --- 4 files changed, 14 insertions(+), 89 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 581ce792..6f9d0346 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "react-doc-viewer": "^0.1.5", "react-dom": "^18.2.0", "react-router-dom": "^6.8.1", + "react-toastify": "^10.0.5", "universal-cookie": "^4.0.4", "use-file-picker": "^2.1.2" }, @@ -2990,6 +2991,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index d8977ab7..53271a85 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "react-doc-viewer": "^0.1.5", "react-dom": "^18.2.0", "react-router-dom": "^6.8.1", + "react-toastify": "^10.0.5", "universal-cookie": "^4.0.4", "use-file-picker": "^2.1.2" }, diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 8342cbf7..00000000 --- a/package-lock.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "name": "gpt-rag-frontend", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "react-toastify": "^10.0.5" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "peer": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-toastify": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", - "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", - "dependencies": { - "clsx": "^2.1.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index e423c7ef..00000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "react-toastify": "^10.0.5" - } -} From bad3d8ced59fd3dfb38cb27752abe856dc832b72 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Fri, 5 Jul 2024 12:30:05 -0400 Subject: [PATCH 091/820] SFC-286-attachment-icon-rotation --- .../src/components/QuestionInput/QuestionInput.module.css | 5 +++++ frontend/src/components/QuestionInput/QuestionInput.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index 8feda090..da4f2dbe 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -34,3 +34,8 @@ .questionInputSendButtonDisabled { opacity: 0.4; } + +.attachmentButton { + transform: rotate(-45deg); + cursor: pointer; +} \ No newline at end of file diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index d9062590..250adc99 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -134,7 +134,7 @@ export const FileAttachmentInput = () => { ))}

-
+
From 41710843dce1f725a88dfccf378a60dc255ceec0 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Mon, 8 Jul 2024 11:38:34 -0400 Subject: [PATCH 092/820] add payment --- .../ButtonPaymentGateway.module.css | 31 +++++++ .../PaymentGateway/ButtonPaymentGateway.tsx | 18 ++++ .../PaymentGateway/PaymentGateway.module.css | 85 +++++++++++++++++++ .../PaymentGateway/PaymentGateway.tsx | 70 +++++++++++++++ frontend/src/index.tsx | 8 ++ frontend/src/pages/layout/Layout.tsx | 2 + 6 files changed, 214 insertions(+) create mode 100644 frontend/src/components/PaymentGateway/ButtonPaymentGateway.module.css create mode 100644 frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx create mode 100644 frontend/src/components/PaymentGateway/PaymentGateway.module.css create mode 100644 frontend/src/components/PaymentGateway/PaymentGateway.tsx diff --git a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.module.css b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.module.css new file mode 100644 index 00000000..f615cdbd --- /dev/null +++ b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.module.css @@ -0,0 +1,31 @@ +.container { + display: flex; + align-items: center; + margin-top: 5px; + gap: 6px; + cursor: pointer; + border: 1.5px solid rgb(231, 231, 231); + border-radius: 5px; + padding: 4px 12px; + transition: .2s; + background-color: white; + width: 200px; + margin-right: 5px; +} + +.container:hover{ + background-color: rgb(243, 243, 243); +} + +.container:active{ + background-color: white; +} + +.button{ + color: rgb(63, 63, 63); + font-size: 24px; +} + +.buttonText{ + font-weight: 500; +} \ No newline at end of file diff --git a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx new file mode 100644 index 00000000..d92998ab --- /dev/null +++ b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx @@ -0,0 +1,18 @@ +import styles from "./ButtonPaymentGateway.module.css"; +import { Text } from "@fluentui/react"; +import { AppContext } from "../../providers/AppProviders"; +import { useContext } from "react"; + + +export const ButtonPaymentGateway = () => { + + const handleRedirect = () => { + window.location.href = "#/payment"; + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.module.css b/frontend/src/components/PaymentGateway/PaymentGateway.module.css new file mode 100644 index 00000000..ba3db396 --- /dev/null +++ b/frontend/src/components/PaymentGateway/PaymentGateway.module.css @@ -0,0 +1,85 @@ +@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap'); + +.subscriptionPlan { + font-family: 'Poppins', sans-serif; + text-align: center; + margin-top: 80px; + padding: 20px; +} + +.subscriptionPlanTitle { + color: #333; + margin-bottom: 40px; + font-size: 2.5em; + font-weight: 600; +} + +.planContainer { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; +} + +.plan { + border: 1px solid #ddd; + border-radius: 10px; + padding: 20px; + margin: 10px; + width: 300px; + height: 400px; + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + background: white; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.planName { + margin: 0; + font-size: 1.8em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + color: #333; +} + +.planPrice { + font-size: 1.5em; + color: #666; + margin: 20px 0; +} + +.planInterval { + font-size: 1.2em; + color: #999; +} + +.planDescription { + font-size: 1em; + color: #777; + margin: 10px 0 20px; + flex-grow: 1; +} + +.planButton { + background-color: #6772e5; + color: white; + border: none; + padding: 12px 25px; + border-radius: 25px; + cursor: pointer; + font-size: 1em; + transition: background-color 0.3s; + font-family: 'Poppins', sans-serif; + outline: none; +} + +.planButton:hover { + background-color: #5469d4; +} + +.planButton:focus { + background-color: #3f51b5; +} \ No newline at end of file diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx new file mode 100644 index 00000000..42c7f8ef --- /dev/null +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import { loadStripe } from '@stripe/stripe-js'; +import { Elements } from '@stripe/react-stripe-js'; +import styles from "./PaymentGateway.module.css"; + +const stripePromise = loadStripe(import.meta.env.VITE_SOME_KEY); + +export const SubscriptionPlans: React.FC = () => { + const [plans, setPlans] = useState([]); + console.log(import.meta.env.VITE_SOME_KEY); + console.log(stripePromise); + useEffect(() => { + setPlans([ + { + id: 'free_plan', + name: 'Current Plan', + description: 'Access to basic features for free.', + price: '0.00', + interval: 'month' + }, + { + id: 'price_1PYvHVEpF6ccgZLwn6uq6d4J', + name: 'Plus Plan', + description: 'Access to all features including premium support.', + price: '30.00', + interval: 'month' + } + ]); + }, []); + + const handleCheckout = async (priceId: string) => { + const stripe = await stripePromise; + const { error } = await stripe!.redirectToCheckout({ + lineItems: [{ price: priceId, quantity: 1 }], + mode: 'subscription', + successUrl: window.location.origin + '/', + cancelUrl: window.location.origin + '/', + }); + + if (error) { + console.error('Error redirecting to checkout:', error); + } + }; + + return ( +
+

Subscription Plans

+
+ {plans.map(plan => ( +
+

{plan.name}

+

${plan.price} per {plan.interval}

+

{plan.description}

+ {plan.id !== 'free_plan' && ( + + )} +
+ ))} +
+
+ ); + }; + + export const PaymentGateway: React.FC = () => { + return ( + + + + ); + }; \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 8017472b..200439b7 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -12,6 +12,7 @@ import AccessDenied from "./pages/AccesDenied"; import Chat from "./pages/chat/Chat"; import Admin from "./pages/admin/Admin"; import { AppProvider } from "./providers/AppProviders"; +import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; initializeIcons(); @@ -39,6 +40,13 @@ export default function App() { } /> } /> + }> + } + /> + } /> + diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 7bcf8650..639ebce1 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -10,6 +10,7 @@ import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistor import { FeedbackRatingButton } from "../../components/FeedbackRating/FeedbackRatingButton"; import { AppContext } from "../../providers/AppProviders"; import { SettingsButton } from "../../components/SettingsButton"; +import { ButtonPaymentGateway } from "../../components/PaymentGateway/ButtonPaymentGateway"; const Layout = () => { const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = @@ -47,6 +48,7 @@ const Layout = () => {
{pathname === "/" && ( <> + From 139a9a156d14f3276b46180099b865436ae26a69 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 8 Jul 2024 14:40:25 -0400 Subject: [PATCH 093/820] add stripe key endpoint --- backend/app.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/app.py b/backend/app.py index 5a8a8f4e..bd287d84 100644 --- a/backend/app.py +++ b/backend/app.py @@ -229,6 +229,15 @@ def getStorageAccount(): logging.exception("[webbackend] exception in /api/get-storage-account") return jsonify({"error": str(e)}), 500 +@app.route("/api/stripe", methods=["GET"]) +def getStripe(): + try: + keySecretName = "stripeKey" + functionKey = get_secret(keySecretName) + return functionKey + except Exception as e: + logging.exception("[webbackend] exception in /api/stripe") + return jsonify({"error": str(e)}), 500 @app.route("/api/get-blob", methods=["POST"]) def getBlob(): From 7d0be3ef11bd27a382af441573d850764277d9b0 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:44:16 -0400 Subject: [PATCH 094/820] add payment gateway form --- frontend/package-lock.json | 39 ++++- frontend/package.json | 2 + frontend/src/api/api.ts | 15 ++ .../PaymentGateway/ButtonPaymentGateway.tsx | 7 +- .../PaymentGateway/PaymentGateway.module.css | 14 ++ .../PaymentGateway/PaymentGateway.tsx | 138 +++++++++++------- frontend/src/pages/chat/Chat.tsx | 8 +- frontend/src/providers/AppProviders.tsx | 3 + 8 files changed, 164 insertions(+), 62 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6f9d0346..b8523b19 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,8 @@ "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/default-layout": "^3.12.0", "@react-spring/web": "^9.7.1", + "@stripe/react-stripe-js": "^2.7.3", + "@stripe/stripe-js": "^4.1.0", "dompurify": "^3.0.1", "mammoth": "^1.7.0", "microsoft-cognitiveservices-speech-sdk": "^1.27.0", @@ -1459,6 +1461,27 @@ "node": ">=14.0.0" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.7.3.tgz", + "integrity": "sha512-05t6oY7cmAJt7asknmeoI4z4GnutgKRZ7dcdTWCkeYclONzIRMuMTiyjBMQ/q3I2sdNizSl25YZ8G6Lg4nN1aw==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0 || ^4.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.1.0.tgz", + "integrity": "sha512-HhstGRUz/4JdbZpb26OcOf8Qb/cFR02arvHvgz4sPFLSnI6ZNHC53Jc6JP/FGNwxtrF719YyUnK0gGy4oyhucQ==", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@swc/helpers": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", @@ -2654,7 +2677,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -2791,6 +2813,21 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 53271a85..80a08244 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,8 @@ "@react-pdf-viewer/core": "^3.12.0", "@react-pdf-viewer/default-layout": "^3.12.0", "@react-spring/web": "^9.7.1", + "@stripe/react-stripe-js": "^2.7.3", + "@stripe/stripe-js": "^4.1.0", "dompurify": "^3.0.1", "mammoth": "^1.7.0", "microsoft-cognitiveservices-speech-sdk": "^1.27.0", diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a46cd8df..c58e458a 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -308,3 +308,18 @@ export async function inviteUser ({ username, email }: any): Promise { return {error : error}; } } +export async function getApiKeyPayment(): Promise { + const response = await fetch("/api/stripe", { + method: "GET", + headers: { + "Content-Type": "application/json", + } + }); + + if (response.status > 299 || !response.ok) { + throw Error("Error getting Api key payment"); + } + + const apiKey = await response.text(); + return apiKey; +} diff --git a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx index d92998ab..9fe9dc6a 100644 --- a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx @@ -1,7 +1,7 @@ import styles from "./ButtonPaymentGateway.module.css"; import { Text } from "@fluentui/react"; -import { AppContext } from "../../providers/AppProviders"; -import { useContext } from "react"; +import { GuestFilled } from "@fluentui/react-icons"; + export const ButtonPaymentGateway = () => { @@ -12,7 +12,8 @@ export const ButtonPaymentGateway = () => { return ( ); }; \ No newline at end of file diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.module.css b/frontend/src/components/PaymentGateway/PaymentGateway.module.css index ba3db396..d665daa0 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.module.css +++ b/frontend/src/components/PaymentGateway/PaymentGateway.module.css @@ -82,4 +82,18 @@ .planButton:focus { background-color: #3f51b5; +} + +.spinnerContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; +} + +.loadingText { + margin-top: 10px; + font-size: 1.5em; + color: #000000; } \ No newline at end of file diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 42c7f8ef..235c6aed 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -1,70 +1,100 @@ import React, { useState, useEffect } from 'react'; -import { loadStripe } from '@stripe/stripe-js'; +import { loadStripe, Stripe } from '@stripe/stripe-js'; import { Elements } from '@stripe/react-stripe-js'; import styles from "./PaymentGateway.module.css"; +import { getApiKeyPayment } from "../../api"; +import { AppContext } from "../../providers/AppProviders"; +import { useContext } from "react"; +import { Spinner } from "@fluentui/react"; -const stripePromise = loadStripe(import.meta.env.VITE_SOME_KEY); +const fetchApiKey = async () => { + const apiKey = await getApiKeyPayment(); + return apiKey; +}; -export const SubscriptionPlans: React.FC = () => { +export const SubscriptionPlans: React.FC<{ stripePromise: Promise }> = ({ stripePromise }) => { const [plans, setPlans] = useState([]); - console.log(import.meta.env.VITE_SOME_KEY); - console.log(stripePromise); + const {user} = useContext(AppContext); + useEffect(() => { - setPlans([ - { - id: 'free_plan', - name: 'Current Plan', - description: 'Access to basic features for free.', - price: '0.00', - interval: 'month' - }, - { - id: 'price_1PYvHVEpF6ccgZLwn6uq6d4J', - name: 'Plus Plan', - description: 'Access to all features including premium support.', - price: '30.00', - interval: 'month' - } - ]); + setPlans([ + { + id: 'free_plan', + name: 'Current Plan', + description: 'Access to basic features for free.', + price: '0.00', + interval: 'month' + }, + { + id: 'price_1PYvHVEpF6ccgZLwn6uq6d4J', + name: 'Plus Plan', + description: 'Access to all features including premium support.', + price: '30.00', + interval: 'month' + } + ]); }, []); - + const handleCheckout = async (priceId: string) => { - const stripe = await stripePromise; - const { error } = await stripe!.redirectToCheckout({ - lineItems: [{ price: priceId, quantity: 1 }], - mode: 'subscription', - successUrl: window.location.origin + '/', - cancelUrl: window.location.origin + '/', - }); - - if (error) { - console.error('Error redirecting to checkout:', error); - } + const stripe = await stripePromise; + const { error } = await stripe!.redirectToCheckout({ + lineItems: [{ price: priceId, quantity: 1 }], + mode: 'subscription', + customerEmail: user.email!, + successUrl: window.location.origin + '/', + cancelUrl: window.location.origin + '/', + }); + + if (error) { + console.error('Error redirecting to checkout:', error); + } }; - + return ( -
-

Subscription Plans

-
- {plans.map(plan => ( -
-

{plan.name}

-

${plan.price} per {plan.interval}

-

{plan.description}

- {plan.id !== 'free_plan' && ( - - )} +
+

Subscription Plans

+
+ {plans.map(plan => ( +
+

{plan.name}

+

${plan.price} per {plan.interval}

+

{plan.description}

+ {plan.id !== 'free_plan' && ( + + )} +
+ ))}
- ))}
-
); - }; - - export const PaymentGateway: React.FC = () => { +}; + +export const PaymentGateway: React.FC = () => { + const [stripePromise, setStripePromise] = useState | null>(null); + + useEffect(() => { + const fetchAndSetStripe = async () => { + const apiKey = await fetchApiKey(); + if (apiKey) { + setStripePromise(loadStripe(apiKey)); + } + }; + + fetchAndSetStripe(); + }, []); + + if (!stripePromise) { + return ( +
+ +
Loading...
+
+ ); + } + return ( - - - + + + ); - }; \ No newline at end of file +}; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index d0f4c230..c3f7d387 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -239,14 +239,14 @@ const Chat = () => { if (_user) { const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; + const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; if (id && name) { - setUser({ id, name, role: undefined }); + setUser({ id, name, email, role: undefined }); } // register user if doesn't exist - const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; - + // const response = await getUsers({ user: { id, name, email } }); // to get all users // verifies if user exists and assigns the role @@ -254,7 +254,7 @@ const Chat = () => { const role = result["role"] || undefined; if (result && role) { - setUser({ id, name, role }); + setUser({ id, name, email, role }); } } } diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 8feb8426..a0b8c2aa 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -10,6 +10,7 @@ interface SettingsType { interface UserInfo { id: string; name: string; + email: string | null; role: string | undefined; } @@ -50,6 +51,7 @@ export const AppContext = createContext({ user: { id: "00000000-0000-0000-0000-000000000000", name: "anonymous", + email: "anonymous@gmail.com", role: undefined, }, setUser: () => {}, @@ -80,6 +82,7 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [user, setUser] = useState({ id: "00000000-0000-0000-0000-000000000000", name: "anonymous", + email: "anonymous@gmail.com", role: undefined }); const [chatId, setChatId] = useState(""); From 6e2232015d470eb1c8569c294a5a57602cc4d242 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 9 Jul 2024 12:06:10 -0400 Subject: [PATCH 095/820] SFC-298-299-delete-icons --- .../HistoryPannel/ChatHistoryListItem.tsx | 27 ++++++++++--------- .../HistoryPannel/ChatHistoryPanel.tsx | 5 ---- .../QuestionInput/QuestionInput.module.css | 3 ++- .../QuestionInput/QuestionInput.tsx | 10 ++++--- frontend/src/pages/chat/Chat.tsx | 4 +++ 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index de1b6835..48995985 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -7,8 +7,8 @@ import pencil from "../../assets/pencil.png"; import yes from "../../assets/check.png"; import no from "../../assets/close.png"; import { Spinner } from "@fluentui/react"; -import { ToastContainer, toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; interface ChatHistoryPanelProps { onDeleteChat: () => void; @@ -224,16 +224,19 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete {deletingIsLoading && isConfirmationDelete(conversation.id) ? ( ) : ( - Edit handleDeleteConversation(conversation.id) - : () => setConfirmationDelete(null) - } - /> + <> + {isConfirmationDelete(conversation.id) && + Edit handleDeleteConversation(conversation.id) + : () => setConfirmationDelete(null) + } + />} + )}
) : ( diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index be913551..a61cbb74 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -21,11 +21,6 @@ export const ChatHistoryPanel: React.FC = ({ functionDele
Chat history
-
- -
{ } } catch (e) { setError(e); + console.log(e) + console.log(typeof e) + console.log(Object.keys(e as object)) + console.log((e as Error).toString()) } finally { setIsLoading(false); } From 1cebef1ad5876e2b3e96912b98cb9226d1bc60c2 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:38:51 -0400 Subject: [PATCH 096/820] Change Citation Tab to Source --- frontend/src/components/AnalysisPanel/AnalysisPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx index ede069b0..b117bd1b 100644 --- a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx +++ b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -62,7 +62,7 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh Loading...

}> From 07ebcd91df850903dbe85558c6c2180b1f9a6d2b Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 10 Jul 2024 15:42:01 -0400 Subject: [PATCH 097/820] SFC-296-remove-last-citation --- frontend/src/pages/chat/Chat.tsx | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index b64326e5..1fdb82e3 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -152,10 +152,10 @@ const Chat = () => { } } catch (e) { setError(e); - console.log(e) - console.log(typeof e) - console.log(Object.keys(e as object)) - console.log((e as Error).toString()) + console.log(e); + console.log(typeof e); + console.log(Object.keys(e as object)); + console.log((e as Error).toString()); } finally { setIsLoading(false); } @@ -250,13 +250,13 @@ const Chat = () => { } // register user if doesn't exist - + // const response = await getUsers({ user: { id, name, email } }); // to get all users // verifies if user exists and assigns the role const result = await checkUser({ user: { id, name, email } }); const role = result["role"] || undefined; - + if (result && role) { setUser({ id, name, email, role }); } @@ -363,12 +363,17 @@ const Chat = () => { } as AskResponse; const onToggleTab = (tab: AnalysisPanelTabs, index: number) => { + if (index !== selectedAnswer) { + setActiveCitation(undefined); + setActiveAnalysisPanelTab(undefined); + } if (activeAnalysisPanelTab === tab && selectedAnswer === index) { setActiveAnalysisPanelTab(undefined); + setSelectedAnswer(-1); + setActiveCitation(undefined); } else { setActiveAnalysisPanelTab(tab); } - setSelectedAnswer(index); }; @@ -385,7 +390,7 @@ const Chat = () => { handleNewChat(); } }; - + window.addEventListener("keydown", handleKeyDown); //If I add this on a useEffect it doesn't work, I don't know why //maybe because it's a global event listener and is called multiple times @@ -409,9 +414,8 @@ const Chat = () => { {conversationIsLoading && }
- Sales Factory logo -

FreddAid

+

FreddAid

Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver @@ -420,7 +424,11 @@ const Chat = () => {

) : ( -
+
{conversationIsLoading && } {dataConversation.length > 0 ? dataConversation.map((item, index) => { From eae33ac2b1d16c3cfbecfbaa1e390ee7609c35b6 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:49:29 -0400 Subject: [PATCH 098/820] Change plan name --- frontend/src/components/PaymentGateway/PaymentGateway.tsx | 2 +- startwin.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 235c6aed..883db3f8 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -27,7 +27,7 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise }, { id: 'price_1PYvHVEpF6ccgZLwn6uq6d4J', - name: 'Plus Plan', + name: 'Enterprise Plan', description: 'Access to all features including premium support.', price: '30.00', interval: 'month' diff --git a/startwin.sh b/startwin.sh index 95df511e..a5c84205 100644 --- a/startwin.sh +++ b/startwin.sh @@ -26,7 +26,7 @@ npm install || exit $? echo "Building frontend" # Uncomment the line if you want to run the build -# npm run build || exit $? +npm run build || exit $? echo "Starting backend" cd ../backend From 2ebe681c21b26e0967c22f22d97f10dba8ca5cbf Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Tue, 16 Jul 2024 13:25:56 -0400 Subject: [PATCH 099/820] SFC-301 separating multiple sources from a single string --- .../src/components/Answer/AnswerParser.tsx | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/Answer/AnswerParser.tsx b/frontend/src/components/Answer/AnswerParser.tsx index 177db617..8bfbdd70 100644 --- a/frontend/src/components/Answer/AnswerParser.tsx +++ b/frontend/src/components/Answer/AnswerParser.tsx @@ -8,14 +8,18 @@ type HtmlParsedAnswer = { }; export function removeCitations(text: string): string { - const newText = text.replace(/\[[^\]]*\]/g, ''); + const newText = text.replace(/\[[^\]]*\]/g, ""); return newText; - } +} -export function parseAnswerToHtml(answer: string, showSources: boolean, onCitationClicked: (citationFilePath: string, filename: string) => void): HtmlParsedAnswer { +export function parseAnswerToHtml( + answer: string, + showSources: boolean, + onCitationClicked: (citationFilePath: string, filename: string) => void +): HtmlParsedAnswer { const citations: string[] = []; const followupQuestions: string[] = []; - var answerHtml: string = ""; + var answerHtml: string = ""; // Extract any follow-up questions that might be in the answer let parsedAnswer = answer.replace(/<<([^>>]+)>>/g, (match, content) => { @@ -33,28 +37,47 @@ export function parseAnswerToHtml(answer: string, showSources: boolean, onCitati if (index % 2 === 0) { return part; } else { - let citationIndex: number; - if (citations.indexOf(part) !== -1) { - citationIndex = citations.indexOf(part) + 1; - } else { - citations.push(part); - citationIndex = citations.length; - } - - const path = getCitationFilePath(part); - + // Check if the part string contains multiple citations + const citationParts = part.split(/,\s*/); + + const separatedCitations = citationParts.map(citationPart => { + let citationIndex: number; + if (citations.indexOf(citationPart) !== -1) { + citationIndex = citations.indexOf(citationPart) + 1; + } else { + citations.push(citationPart); + citationIndex = citations.length; + } + + const path = getCitationFilePath(citationPart); + return { + index: citationIndex, + part: citationPart, + path: path + }; + }); + return renderToStaticMarkup( - onCitationClicked(path, part)} tabIndex={0}> - {citationIndex} - + <> + {separatedCitations.map(citation => ( + onCitationClicked(citation.path, citation.part)} + tabIndex={0} + > + {citation.index} + + ))} + ); } }); answerHtml = fragments.join(""); - - } else { + } else { answerHtml = removeCitations(parsedAnswer); - } + } return { answerHtml: answerHtml, From bd88ccb824091011a46d32c7956adfa7b14ba9a8 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:15:44 -0400 Subject: [PATCH 100/820] allow the user to manually enter their email --- frontend/src/components/PaymentGateway/PaymentGateway.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 883db3f8..8594b432 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -3,8 +3,6 @@ import { loadStripe, Stripe } from '@stripe/stripe-js'; import { Elements } from '@stripe/react-stripe-js'; import styles from "./PaymentGateway.module.css"; import { getApiKeyPayment } from "../../api"; -import { AppContext } from "../../providers/AppProviders"; -import { useContext } from "react"; import { Spinner } from "@fluentui/react"; const fetchApiKey = async () => { @@ -14,7 +12,6 @@ const fetchApiKey = async () => { export const SubscriptionPlans: React.FC<{ stripePromise: Promise }> = ({ stripePromise }) => { const [plans, setPlans] = useState([]); - const {user} = useContext(AppContext); useEffect(() => { setPlans([ @@ -40,7 +37,6 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise const { error } = await stripe!.redirectToCheckout({ lineItems: [{ price: priceId, quantity: 1 }], mode: 'subscription', - customerEmail: user.email!, successUrl: window.location.origin + '/', cancelUrl: window.location.origin + '/', }); From fa54f98464a2232da6fed1c9af90db5f0a979908 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:53:59 -0400 Subject: [PATCH 101/820] SFC-145 FE - Add screen reader compatibility --- .../src/components/AnalysisPanel/AnalysisPanel.tsx | 5 +++++ .../components/PaymentGateway/PaymentGateway.tsx | 13 ++++++++++--- frontend/src/components/SettingsPanel/index.tsx | 5 +++-- frontend/src/pages/chat/Chat.tsx | 13 +++++++++---- startwin.sh | 2 +- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx index b117bd1b..05958761 100644 --- a/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx +++ b/frontend/src/components/AnalysisPanel/AnalysisPanel.tsx @@ -51,11 +51,13 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh className={className} selectedKey={activeTab} onLinkClick={pivotItem => pivotItem && onActiveTabChanged(pivotItem.props.itemKey! as AnalysisPanelTabs)} + aria-label="Analysis Panel" >
@@ -64,6 +66,7 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh itemKey={AnalysisPanelTabs.CitationTab} headerText="Source" headerButtonProps={isDisabledCitationTab ? pivotItemDisabledStyle : undefined} + aria-label="Source Tab" > Loading...

}> @@ -81,6 +84,7 @@ export const AnalysisPanel = ({ answer, activeTab, activeCitation, citationHeigh cursor: "pointer", backgroundColor: "transparent" }} + aria-label="Close Panel Button" >
)} + aria-label="Close Panel Pivot Item" /> diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 883db3f8..d22a6dc7 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -51,7 +51,7 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise }; return ( -
+

Subscription Plans

{plans.map(plan => ( @@ -60,7 +60,14 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise

${plan.price} per {plan.interval}

{plan.description}

{plan.id !== 'free_plan' && ( - + )}
))} @@ -85,7 +92,7 @@ export const PaymentGateway: React.FC = () => { if (!stripePromise) { return ( -
+
Loading...
diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index c08bdd50..64871de2 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -171,7 +171,7 @@ export const SettingsPanel = () => { }; return ( -
+
{ showValue snapToStep onChange={e => handleSetTemperature(e)} + aria-labelledby="temperature-slider" />
- setIsDialogOpen(true)}> + setIsDialogOpen(true)} aria-label="Save settings">   Save diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 1fdb82e3..7051b9d7 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -441,7 +441,7 @@ const Chat = () => { return (
-
+
{ : answers.map((answer, index) => (
-
+
{ {error ? ( <> -
+
makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null)} @@ -572,6 +572,7 @@ const Chat = () => { multiline autoAdjustHeight onChange={onPromptTemplateChange} + aria-label="Override prompt template" /> { max={50} defaultValue={retrieveCount.toString()} onChange={onRetrieveCountChange} + aria-label="Number of documents to retrieve" /> - + { label="Use query-contextual summaries instead of whole documents" onChange={onUseSemanticCaptionsChange} disabled={!useSemanticRanker} + aria-label="Use query-contextual summaries" />
diff --git a/startwin.sh b/startwin.sh index a5c84205..76444895 100644 --- a/startwin.sh +++ b/startwin.sh @@ -26,7 +26,7 @@ npm install || exit $? echo "Building frontend" # Uncomment the line if you want to run the build -npm run build || exit $? +#npm run build || exit $? echo "Starting backend" cd ../backend From c9982da252d22f8f16685a838c0a6095aa0d1e07 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Wed, 17 Jul 2024 18:48:34 -0400 Subject: [PATCH 102/820] SFC-312 create function to upload files to azure blob storage --- backend/app.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index bd287d84..26d05d1c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -12,6 +12,7 @@ from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient from urllib.parse import unquote +import uuid import smtplib from email.mime.text import MIMEText @@ -55,6 +56,7 @@ def get_secret(secretName): SPEECH_RECOGNITION_LANGUAGE = os.getenv("SPEECH_RECOGNITION_LANGUAGE") SPEECH_SYNTHESIS_LANGUAGE = os.getenv("SPEECH_SYNTHESIS_LANGUAGE") SPEECH_SYNTHESIS_VOICE_NAME = os.getenv("SPEECH_SYNTHESIS_VOICE_NAME") +AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING") app = Flask(__name__) CORS(app) @@ -229,6 +231,7 @@ def getStorageAccount(): logging.exception("[webbackend] exception in /api/get-storage-account") return jsonify({"error": str(e)}), 500 + @app.route("/api/stripe", methods=["GET"]) def getStripe(): try: @@ -239,6 +242,37 @@ def getStripe(): logging.exception("[webbackend] exception in /api/stripe") return jsonify({"error": str(e)}), 500 + +@app.route("/api/upload-blob", methods=["POST"]) +def uploadBlob(): + if 'file' not in request.files: + print('No file sent') + return jsonify({"error": "No file sent"}), 400 + + valid_file_extensions = [".csv", ".xlsx", ".xls"] + + file = request.files['file'] + + extension = os.path.splitext(file.filename)[1] + + if extension not in valid_file_extensions: + return jsonify({"error": "Invalid file type"}), 400 + + filename = str(uuid.uuid4()) + extension + + try: + blob_service_client = BlobServiceClient.from_connection_string( + AZURE_STORAGE_CONNECTION_STRING + ) + blob_client = blob_service_client.get_blob_client(container="files", blob=filename) + blob_client.upload_blob(data=file, blob_type="BlockBlob") + + return jsonify({"blob_url": blob_client.url}), 200 + except Exception as e: + logging.exception("[webbackend] exception in /api/upload-blob") + return jsonify({"error": str(e)}), 500 + + @app.route("/api/get-blob", methods=["POST"]) def getBlob(): logging.exception("------------------ENTRA ------------") @@ -656,4 +690,4 @@ def checkUser(): if __name__ == "__main__": - app.run(host="0.0.0.0", port=8000) + app.run(host="0.0.0.0", port=8000, debug=True) From 87a5445b3fd6fe3d738ef17b2c90dc8356806de5 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Wed, 17 Jul 2024 18:52:24 -0400 Subject: [PATCH 103/820] added new envvar for container name --- backend/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index 26d05d1c..1cf713b6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -57,6 +57,7 @@ def get_secret(secretName): SPEECH_SYNTHESIS_LANGUAGE = os.getenv("SPEECH_SYNTHESIS_LANGUAGE") SPEECH_SYNTHESIS_VOICE_NAME = os.getenv("SPEECH_SYNTHESIS_VOICE_NAME") AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING") +AZURE_CSV_STORAGE_NAME = os.getenv("AZURE_CSV_STORAGE_CONTAINER", "files") app = Flask(__name__) CORS(app) @@ -264,7 +265,7 @@ def uploadBlob(): blob_service_client = BlobServiceClient.from_connection_string( AZURE_STORAGE_CONNECTION_STRING ) - blob_client = blob_service_client.get_blob_client(container="files", blob=filename) + blob_client = blob_service_client.get_blob_client(container=AZURE_CSV_STORAGE_NAME, blob=filename) blob_client.upload_blob(data=file, blob_type="BlockBlob") return jsonify({"blob_url": blob_client.url}), 200 From 5dcf3b29fdc8d0d8fd39a47129e08f9b0dda9217 Mon Sep 17 00:00:00 2001 From: Yalwin <56238618+Yalwin@users.noreply.github.com> Date: Thu, 18 Jul 2024 00:50:50 -0400 Subject: [PATCH 104/820] SFC-143 FE - Add keyboard navigation --- frontend/src/pages/chat/Chat.tsx | 8 ++++++-- frontend/src/providers/AppProviders.tsx | 14 +++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 1fdb82e3..01a2598e 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -382,10 +382,14 @@ const Chat = () => { }; const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "O" && event.ctrlKey && event.shiftKey) { + + const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isAlt = event.altKey; + + if (event.code === 'KeyO' && isCtrlOrCmd && isAlt) { event.preventDefault(); clearChat(); - } else if (event.key === "Y" && event.ctrlKey && event.shiftKey) { + } else if (event.code === 'KeyY' && isCtrlOrCmd && isAlt) { event.preventDefault(); handleNewChat(); } diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index a0b8c2aa..6547fba1 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -93,27 +93,31 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [newChatDeleted, setNewChatDeleted] = useState(false); const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "?" && event.ctrlKey && event.shiftKey) { + + const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isAlt = event.altKey; + + if (event.code === 'Slash' && isCtrlOrCmd && isAlt) { event.preventDefault(); setShowFeedbackRatingPanel(!showFeedbackRatingPanel); setSettingsPanel(false); setShowHistoryPanel(false); - } else if (event.key === ";" && event.ctrlKey && event.shiftKey) { + } else if (event.code === 'Period' && isCtrlOrCmd && isAlt) { event.preventDefault() setShowHistoryPanel(!showHistoryPanel); setShowFeedbackRatingPanel(false); setSettingsPanel(false); - } else if (event.key === ":" && event.ctrlKey && event.shiftKey) { + } else if (event.code === 'Comma' && isCtrlOrCmd && isAlt) { event.preventDefault() setSettingsPanel(!settingsPanel); setShowHistoryPanel(false); setShowFeedbackRatingPanel(false); } - else if(event.key === ")" && event.ctrlKey && event.shiftKey){ + else if(event.code === 'Digit0' && isCtrlOrCmd && isAlt){ event.preventDefault() window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; } - else if(event.key === "(" && event.ctrlKey && event.shiftKey){ + else if(event.code === 'Digit9' && isCtrlOrCmd && isAlt){ event.preventDefault() window.location.href = "#/admin"; } From 3283ed66ff6beb07389746ab2d3a48c2b40ec529 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 22 Jul 2024 19:36:14 -0400 Subject: [PATCH 105/820] SFC-316-connect-button-upload-function --- backend/app.py | 3 + frontend/src/api/api.ts | 52 ++++++++++++------ frontend/src/api/models.ts | 1 + .../QuestionInput/QuestionInput.tsx | 55 +++++++++++++++---- frontend/src/pages/chat/Chat.tsx | 15 +++-- 5 files changed, 92 insertions(+), 34 deletions(-) diff --git a/backend/app.py b/backend/app.py index 1cf713b6..9c42bb07 100644 --- a/backend/app.py +++ b/backend/app.py @@ -73,10 +73,12 @@ def static_file(path): def chatgpt(): conversation_id = request.json["conversation_id"] question = request.json["query"] + file_blob_url = requests.json["url"] client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") logging.info("[webbackend] conversation_id: " + conversation_id) logging.info("[webbackend] question: " + question) + logging.info("[webbackend] file_blob_url: " + file_blob_url) logging.info(f"[webbackend] User principal: {client_principal_id}") logging.info(f"[webbackend] User name: {client_principal_name}") @@ -104,6 +106,7 @@ def chatgpt(): { "conversation_id": conversation_id, "question": question, + "url": file_blob_url, "client_principal_id": client_principal_id, "client_principal_name": client_principal_name, } diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index c58e458a..afa06dba 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -13,11 +13,10 @@ import { UserInfo } from "./models"; - export async function getUsers({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; - try{ + try { const response = await fetch("/api/getusers", { method: "GET", headers: { @@ -26,19 +25,17 @@ export async function getUsers({ user }: any): Promise { "X-MS-CLIENT-PRINCIPAL-NAME": user_name } }); - + const parsedResponse = await response.json(); if (response.status > 299 || !response.ok) { throw Error("Unknown error in getUsers"); } return parsedResponse; - } - catch(error){ + } catch (error) { console.log("Error fetching users", error); return { data: null }; } -}; - +} export async function checkUser({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; @@ -55,7 +52,7 @@ export async function checkUser({ user }: any): Promise { email: user.email }) }); - + const parsedResponse = await response.json(); if (response.status > 299 || !response.ok) { throw Error("Unknown error in checkUser"); @@ -64,7 +61,7 @@ export async function checkUser({ user }: any): Promise { } return { data: null }; -}; +} export async function getUserInfo(): Promise { const response = await fetch("/.auth/me"); @@ -107,7 +104,7 @@ export async function postSettings({ user, temperature }: PostSettingsProps): Pr "X-MS-CLIENT-PRINCIPAL-NAME": user_name }, body: JSON.stringify({ - temperature, + temperature }) }); const fetchedData = await response.json(); @@ -129,6 +126,7 @@ export async function chatApiGpt(options: ChatRequestGpt): Promise { try { const response = await fetch(`/api/chat-conversations/${chatId}`, { @@ -245,7 +242,7 @@ export function getCitationFilePath(citation: string): string { } export function getFilePath(fileUrl: string) { - if(!fileUrl.endsWith(".pdf") || !fileUrl.endsWith(".docx") || fileUrl.endsWith(".doc")) { + if (!fileUrl.endsWith(".pdf") || !fileUrl.endsWith(".docx") || fileUrl.endsWith(".doc")) { return fileUrl; } const regex = /documents\/(.*)/; @@ -289,7 +286,7 @@ export async function postFeedbackRating({ user, conversation_id, feedback_messa }); } -export async function inviteUser ({ username, email }: any): Promise { +export async function inviteUser({ username, email }: any): Promise { try { const response = await fetch("/api/inviteUser", { method: "POST", @@ -305,21 +302,42 @@ export async function inviteUser ({ username, email }: any): Promise { return fetchedData; } catch (error) { console.error("Error inviting user", error); - return {error : error}; + return { error: error }; } } export async function getApiKeyPayment(): Promise { const response = await fetch("/api/stripe", { method: "GET", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json" } }); - + if (response.status > 299 || !response.ok) { throw Error("Error getting Api key payment"); } - + const apiKey = await response.text(); return apiKey; } + +export async function uploadFile(file: any) { + const formdata = new FormData(); + formdata.append("file", file); + try { + const response = await fetch("/api/upload-blob", { + method: "POST", + body: formdata, + redirect: "follow" + }); + if (!response.ok) { + throw new Error(`Server responded with ${response.status}: ${response.statusText}`); + } + const result = await response.json(); + console.log("File uploaded successfully:", result); + return result; + } catch (error) { + console.error("Error uploading file:", error); + throw error; + } +} diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 4f526af2..7fb84c2e 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -84,6 +84,7 @@ export type ChatRequestGpt = { approach: Approaches; conversation_id: string; query: string; + file_blob_url: string; overrides?: AskRequestOverrides; }; diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 5f1f84b5..f3bea8f5 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -4,9 +4,11 @@ import { getTokenOrRefresh } from "./token_util"; import { Send32Filled, Attach32Filled, SlideMicrophone32Filled } from "@fluentui/react-icons"; import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from "microsoft-cognitiveservices-speech-sdk"; +import { uploadFile } from "../../api"; + import styles from "./QuestionInput.module.css"; interface Props { - onSend: (question: string) => void; + onSend: (question: string, fileBlobUrl: string | null) => void; disabled: boolean; placeholder?: string; clearOnSend?: boolean; @@ -14,23 +16,37 @@ interface Props { import { useFilePicker } from "use-file-picker"; -export const FileAttachmentInput = () => { +export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: string) => void}) => { const [files, setFiles] = useState([]); + const [loadingFiles, setLoadingFiles] = useState(false); + const [error, setError] = useState(""); + const { openFilePicker, filesContent, loading, errors } = useFilePicker({ readAs: "DataURL", accept: ["xls", "xlsx", "csv"], multiple: false, - onFilesSelected: ({ plainFiles, filesContent, errors }) => { + onFilesSelected: async ({ plainFiles, filesContent, errors }) => { // this callback is always called, even if there are errors - console.log("onFilesSelected", plainFiles, filesContent, errors); + setLoadingFiles(true); + var response; + try{ + response = await uploadFile(plainFiles[0]); + setLoadingFiles(false); + setError(""); + setFileBlobUrl(response.data.blob_url); + } catch (error) { + setLoadingFiles(false); + setError("Error uploading file"); + return; + } }, onFilesRejected: ({ errors }) => { // this callback is called when there were validation errors - console.log("onFilesRejected", errors); + setError("Error with the file picker"); + setLoadingFiles(false); }, - onFilesSuccessfullySelected: ({ plainFiles, filesContent }) => { + onFilesSuccessfullySelected: async ({ plainFiles, filesContent }) => { // this callback is called when the files are successfully selected - console.log("onFilesSuccessfullySelected", plainFiles, filesContent); setFiles(plainFiles); } }); @@ -83,7 +99,7 @@ export const FileAttachmentInput = () => { display: "flex", flexDirection: "row", position: "fixed", - bottom: 200, + bottom: 200 }} > {files.map((file, index) => ( @@ -104,7 +120,7 @@ export const FileAttachmentInput = () => { overflow: "hidden", textOverflow: "ellipsis", backgroundColor: "white", - zIndex: 5, + zIndex: 5 }} >
))} + {error &&
{error}
} + {loadingFiles && !error && ( +
+ Loading file... + +
+ )}

@@ -145,13 +175,14 @@ export const FileAttachmentInput = () => { export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { const [question, setQuestion] = useState(""); + const [fileBlobUrl, setFileBlobUrl] = useState(null); const sendQuestion = () => { if (disabled || !question.trim()) { return; } - onSend(question); + onSend(question, fileBlobUrl); if (clearOnSend) { setQuestion(""); @@ -211,7 +242,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr return ( - + { } = useContext(AppContext); const lastQuestionRef = useRef(""); + const lastFileBlobUrl = useRef(""); const chatMessageStreamEnd = useRef(null); const [fileType, setFileType] = useState(""); @@ -77,8 +78,9 @@ const Chat = () => { const [userId, setUserId] = useState(""); // this is more like a conversation id instead of a user id const triggered = useRef(false); - const makeApiRequestGpt = async (question: string, chatId: string | null) => { + const makeApiRequestGpt = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { lastQuestionRef.current = question; + lastFileBlobUrl.current = fileBlobUrl || ""; error && setError(undefined); setIsLoading(true); @@ -98,6 +100,7 @@ const Chat = () => { approach: Approaches.ReadRetrieveRead, conversation_id: chatId !== null ? chatId : userId, query: question, + file_blob_url: fileBlobUrl || "", overrides: { promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, @@ -164,6 +167,7 @@ const Chat = () => { const clearChat = () => { if (lastQuestionRef.current || dataConversation.length > 0 || !chatIsCleaned) { lastQuestionRef.current = ""; + lastFileBlobUrl.current = ""; error && setError(undefined); setActiveCitation(undefined); setActiveAnalysisPanelTab(undefined); @@ -178,6 +182,7 @@ const Chat = () => { const handleNewChat = () => { if (lastQuestionRef.current || dataConversation.length > 0 || chatIsCleaned) { lastQuestionRef.current = ""; + lastFileBlobUrl.current = ""; error && setError(undefined); setActiveCitation(undefined); setActiveAnalysisPanelTab(undefined); @@ -453,7 +458,7 @@ const Chat = () => { onCitationClicked={(c, n) => onShowCitation(c, n, index)} onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} showFollowupQuestions={false} showSources={true} /> @@ -472,7 +477,7 @@ const Chat = () => { onCitationClicked={(c, n) => onShowCitation(c, n, index)} onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} showFollowupQuestions={false} showSources={true} /> @@ -494,7 +499,7 @@ const Chat = () => {
makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null)} + onRetry={() => makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current)} />
@@ -527,7 +532,7 @@ const Chat = () => { clearOnSend placeholder={placeholderText} disabled={isLoading} - onSend={question => makeApiRequestGpt(question, chatId !== "" ? chatId : null)} + onSend={(question, fileBlobUrl) => makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null)} />
From 5d6ef55e5ae4948c335a7db62babb9defb496099 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 23 Jul 2024 09:15:52 -0400 Subject: [PATCH 106/820] attend comments --- frontend/src/pages/chat/Chat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 54e67f81..3bd1cf80 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -62,7 +62,7 @@ const Chat = () => { } = useContext(AppContext); const lastQuestionRef = useRef(""); - const lastFileBlobUrl = useRef(""); + const lastFileBlobUrl = useRef(""); const chatMessageStreamEnd = useRef(null); const [fileType, setFileType] = useState(""); @@ -80,7 +80,7 @@ const Chat = () => { const makeApiRequestGpt = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { lastQuestionRef.current = question; - lastFileBlobUrl.current = fileBlobUrl || ""; + lastFileBlobUrl.current = fileBlobUrl; error && setError(undefined); setIsLoading(true); From 3131deb1f6600a0ee53a1fe3756a05f985f5496b Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 23 Jul 2024 11:04:05 -0400 Subject: [PATCH 107/820] SFC-307-add-confirmation-message-suscription --- .../PaymentGateway/PaymentGateway.module.css | 2 +- .../PaymentGateway/PaymentGateway.tsx | 2 +- .../PaymentGateway/SuccessPayment.module.css | 34 +++++++++++++++++++ .../PaymentGateway/SuccessPayment.tsx | 24 +++++++++++++ frontend/src/index.tsx | 8 +++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/PaymentGateway/SuccessPayment.module.css create mode 100644 frontend/src/components/PaymentGateway/SuccessPayment.tsx diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.module.css b/frontend/src/components/PaymentGateway/PaymentGateway.module.css index d665daa0..1af27275 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.module.css +++ b/frontend/src/components/PaymentGateway/PaymentGateway.module.css @@ -3,7 +3,7 @@ .subscriptionPlan { font-family: 'Poppins', sans-serif; text-align: center; - margin-top: 80px; + margin-top: 40px; padding: 20px; } diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 81bcee2d..eff29b04 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -37,7 +37,7 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise const { error } = await stripe!.redirectToCheckout({ lineItems: [{ price: priceId, quantity: 1 }], mode: 'subscription', - successUrl: window.location.origin + '/', + successUrl: window.location.origin + '/success-payment', cancelUrl: window.location.origin + '/', }); diff --git a/frontend/src/components/PaymentGateway/SuccessPayment.module.css b/frontend/src/components/PaymentGateway/SuccessPayment.module.css new file mode 100644 index 00000000..b98663a3 --- /dev/null +++ b/frontend/src/components/PaymentGateway/SuccessPayment.module.css @@ -0,0 +1,34 @@ +.planContainer { + display: flex; + flex-direction: row; + justify-content: center; +} + +.textColumn { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 20px; +} + +.planButton { + background-color: #6772e5; + color: white; + border: none; + padding: 12px 25px; + border-radius: 25px; + cursor: pointer; + font-size: 1em; + transition: background-color 0.3s; + font-family: 'Poppins', sans-serif; + outline: none; + } + + .planButton:hover { + background-color: #5469d4; + } + + .planButton:focus { + background-color: #3f51b5; + } \ No newline at end of file diff --git a/frontend/src/components/PaymentGateway/SuccessPayment.tsx b/frontend/src/components/PaymentGateway/SuccessPayment.tsx new file mode 100644 index 00000000..0e40a1c9 --- /dev/null +++ b/frontend/src/components/PaymentGateway/SuccessPayment.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import styles from "./SuccessPayment.module.css"; + +const SuccessPayment: React.FC = () => { + return ( +
+
+

Payment Successful!

+

Thank you for subscribing to our service.

+

Your payment has been successfully processed.

+ +
+
+ ); +}; + +export default SuccessPayment; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 200439b7..701aa32a 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -13,6 +13,7 @@ import Chat from "./pages/chat/Chat"; import Admin from "./pages/admin/Admin"; import { AppProvider } from "./providers/AppProviders"; import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; +import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; initializeIcons(); @@ -47,6 +48,13 @@ export default function App() { /> } /> + }> + } + /> + } /> + From 595cd6aec0adabddd837f8fdbb4c09afda0d9dcc Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 23 Jul 2024 11:47:57 -0400 Subject: [PATCH 108/820] SFC-fix-error --- backend/app.py | 2 +- start.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index 9c42bb07..ad29007e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -78,7 +78,7 @@ def chatgpt(): client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") logging.info("[webbackend] conversation_id: " + conversation_id) logging.info("[webbackend] question: " + question) - logging.info("[webbackend] file_blob_url: " + file_blob_url) + logging.info(f"[webbackend] file_blob_url: {file_blob_url}") logging.info(f"[webbackend] User principal: {client_principal_id}") logging.info(f"[webbackend] User name: {client_principal_name}") diff --git a/start.sh b/start.sh index e1aa484c..96911451 100755 --- a/start.sh +++ b/start.sh @@ -48,6 +48,7 @@ fi echo "" echo "Building frontend" +npm run build echo "" # npm run build From d294f6c19655c38df0f7e1ab1fdfe6ce7fb8d971 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 25 Jul 2024 15:52:02 -0400 Subject: [PATCH 109/820] SFC-fix-request --- backend/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index ad29007e..260e69e2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -73,7 +73,7 @@ def static_file(path): def chatgpt(): conversation_id = request.json["conversation_id"] question = request.json["query"] - file_blob_url = requests.json["url"] + file_blob_url = request.json["url"] client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") logging.info("[webbackend] conversation_id: " + conversation_id) From 85505f496f22cb9cb58864f15f237509d697607c Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 29 Jul 2024 13:50:46 -0400 Subject: [PATCH 110/820] SFC-fix-history-order --- frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 48995985..036df7ef 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -160,7 +160,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete return matches; } return false; - }); + }).reverse(); return { label, data: filteredData }; }); From bffbf72d18391231b4fa28928e22ce387894a8cc Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Mon, 29 Jul 2024 17:05:34 -0400 Subject: [PATCH 111/820] SFC-267 answer question given csv --- frontend/src/components/QuestionInput/QuestionInput.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index f3bea8f5..992048b0 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -28,12 +28,11 @@ export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: str onFilesSelected: async ({ plainFiles, filesContent, errors }) => { // this callback is always called, even if there are errors setLoadingFiles(true); - var response; try{ - response = await uploadFile(plainFiles[0]); + const data = await uploadFile(plainFiles[0]); setLoadingFiles(false); setError(""); - setFileBlobUrl(response.data.blob_url); + setFileBlobUrl(data.blob_url); } catch (error) { setLoadingFiles(false); setError("Error uploading file"); From 6b1e107815dc5136cd2ac03239a480de097978a0 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 30 Jul 2024 08:52:36 -0400 Subject: [PATCH 112/820] SFC-334-modify-admin-style (#92) --- frontend/src/pages/admin/Admin.tsx | 326 +++++++++++++++-------------- 1 file changed, 173 insertions(+), 153 deletions(-) diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 38ad2293..2bc7abae 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -1,11 +1,9 @@ -import React, { useEffect, useState, ReactNode, useContext } from "react"; +import React, { useEffect, useState, useContext } from "react"; import { PrimaryButton, IconButton, Spinner, Dialog, DialogContent, Label, Dropdown, DefaultButton, MessageBar } from "@fluentui/react"; -import { Announced } from "@fluentui/react/lib/Announced"; + import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; import { AddFilled } from "@fluentui/react-icons"; -import { DetailsList, DetailsListLayoutMode, Selection, IColumn } from "@fluentui/react/lib/DetailsList"; -import { MarqueeSelection } from "@fluentui/react/lib/MarqueeSelection"; -import { mergeStyles } from "@fluentui/react/lib/Styling"; + import { AppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; @@ -13,141 +11,7 @@ import { checkUser, getUsers, inviteUser } from "../../api"; import styles from "./Admin.module.css"; -const exampleChildClass = mergeStyles({ - display: "block", - marginBottom: "10px" -}); - -const textFieldStyles: Partial = { root: { maxWidth: "300px" } }; - -export interface IUserListItem { - key: number; - name: string; - email: string; - role: string; - actions: ReactNode; -} - -export interface IUserListState { - items: IUserListItem[]; - selectionDetails: string; -} - -export class UserList extends React.Component< - { - users: any[]; - }, - IUserListState -> { - private _selection: Selection; - private _allItems: IUserListItem[]; - private _columns: IColumn[]; - - constructor(props: { users: any[] }) { - super(props); - - this._selection = new Selection({ - onSelectionChanged: () => this.setState({ selectionDetails: this._getSelectionDetails() }) - }); - - // Populate with items for demos. - this._allItems = props.users.map((user, index) => { - return { - key: user.id, - name: user.data.name, - email: user.data.email, - role: user.data.role, - value: index, - actions: actions({}) - }; - }); - - this._columns = [ - { key: "column1", name: "Name", fieldName: "name", minWidth: 100, maxWidth: 200, isResizable: true }, - { key: "column2", name: "Email", fieldName: "email", minWidth: 200, maxWidth: 300, isResizable: true }, - { key: "column3", name: "Role", fieldName: "role", minWidth: 100, maxWidth: 100, isResizable: false }, - { key: "column4", name: "Actions", fieldName: "actions", minWidth: 200, maxWidth: 300, isResizable: true } - ]; - - this.state = { - items: this._allItems, - selectionDetails: this._getSelectionDetails() - }; - } - - public render(): JSX.Element { - const { items, selectionDetails } = this.state; - return ( -
-
{selectionDetails}
- - - - - - - {this._allItems.length === 0 && ( -
-

No users found

-
- )} -
- ); - } - - private _getSelectionDetails(): string { - const selectionCount = this._selection.getSelectedCount(); - - switch (selectionCount) { - case 0: - return "No items selected"; - case 1: - return "1 item selected: " + (this._selection.getSelection()[0] as IUserListItem).name; - default: - return `${selectionCount} items selected`; - } - } - - private _onFilter = (ev: React.FormEvent, text: string | undefined): void => { - this.setState({ - items: text ? this._allItems.filter(i => i.name.toLowerCase().indexOf(text.toLowerCase()) > -1) : this._allItems - }); - }; - - private _onItemInvoked = (item: IUserListItem): void => { - alert(`Item invoked: ${item.name}`); - }; -} - -const actions: React.FC = () => { - const iconStyle = { - icon: { color: "black" }, - root: { - selectors: { - ":hover .ms-Button-icon": { - color: "rgb(0, 120, 212);" - } - } - } - }; - return ( -
- {}} /> - {}} /> -
- ); -}; - +const textFieldStyles: Partial = { root: { maxWidth: "900px" } }; export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; setIsOpen: React.Dispatch>; users: never[] }) => { const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); @@ -306,6 +170,25 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; > { onConfirm(); @@ -340,6 +223,8 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; const Admin = () => { const { user } = useContext(AppContext); + const [search, setSearch] = useState(""); + const [filteredUsers, setFilteredUsers] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); @@ -352,12 +237,23 @@ const Admin = () => { if (!Array.isArray(usersList)) { usersList = []; } - setUsers(usersList); + //setUsers(usersList); setLoading(false); }; getUserList(); }, []); + useEffect(() => { + if (!search) { + setFilteredUsers(users); + } else { + const filtered = users.filter((user: any) => { + return user.name.toLowerCase().includes(search.toLowerCase()) || user.email.toLowerCase().includes(search.toLowerCase()); + }); + setFilteredUsers(filtered); + } + }, [search]); + return (
{user.role !== "admin" &&

Access denied

} @@ -378,28 +274,152 @@ const Admin = () => {

Roles and access

+
+
{ setIsOpen(true); }} /> + { + setSearch(newValue || ""); + }} + />
+ -
- {loading ? ( - + ) : ( + + - ) : ( - - )} - + > + + + + + + + + + {filteredUsers.map((user: any, index) => { + return ( + + + + + + + ); + })} + +
+ Name + EmailRoleActions
{user.name}{user.email} +
+
+ {user.role} +
+
+
+ { +
+ {}} + /> + {}} + /> +
+ } +
+ )} )}
From c25307b192f4d820872912ca53f8a1c961c94c5a Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 30 Jul 2024 16:55:07 -0400 Subject: [PATCH 113/820] SFC-fix-user-list --- frontend/src/pages/admin/Admin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 2bc7abae..2df85524 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -237,7 +237,7 @@ const Admin = () => { if (!Array.isArray(usersList)) { usersList = []; } - //setUsers(usersList); + setUsers(usersList); setLoading(false); }; getUserList(); From 03a9d0b5d99bb7ddc6828c1c25087ef6f0015745 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 30 Jul 2024 21:09:52 -0400 Subject: [PATCH 114/820] SFC-334-fixs --- frontend/src/pages/admin/Admin.tsx | 72 ++++++++++++++++++------------ 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 2df85524..4e5c8bfb 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -170,25 +170,24 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; > { onConfirm(); @@ -238,6 +237,7 @@ const Admin = () => { usersList = []; } setUsers(usersList); + setFilteredUsers(usersList); setLoading(false); }; getUserList(); @@ -248,7 +248,7 @@ const Admin = () => { setFilteredUsers(users); } else { const filtered = users.filter((user: any) => { - return user.name.toLowerCase().includes(search.toLowerCase()) || user.email.toLowerCase().includes(search.toLowerCase()); + return user.data.name.toLowerCase().includes(search.toLowerCase()) || user.data.email.toLowerCase().includes(search.toLowerCase()); }); setFilteredUsers(filtered); } @@ -363,9 +363,27 @@ const Admin = () => { {filteredUsers.map((user: any, index) => { return ( - - {user.name} - {user.email} + + + {user.data.name} + + + {user.data.email} +
{
- {user.role} + {user.data.role}
@@ -393,7 +411,6 @@ const Admin = () => {
{ /> Date: Wed, 31 Jul 2024 10:43:55 -0400 Subject: [PATCH 115/820] SFC-330-endpoint-suscription-status --- backend/app.py | 57 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/backend/app.py b/backend/app.py index 260e69e2..3100d8c2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -249,13 +249,13 @@ def getStripe(): @app.route("/api/upload-blob", methods=["POST"]) def uploadBlob(): - if 'file' not in request.files: - print('No file sent') + if "file" not in request.files: + print("No file sent") return jsonify({"error": "No file sent"}), 400 - + valid_file_extensions = [".csv", ".xlsx", ".xls"] - file = request.files['file'] + file = request.files["file"] extension = os.path.splitext(file.filename)[1] @@ -268,9 +268,11 @@ def uploadBlob(): blob_service_client = BlobServiceClient.from_connection_string( AZURE_STORAGE_CONNECTION_STRING ) - blob_client = blob_service_client.get_blob_client(container=AZURE_CSV_STORAGE_NAME, blob=filename) + blob_client = blob_service_client.get_blob_client( + container=AZURE_CSV_STORAGE_NAME, blob=filename + ) blob_client.upload_blob(data=file, blob_type="BlockBlob") - + return jsonify({"blob_url": blob_client.url}), 200 except Exception as e: logging.exception("[webbackend] exception in /api/upload-blob") @@ -693,5 +695,48 @@ def checkUser(): return jsonify({"error": str(e)}), 500 +@app.route("/api/getUser", methods=["GET"]) +def getUser(): + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") + + if not client_principal_id or not client_principal_name: + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) + + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = "orchestrator-host--checkuser" + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--checkuser") + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + + try: + url = CHECK_USER_ENDPOINT + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + response = requests.request( + "GET", url, headers=headers, params={"id": client_principal_id} + ) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text + except Exception as e: + logging.exception("[webbackend] exception in /getUser") + return jsonify({"error": str(e)}), 500 + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) From c0cbfcd4c4d5ebfe16dabbd3bbdba26ef0e47496 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 1 Aug 2024 12:15:44 -0400 Subject: [PATCH 116/820] SFC-fix-citation-urls-path (#96) --- .../src/components/Answer/AnswerParser.tsx | 2 +- .../Answer/parseAnswerToHtml.test.tsx | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/Answer/parseAnswerToHtml.test.tsx diff --git a/frontend/src/components/Answer/AnswerParser.tsx b/frontend/src/components/Answer/AnswerParser.tsx index 8bfbdd70..0092ef41 100644 --- a/frontend/src/components/Answer/AnswerParser.tsx +++ b/frontend/src/components/Answer/AnswerParser.tsx @@ -64,7 +64,7 @@ export function parseAnswerToHtml( key={`citation-${citation.index}`} className="supContainer" title={citation.part} - onClick={() => onCitationClicked(citation.path, citation.part)} + onClick={() => onCitationClicked(citation.part, citation.path)} tabIndex={0} > {citation.index} diff --git a/frontend/src/components/Answer/parseAnswerToHtml.test.tsx b/frontend/src/components/Answer/parseAnswerToHtml.test.tsx new file mode 100644 index 00000000..0f0e981f --- /dev/null +++ b/frontend/src/components/Answer/parseAnswerToHtml.test.tsx @@ -0,0 +1,71 @@ +import { describe, it, expect, vi } from "vitest"; +import React from "react"; +import { render, fireEvent } from "@testing-library/react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { parseAnswerToHtml } from "./AnswerParser"; // Adjust path as needed +import { getCitationFilePath } from "../../api"; // Adjust path as needed +// Mock the getCitationFilePath function +vi.mock("../../api", () => ({ + getCitationFilePath: vi.fn(citationPart => `https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/${citationPart}`) +})); + +describe("parseAnswerToHtml", () => { + it("should parse the answer and generate the correct HTML with citations", () => { + const answer = `The consumer pulse segmentation is a two-level segmentation solution developed by Sales Factory based on the demographics, life stage, income, and behaviors of US consumers. It consists of five primary segments and fifteen secondary segments, resulting in a total of 20 segments. + + The five primary segments are as follows: + + 1. Aspiring Singles: This segment represents 22% of US consumers and consists of younger, more diverse individuals who are mainly singles and renters with lower per capita income. They prioritize experiences over material possessions and are active users of social media for product recommendations. + + 2. Affluent & Educated: This segment represents 9% of US consumers and is characterized by individuals with high educational attainment and full-time employment. They have a higher proportion of males and are predominantly Millennials. They are more likely to reside in urban areas and the Western region of the US. + + 3. Stable Strategists: This segment represents 25% of US consumers and is mostly composed of Gen X individuals. It has an equal gender distribution and exhibits stable and strategic behaviors. + + 4. Cautious and Accepting: This segment represents 19% of US consumers and is characterized by individuals who are cautious in their decision-making and accepting of new trends and technologies. + + 5. Sunsetting Suburbanites: This segment represents 25% of the US population and consists of individuals who are transitioning into retirement and reside in suburban areas. + + Each primary segment is further divided into three subsegments, resulting in a total of fifteen secondary segments. These subsegments provide a greater ability to target and reach specific consumer groups on digital platforms. + + Understanding the consumer pulse segmentation allows businesses to tailor their marketing strategies and engage with their target audiences more effectively based on their preferences and behaviors. + + Sources: + - [Sales Factory Consumer Pulse Segmentation](https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Segmentation/Consumer%20Pulse%20Segmentation%20v2.docx) + - [Sales Factory Consumer Pulse Segmentation Summary](https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Segmentation/Consumer%20Pulse%20Segmentation%20Summary.docx) + - [Sales Factory Consumer Pulse Segments - Deep Dive](https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Segmentation/Consumer%20Pulse%20Segments%20-%20Deep%20Dive%20v4.docx)`; + + const showSources = true; + const onCitationClicked = vi.fn(); + + // Using React.createElement to avoid JSX transformation issues + const expectedHtml = renderToStaticMarkup( + React.createElement( + "a", + { + key: "1", + className: "supContainer", + title: "Sales Factory Consumer Pulse Segmentation", + onClick: () => + onCitationClicked( + "https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Segmentation/Consumer%20Pulse%20Segmentation%20v2.docx", + "Sales Factory Consumer Pulse Segmentation" + ), + tabIndex: 0 + }, + React.createElement("sup", null, "1") + ) + ); + + const result = parseAnswerToHtml(answer, showSources, onCitationClicked); + + // expect(result.answerHtml).toBe(expectedHtml); + expect(result.citations).toEqual([ + "Sales Factory Consumer Pulse Segmentation", + "Sales Factory Consumer Pulse Segmentation Summary", + "Sales Factory Consumer Pulse Segments - Deep Dive" + ]); + expect(result.followupQuestions).toEqual([]); + + expect(onCitationClicked).not.toHaveBeenCalled(); // Ensure the callback is not called by default + }); +}); From 226759f1e115c48bafa53cb322661dc88ed8a07c Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 1 Aug 2024 14:33:36 -0400 Subject: [PATCH 117/820] HOTFIX install missing dependecy --- frontend/package-lock.json | 1890 ++++++++++++++++- frontend/package.json | 3 +- .../Answer/parseAnswerToHtml.test.tsx | 1 - 3 files changed, 1846 insertions(+), 48 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b8523b19..2417240f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -35,7 +35,8 @@ "@vitejs/plugin-react": "^3.1.0", "prettier": "^2.8.3", "typescript": "^4.9.3", - "vite": "^4.1.0" + "vite": "^4.1.0", + "vitest": "^2.0.5" } }, "node_modules/@ampproject/remapping": { @@ -438,6 +439,22 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -1074,9 +1091,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1461,6 +1478,214 @@ "node": ">=14.0.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.2.tgz", + "integrity": "sha512-OHflWINKtoCFSpm/WmuQaWW4jeX+3Qt3XQDepkkiFTsoxFc5BpF3Z5aDxFZgBqRjO6ATP5+b1iilp4kGIZVWlA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.2.tgz", + "integrity": "sha512-k0OC/b14rNzMLDOE6QMBCjDRm3fQOHAL8Ldc9bxEWvMo4Ty9RY6rWmGetNTWhPo+/+FNd1lsQYRd0/1OSix36A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.2.tgz", + "integrity": "sha512-IIARRgWCNWMTeQH+kr/gFTHJccKzwEaI0YSvtqkEBPj7AshElFq89TyreKNFAGh5frLfDCbodnq+Ye3dqGKPBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.2.tgz", + "integrity": "sha512-52udDMFDv54BTAdnw+KXNF45QCvcJOcYGl3vQkp4vARyrcdI/cXH8VXTEv/8QWfd6Fru8QQuw1b2uNersXOL0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.2.tgz", + "integrity": "sha512-r+SI2t8srMPYZeoa1w0o/AfoVt9akI1ihgazGYPQGRilVAkuzMGiTtexNZkrPkQsyFrvqq/ni8f3zOnHw4hUbA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.2.tgz", + "integrity": "sha512-+tYiL4QVjtI3KliKBGtUU7yhw0GMcJJuB9mLTCEauHEsqfk49gtUBXGtGP3h1LW8MbaTY6rSFIQV1XOBps1gBA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.2.tgz", + "integrity": "sha512-OR5DcvZiYN75mXDNQQxlQPTv4D+uNCUsmSCSY2FolLf9W5I4DSoJyg7z9Ea3TjKfhPSGgMJiey1aWvlWuBzMtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.2.tgz", + "integrity": "sha512-Hw3jSfWdUSauEYFBSFIte6I8m6jOj+3vifLg8EU3lreWulAUpch4JBjDMtlKosrBzkr0kwKgL9iCfjA8L3geoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.2.tgz", + "integrity": "sha512-rhjvoPBhBwVnJRq/+hi2Q3EMiVF538/o9dBuj9TVLclo9DuONqt5xfWSaE6MYiFKpo/lFPJ/iSI72rYWw5Hc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.2.tgz", + "integrity": "sha512-EAz6vjPwHHs2qOCnpQkw4xs14XJq84I81sDRGPEjKPFVPBw7fwvtwhVjcZR6SLydCv8zNK8YGFblKWd/vRmP8g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.2.tgz", + "integrity": "sha512-IJSUX1xb8k/zN9j2I7B5Re6B0NNJDJ1+soezjNojhT8DEVeDNptq2jgycCOpRhyGj0+xBn7Cq+PK7Q+nd2hxLA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.2.tgz", + "integrity": "sha512-OgaToJ8jSxTpgGkZSkwKE+JQGihdcaqnyHEFOSAU45utQ+yLruE1dkonB2SDI8t375wOKgNn8pQvaWY9kPzxDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.2.tgz", + "integrity": "sha512-5V3mPpWkB066XZZBgSd1lwozBk7tmOkKtquyCJ6T4LN3mzKENXyBwWNQn8d0Ci81hvlBw5RoFgleVpL6aScLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.2.tgz", + "integrity": "sha512-ayVstadfLeeXI9zUPiKRVT8qF55hm7hKa+0N1V6Vj+OTNFfKSoUxyZvzVvgtBxqSb5URQ8sK6fhwxr9/MLmxdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.2.tgz", + "integrity": "sha512-Mda7iG4fOLHNsPqjWSjANvNZYoW034yxgrndof0DwCy0D3FvTjeNo+HGE6oGWgvcLZNLlcp0hLEFcRs+UGsMLg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.2.tgz", + "integrity": "sha512-DPi0ubYhSow/00YqmG1jWm3qt1F8aXziHc/UNy8bo9cpCacqhuWu+iSq/fp2SyEQK7iYTZ60fBU9cat3MXTjIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@stripe/react-stripe-js": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.7.3.tgz", @@ -1504,6 +1729,12 @@ "@types/trusted-types": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/mustache": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.5.tgz", @@ -1588,6 +1819,96 @@ "vite": "^4.1.0-beta.0" } }, + "node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -1703,6 +2024,15 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/babel-plugin-styled-components": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", @@ -1812,6 +2142,15 @@ "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==" }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -1859,6 +2198,22 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1872,6 +2227,15 @@ "node": ">=4" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -1959,6 +2323,20 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -1983,9 +2361,9 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -2010,6 +2388,15 @@ "node": ">=8" } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2115,29 +2502,85 @@ "node": ">=0.8.0" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/file-selector": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", - "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">= 10" + "@types/estree": "^1.0.0" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "optional": true, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, "dependencies": { - "minipass": "^3.0.0" + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/file-selector": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.2.4.tgz", + "integrity": "sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" }, "engines": { "node": ">= 8" @@ -2209,6 +2652,27 @@ "node": ">=6.9.0" } }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2284,6 +2748,15 @@ "node": ">= 6.0.0" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -2329,6 +2802,12 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2406,6 +2885,15 @@ "underscore": "^1.13.1" } }, + "node_modules/loupe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2496,6 +2984,12 @@ } } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/microsoft-cognitiveservices-speech-sdk": { "version": "1.36.0", "resolved": "https://registry.npmjs.org/microsoft-cognitiveservices-speech-sdk/-/microsoft-cognitiveservices-speech-sdk-1.36.0.tgz", @@ -2509,6 +3003,18 @@ "ws": "^7.5.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", @@ -2661,6 +3167,33 @@ "node": ">=6" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -2690,6 +3223,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/option": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", @@ -2713,6 +3261,15 @@ "node": ">=0.10.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/path2d": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", @@ -2732,6 +3289,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/pdfjs-dist": { "version": "3.11.174", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.11.174.tgz", @@ -2746,9 +3318,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3143,6 +3715,33 @@ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3216,6 +3815,18 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -3250,6 +3861,18 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/styled-components": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", @@ -3341,6 +3964,39 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, + "node_modules/tinybench": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -3515,28 +4171,1170 @@ } } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "node_modules/vite-node": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "dev": true, "dependencies": { - "loose-envify": "^1.0.0" + "cac": "^6.7.14", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "optional": true + "node_modules/vite-node/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "node_modules/vite-node/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, "optional": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite-node/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vite-node/node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vite-node/node_modules/rollup": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.2.tgz", + "integrity": "sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.19.2", + "@rollup/rollup-android-arm64": "4.19.2", + "@rollup/rollup-darwin-arm64": "4.19.2", + "@rollup/rollup-darwin-x64": "4.19.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.2", + "@rollup/rollup-linux-arm-musleabihf": "4.19.2", + "@rollup/rollup-linux-arm64-gnu": "4.19.2", + "@rollup/rollup-linux-arm64-musl": "4.19.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.2", + "@rollup/rollup-linux-riscv64-gnu": "4.19.2", + "@rollup/rollup-linux-s390x-gnu": "4.19.2", + "@rollup/rollup-linux-x64-gnu": "4.19.2", + "@rollup/rollup-linux-x64-musl": "4.19.2", + "@rollup/rollup-win32-arm64-msvc": "4.19.2", + "@rollup/rollup-win32-ia32-msvc": "4.19.2", + "@rollup/rollup-win32-x64-msvc": "4.19.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", + "execa": "^8.0.1", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vitest/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/vitest/node_modules/postcss": { + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest/node_modules/rollup": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.2.tgz", + "integrity": "sha512-6/jgnN1svF9PjNYJ4ya3l+cqutg49vOZ4rVgsDKxdl+5gpGPnByFXWGyfH9YGx9i3nfBwSu1Iyu6vGwFFA0BdQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.19.2", + "@rollup/rollup-android-arm64": "4.19.2", + "@rollup/rollup-darwin-arm64": "4.19.2", + "@rollup/rollup-darwin-x64": "4.19.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.2", + "@rollup/rollup-linux-arm-musleabihf": "4.19.2", + "@rollup/rollup-linux-arm64-gnu": "4.19.2", + "@rollup/rollup-linux-arm64-musl": "4.19.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.2", + "@rollup/rollup-linux-riscv64-gnu": "4.19.2", + "@rollup/rollup-linux-s390x-gnu": "4.19.2", + "@rollup/rollup-linux-x64-gnu": "4.19.2", + "@rollup/rollup-linux-x64-musl": "4.19.2", + "@rollup/rollup-win32-arm64-msvc": "4.19.2", + "@rollup/rollup-win32-ia32-msvc": "4.19.2", + "@rollup/rollup-win32-x64-msvc": "4.19.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, "node_modules/wide-align": { diff --git a/frontend/package.json b/frontend/package.json index 80a08244..a85bf2b7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "@vitejs/plugin-react": "^3.1.0", "prettier": "^2.8.3", "typescript": "^4.9.3", - "vite": "^4.1.0" + "vite": "^4.1.0", + "vitest": "^2.0.5" } } diff --git a/frontend/src/components/Answer/parseAnswerToHtml.test.tsx b/frontend/src/components/Answer/parseAnswerToHtml.test.tsx index 0f0e981f..89f4d511 100644 --- a/frontend/src/components/Answer/parseAnswerToHtml.test.tsx +++ b/frontend/src/components/Answer/parseAnswerToHtml.test.tsx @@ -1,6 +1,5 @@ import { describe, it, expect, vi } from "vitest"; import React from "react"; -import { render, fireEvent } from "@testing-library/react"; import { renderToStaticMarkup } from "react-dom/server"; import { parseAnswerToHtml } from "./AnswerParser"; // Adjust path as needed import { getCitationFilePath } from "../../api"; // Adjust path as needed From 35582f62cdd50ffac8d67536c346d1db39ffee44 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Fri, 2 Aug 2024 17:47:16 -0400 Subject: [PATCH 118/820] SFC-337-338-setup-stripe-webhook --- backend/app.py | 96 ++++++++++++++++++- backend/requirements.txt | 3 +- frontend/src/api/api.ts | 22 +++++ .../PaymentGateway/PaymentGateway.tsx | 70 +++++++------- 4 files changed, 155 insertions(+), 36 deletions(-) diff --git a/backend/app.py b/backend/app.py index 3100d8c2..e040dafe 100644 --- a/backend/app.py +++ b/backend/app.py @@ -5,7 +5,8 @@ import logging import requests import json -from flask import Flask, request, jsonify, Response +import stripe +from flask import Flask, request, jsonify, Response, redirect from flask_cors import CORS from dotenv import load_dotenv from azure.keyvault.secrets import SecretClient @@ -35,6 +36,9 @@ EMAIL_USER = os.getenv("EMAIL_USER") EMAIL_PORT = os.getenv("EMAIL_PORT") +# stripe +stripe.api_key = os.getenv("STRIPE_API_KEY") + INVITATION_LINK = os.getenv("INVITATION_LINK") LOGLEVEL = os.environ.get("LOGLEVEL", "INFO").upper() @@ -236,6 +240,29 @@ def getStorageAccount(): return jsonify({"error": str(e)}), 500 +@app.route("/create-checkout-session", methods=["POST"]) +def create_checkout_session(): + price = request.json["priceId"] + userId = request.json["userId"] + success_url = request.json["successUrl"] + cancel_url = request.json["cancelUrl"] + try: + checkout_session = stripe.checkout.Session.create( + line_items=[ + {"price": price, "quantity": 1}, + ], + mode="subscription", + client_reference_id=userId, + metadata={"userId": userId}, + success_url=success_url, + cancel_url=cancel_url, + automatic_tax={"enabled": True}, + ) + except Exception as e: + return str(e) + + return jsonify({"url": checkout_session.url}) + @app.route("/api/stripe", methods=["GET"]) def getStripe(): try: @@ -246,6 +273,73 @@ def getStripe(): logging.exception("[webbackend] exception in /api/stripe") return jsonify({"error": str(e)}), 500 +@app.route("/webhook", methods=["POST"]) +def webhook(): + stripe.api_key = os.getenv("STRIPE_API_KEY") + endpoint_secret = os.getenv('STRIPE_SIGNING_SECRET') + + event = None + payload = request.data + + try: + event = json.loads(payload) + except json.decoder.JSONDecodeError as e: + print("⚠️ Webhook error while parsing basic request." + str(e)) + return jsonify(success=False) + if endpoint_secret: + # Only verify the event if there is an endpoint secret defined + # Otherwise use the basic event deserialized with json + sig_header = request.headers["STRIPE_SIGNATURE"] + try: + event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret) + except stripe.error.SignatureVerificationError as e: + print("⚠️ Webhook signature verification failed. " + str(e)) + return jsonify(success=False) + + # Handle the event + print("🔔 Webhook received!", event["type"]) + if event["type"] == "checkout.session.completed": + userId = event["data"]["object"]["client_reference_id"] + sessionId = event["data"]["object"]["id"] + subscriptionId = event["data"]["object"]["subscription"] + paymentStatus = event["data"]["object"]["payment_status"] + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = "orchestrator-host--checkuser" + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--checkuser") + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + try: + url = CHECK_USER_ENDPOINT + payload = json.dumps( + { + "id": userId, + "sessionId": sessionId, + "subscriptionId": subscriptionId, + "paymentStatus": paymentStatus, + } + ) + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + response = requests.request("PATCH", url, headers=headers, data=payload) + logging.info(f"[webbackend] RESPONSE: {response.text[:500]}...") + except Exception as e: + logging.exception("[webbackend] exception in /api/checkUser") + return jsonify({"error": str(e)}), 500 + else: + # Unexpected event type + print("Unexpected event type") + + return jsonify(success=True) + @app.route("/api/upload-blob", methods=["POST"]) def uploadBlob(): diff --git a/backend/requirements.txt b/backend/requirements.txt index 04a616c3..4f4e9c74 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,4 +6,5 @@ requests==2.28.2 python-dotenv==1.0.0 azure-identity azure-keyvault-secrets -azure-storage-blob==12.19.0 \ No newline at end of file +azure-storage-blob==12.19.0 +stripe==10.5.0 \ No newline at end of file diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index afa06dba..3d1dba6b 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -341,3 +341,25 @@ export async function uploadFile(file: any) { throw error; } } + +export async function createCheckoutSession({ userId, priceId, successUrl, cancelUrl }: any) { + const response = await fetch("/create-checkout-session", { + method: "POST", + headers: { + "Content-Type": "application/json", + + }, + body: JSON.stringify({ + userId, + priceId, + successUrl, + cancelUrl + }) + }); + if (response.status > 299 || !response.ok) { + throw Error("Error creating checkout session"); + } + + const session = await response.json(); + return session; +} diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index eff29b04..bca0b7e3 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -1,8 +1,9 @@ -import React, { useState, useEffect } from 'react'; -import { loadStripe, Stripe } from '@stripe/stripe-js'; -import { Elements } from '@stripe/react-stripe-js'; +import React, { useState, useEffect, useContext } from "react"; +import { loadStripe, Stripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; import styles from "./PaymentGateway.module.css"; -import { getApiKeyPayment } from "../../api"; +import { getApiKeyPayment, createCheckoutSession } from "../../api"; +import { AppContext } from "../../providers/AppProviders"; import { Spinner } from "@fluentui/react"; const fetchApiKey = async () => { @@ -11,39 +12,38 @@ const fetchApiKey = async () => { }; export const SubscriptionPlans: React.FC<{ stripePromise: Promise }> = ({ stripePromise }) => { + const { user } = useContext(AppContext); + const [plans, setPlans] = useState([]); useEffect(() => { setPlans([ { - id: 'free_plan', - name: 'Current Plan', - description: 'Access to basic features for free.', - price: '0.00', - interval: 'month' + id: "free_plan", + name: "Current Plan", + description: "Access to basic features for free.", + price: "0.00", + interval: "month" }, { - id: 'price_1PYvHVEpF6ccgZLwn6uq6d4J', - name: 'Enterprise Plan', - description: 'Access to all features including premium support.', - price: '30.00', - interval: 'month' + id: "price_1PYvHVEpF6ccgZLwn6uq6d4J", + name: "Enterprise Plan", + description: "Access to all features including premium support.", + price: "30.00", + interval: "month" } ]); }, []); const handleCheckout = async (priceId: string) => { - const stripe = await stripePromise; - const { error } = await stripe!.redirectToCheckout({ - lineItems: [{ price: priceId, quantity: 1 }], - mode: 'subscription', - successUrl: window.location.origin + '/success-payment', - cancelUrl: window.location.origin + '/', + const { url } = await createCheckoutSession({ + userId: user.id, + priceId, + successUrl: window.location.origin + "#/success-payment", + cancelUrl: window.location.origin + "/" }); - - if (error) { - console.error('Error redirecting to checkout:', error); - } + console.log(url); + window.location.href = url }; return ( @@ -53,14 +53,16 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise {plans.map(plan => (

{plan.name}

-

${plan.price} per {plan.interval}

+

+ ${plan.price} per {plan.interval} +

{plan.description}

- {plan.id !== 'free_plan' && ( - @@ -88,11 +90,11 @@ export const PaymentGateway: React.FC = () => { if (!stripePromise) { return ( -
+
Loading...
-
- ); +
+ ); } return ( From 734ef87cfd3d112f88fa465fd452be84c57c4852 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 5 Aug 2024 15:43:43 -0400 Subject: [PATCH 119/820] SFC-331-339-stripe-ui-validations --- .../PaymentGateway/ButtonPaymentGateway.tsx | 22 +++++---- .../PaymentGateway/PaymentGateway.module.css | 11 +++++ .../PaymentGateway/PaymentGateway.tsx | 46 +++++++++++-------- .../QuestionInput/QuestionInput.tsx | 22 ++++----- frontend/src/pages/chat/Chat.tsx | 25 ++++++---- frontend/src/providers/AppProviders.tsx | 8 +++- 6 files changed, 85 insertions(+), 49 deletions(-) diff --git a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx index 9fe9dc6a..ae77c5e7 100644 --- a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx @@ -1,19 +1,25 @@ +import { useContext } from "react"; import styles from "./ButtonPaymentGateway.module.css"; import { Text } from "@fluentui/react"; -import { GuestFilled } from "@fluentui/react-icons"; - +import { GuestFilled } from "@fluentui/react-icons"; +import { AppContext } from "../../providers/AppProviders"; export const ButtonPaymentGateway = () => { + const { user } = useContext(AppContext); const handleRedirect = () => { window.location.href = "#/payment"; }; - + return ( - + <> + {user.subscriptionStatus === "inactive" && ( + + )} + ); -}; \ No newline at end of file +}; diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.module.css b/frontend/src/components/PaymentGateway/PaymentGateway.module.css index 1af27275..ecded0f7 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.module.css +++ b/frontend/src/components/PaymentGateway/PaymentGateway.module.css @@ -33,6 +33,7 @@ display: flex; flex-direction: column; justify-content: space-between; + position: relative; } .planName { @@ -63,6 +64,16 @@ flex-grow: 1; } +.currentIndicator{ + color: #6772e5; + font-family: 'Poppins', sans-serif; + position: absolute; + width: 100%; + text-align: center; + top: 0px; + left: 0px; +} + .planButton { background-color: #6772e5; color: white; diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index bca0b7e3..a878e659 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -15,12 +15,13 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise const { user } = useContext(AppContext); const [plans, setPlans] = useState([]); + const [currentPlan, setCurrentPlan] = useState(user.subscriptionId ? 1 : 0); useEffect(() => { setPlans([ { id: "free_plan", - name: "Current Plan", + name: "Free Plan", description: "Access to basic features for free.", price: "0.00", interval: "month" @@ -43,31 +44,38 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise cancelUrl: window.location.origin + "/" }); console.log(url); - window.location.href = url + window.location.href = url; }; return (

Subscription Plans

- {plans.map(plan => ( -
-

{plan.name}

-

- ${plan.price} per {plan.interval} -

-

{plan.description}

- {plan.id !== "free_plan" && ( - - )} -
+ onClick={() => handleCheckout(plan.id)} + role="button" + aria-label={`Subscribe to ${plan.name}`} + > + {user.subscriptionId && user.subscriptionStatus === "inactive" + ? "Reactivate subscription" + : user.subscriptionStatus === "active" + ? "Edit payment information" + : "Subscribe"} + + )} +
+ ))}
diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 992048b0..856dc105 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useState, useContext } from "react"; +import { AppContext } from "../../providers/AppProviders"; import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; import { getTokenOrRefresh } from "./token_util"; import { Send32Filled, Attach32Filled, SlideMicrophone32Filled } from "@fluentui/react-icons"; @@ -16,7 +17,7 @@ interface Props { import { useFilePicker } from "use-file-picker"; -export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: string) => void}) => { +export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: string) => void }) => { const [files, setFiles] = useState([]); const [loadingFiles, setLoadingFiles] = useState(false); const [error, setError] = useState(""); @@ -28,7 +29,7 @@ export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: str onFilesSelected: async ({ plainFiles, filesContent, errors }) => { // this callback is always called, even if there are errors setLoadingFiles(true); - try{ + try { const data = await uploadFile(plainFiles[0]); setLoadingFiles(false); setError(""); @@ -141,12 +142,7 @@ export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: str > × - +
{file.name}
))} @@ -156,7 +152,7 @@ export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: str style={{ display: "flex", flexDirection: "row", - alignItems: "center", + alignItems: "center" }} > Loading file... @@ -173,11 +169,13 @@ export const FileAttachmentInput = ({setFileBlobUrl}: {setFileBlobUrl: (url: str }; export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { + const { user } = useContext(AppContext); + const [question, setQuestion] = useState(""); const [fileBlobUrl, setFileBlobUrl] = useState(null); const sendQuestion = () => { - if (disabled || !question.trim()) { + if (disabled || !question.trim() || user.subscriptionStatus === "inactive") { return; } @@ -237,7 +235,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr } }; - const sendQuestionDisabled = disabled || !question.trim(); + const sendQuestionDisabled = disabled || !question.trim() || user.subscriptionStatus === "inactive"; return ( diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 3bd1cf80..e19ebc45 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -251,7 +251,7 @@ const Chat = () => { const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; if (id && name) { - setUser({ id, name, email, role: undefined }); + setUser({ id, name, email, role: undefined, subscriptionStatus: "inactive" }); } // register user if doesn't exist @@ -261,9 +261,10 @@ const Chat = () => { // verifies if user exists and assigns the role const result = await checkUser({ user: { id, name, email } }); const role = result["role"] || undefined; + const subscriptionStatus = result["subscriptionStatus"] || undefined; if (result && role) { - setUser({ id, name, email, role }); + setUser({ id, name, email, role, subscriptionStatus }); } } } @@ -387,14 +388,13 @@ const Chat = () => { }; const handleKeyDown = (event: KeyboardEvent) => { - - const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isCtrlOrCmd = event.ctrlKey || event.metaKey; const isAlt = event.altKey; - if (event.code === 'KeyO' && isCtrlOrCmd && isAlt) { + if (event.code === "KeyO" && isCtrlOrCmd && isAlt) { event.preventDefault(); clearChat(); - } else if (event.code === 'KeyY' && isCtrlOrCmd && isAlt) { + } else if (event.code === "KeyY" && isCtrlOrCmd && isAlt) { event.preventDefault(); handleNewChat(); } @@ -499,7 +499,9 @@ const Chat = () => {
makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current)} + onRetry={() => + makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) + } />
@@ -593,7 +595,12 @@ const Chat = () => { onChange={onRetrieveCountChange} aria-label="Number of documents to retrieve" /> - + { label="Use query-contextual summaries instead of whole documents" onChange={onUseSemanticCaptionsChange} disabled={!useSemanticRanker} - aria-label="Use query-contextual summaries" + aria-label="Use query-contextual summaries" /> ({ name: "anonymous", email: "anonymous@gmail.com", role: undefined, + subscriptionStatus: "inactive", + subscriptionId: undefined }, setUser: () => {}, dataConversation: [], @@ -83,7 +87,9 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => id: "00000000-0000-0000-0000-000000000000", name: "anonymous", email: "anonymous@gmail.com", - role: undefined + role: undefined, + subscriptionStatus: "inactive", + subscriptionId: undefined }); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); From c88fd2e837672102686233c8a6c83f1bc2618e65 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 7 Aug 2024 16:41:56 -0400 Subject: [PATCH 120/820] SFC-332-add-expiration-finish-organizations --- backend/app.py | 72 ++++++++++++++++--- frontend/src/api/api.ts | 24 ++++++- .../PaymentGateway/ButtonPaymentGateway.tsx | 4 +- .../PaymentGateway/PaymentGateway.tsx | 12 ++-- .../QuestionInput/QuestionInput.tsx | 6 +- frontend/src/pages/chat/Chat.tsx | 36 ++++++++-- frontend/src/providers/AppProviders.tsx | 63 ++++++++++------ 7 files changed, 166 insertions(+), 51 deletions(-) diff --git a/backend/app.py b/backend/app.py index e040dafe..4bc6bf83 100644 --- a/backend/app.py +++ b/backend/app.py @@ -28,6 +28,7 @@ FEEDBACK_ENDPOINT = ORCHESTRATOR_URI + "/feedback" HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/conversations" CHECK_USER_ENDPOINT = ORCHESTRATOR_URI + "/checkUser" +SUBSCRIPTION_ENDPOINT = ORCHESTRATOR_URI + "/subscriptions" STORAGE_ACCOUNT = os.getenv("STORAGE_ACCOUNT") # email @@ -246,6 +247,7 @@ def create_checkout_session(): userId = request.json["userId"] success_url = request.json["successUrl"] cancel_url = request.json["cancelUrl"] + organizationId = request.json["organizationId"] try: checkout_session = stripe.checkout.Session.create( line_items=[ @@ -253,14 +255,23 @@ def create_checkout_session(): ], mode="subscription", client_reference_id=userId, - metadata={"userId": userId}, + metadata={"userId": userId, + "organizationId": organizationId}, success_url=success_url, cancel_url=cancel_url, automatic_tax={"enabled": True}, + custom_fields=[ + { + "key": "organization_name", + "label": {"type": "custom", "custom": "Organization Name"}, + "type": "text", + "text": {"minimum_length": 5, "maximum_length": 100}, + }, + ], ) except Exception as e: return str(e) - + return jsonify({"url": checkout_session.url}) @app.route("/api/stripe", methods=["GET"]) @@ -276,7 +287,7 @@ def getStripe(): @app.route("/webhook", methods=["POST"]) def webhook(): stripe.api_key = os.getenv("STRIPE_API_KEY") - endpoint_secret = os.getenv('STRIPE_SIGNING_SECRET') + endpoint_secret = os.getenv("STRIPE_SIGNING_SECRET") event = None payload = request.data @@ -297,19 +308,22 @@ def webhook(): return jsonify(success=False) # Handle the event - print("🔔 Webhook received!", event["type"]) if event["type"] == "checkout.session.completed": + print("🔔 Webhook received!", event["type"]) userId = event["data"]["object"]["client_reference_id"] + organizationId = event["data"]["object"]["metadata"]["organizationId"] sessionId = event["data"]["object"]["id"] subscriptionId = event["data"]["object"]["subscription"] paymentStatus = event["data"]["object"]["payment_status"] + organizationName = event["data"]["object"]["custom_fields"][0]["text"]["value"] + expirationDate = event["data"]["object"]["expires_at"] try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = "orchestrator-host--checkuser" + keySecretName = "orchestrator-host--subscriptions" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--checkuser") + logging.exception("[webbackend] exception in /api/orchestrator-host--subscriptions") return ( jsonify( { @@ -319,17 +333,20 @@ def webhook(): 500, ) try: - url = CHECK_USER_ENDPOINT + url = SUBSCRIPTION_ENDPOINT payload = json.dumps( { "id": userId, + "organizationId": organizationId, "sessionId": sessionId, "subscriptionId": subscriptionId, "paymentStatus": paymentStatus, + "organizationName": organizationName, + "expirationDate": expirationDate } ) headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("PATCH", url, headers=headers, data=payload) + response = requests.request("POST", url, headers=headers, data=payload) logging.info(f"[webbackend] RESPONSE: {response.text[:500]}...") except Exception as e: logging.exception("[webbackend] exception in /api/checkUser") @@ -340,7 +357,6 @@ def webhook(): return jsonify(success=True) - @app.route("/api/upload-blob", methods=["POST"]) def uploadBlob(): if "file" not in request.files: @@ -372,7 +388,6 @@ def uploadBlob(): logging.exception("[webbackend] exception in /api/upload-blob") return jsonify({"error": str(e)}), 500 - @app.route("/api/get-blob", methods=["POST"]) def getBlob(): logging.exception("------------------ENTRA ------------") @@ -393,7 +408,6 @@ def getBlob(): logging.exception(blob_name) return jsonify({"error": str(e)}), 500 - @app.route("/api/settings", methods=["GET"]) def getSettings(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -789,6 +803,42 @@ def checkUser(): return jsonify({"error": str(e)}), 500 +@app.route("/api/get-organization-subscription", methods=["GET"]) +def getOrganization(): + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + if not client_principal_id: + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id" + } + ), + 400, + ) + try: + keySecretName = "orchestrator-host--subscriptions" + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--subscriptions") + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + try: + organizationId = request.args.get("organizationId") + url = SUBSCRIPTION_ENDPOINT + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + response = requests.request("GET", url, headers=headers, params={"organizationId": organizationId}) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text + except Exception as e: + logging.exception("[webbackend] exception in /get-organization") + return jsonify({"error": str(e)}), 500 + @app.route("/api/getUser", methods=["GET"]) def getUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 3d1dba6b..9866ac86 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -342,18 +342,18 @@ export async function uploadFile(file: any) { } } -export async function createCheckoutSession({ userId, priceId, successUrl, cancelUrl }: any) { +export async function createCheckoutSession({ userId, priceId, successUrl, cancelUrl, organizationId }: any) { const response = await fetch("/create-checkout-session", { method: "POST", headers: { "Content-Type": "application/json", - }, body: JSON.stringify({ userId, priceId, successUrl, - cancelUrl + cancelUrl, + organizationId }) }); if (response.status > 299 || !response.ok) { @@ -363,3 +363,21 @@ export async function createCheckoutSession({ userId, priceId, successUrl, cance const session = await response.json(); return session; } + +export async function getOrganizationSubscription({userId, organizationId} : any) { + const response = await fetch("/api/get-organization-subscription?organizationId=" + organizationId, { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId, + }, + }); + + if (response.status > 299 || !response.ok) { + throw Error("Error getting organization subscription"); + } + + const subscription = await response.json(); + return subscription; + +} diff --git a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx index ae77c5e7..47ebb286 100644 --- a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx @@ -6,7 +6,7 @@ import { GuestFilled } from "@fluentui/react-icons"; import { AppContext } from "../../providers/AppProviders"; export const ButtonPaymentGateway = () => { - const { user } = useContext(AppContext); + const { user, organization } = useContext(AppContext); const handleRedirect = () => { window.location.href = "#/payment"; @@ -14,7 +14,7 @@ export const ButtonPaymentGateway = () => { return ( <> - {user.subscriptionStatus === "inactive" && ( + {(organization.owner === user.id || !organization.subscriptionId) && ( diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 856dc105..b9cea16c 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -169,13 +169,13 @@ export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: }; export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { - const { user } = useContext(AppContext); + const { user, organization } = useContext(AppContext); const [question, setQuestion] = useState(""); const [fileBlobUrl, setFileBlobUrl] = useState(null); const sendQuestion = () => { - if (disabled || !question.trim() || user.subscriptionStatus === "inactive") { + if (disabled || !question.trim() || organization.subscriptionStatus === "inactive" || !organization.subscriptionId) { return; } @@ -235,7 +235,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr } }; - const sendQuestionDisabled = disabled || !question.trim() || user.subscriptionStatus === "inactive"; + const sendQuestionDisabled = disabled || !question.trim() || organization.subscriptionStatus === "inactive" || !organization.subscriptionId; return ( diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index e19ebc45..fbc63569 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -4,7 +4,18 @@ import { AddRegular, BroomRegular, SparkleFilled, TabDesktopMultipleBottomRegula import styles from "./Chat.module.css"; -import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, ChatTurn, getUserInfo, checkUser, getUsers } from "../../api"; +import { + chatApiGpt, + Approaches, + AskResponse, + ChatRequest, + ChatRequestGpt, + ChatTurn, + getUserInfo, + checkUser, + getUsers, + getOrganizationSubscription +} from "../../api"; import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; import { QuestionInput } from "../../components/QuestionInput"; import { ExampleList } from "../../components/Example"; @@ -58,7 +69,8 @@ const Chat = () => { setChatIsCleaned, chatIsCleaned, settingsPanel, - setUser + setUser, + setOrganization } = useContext(AppContext); const lastQuestionRef = useRef(""); @@ -251,7 +263,7 @@ const Chat = () => { const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; if (id && name) { - setUser({ id, name, email, role: undefined, subscriptionStatus: "inactive" }); + setUser({ id, name, email, role: undefined, organizationId: undefined }); } // register user if doesn't exist @@ -261,10 +273,20 @@ const Chat = () => { // verifies if user exists and assigns the role const result = await checkUser({ user: { id, name, email } }); const role = result["role"] || undefined; - const subscriptionStatus = result["subscriptionStatus"] || undefined; - - if (result && role) { - setUser({ id, name, email, role, subscriptionStatus }); + const organizationId = result["organizationId"] || undefined; + + const organization = await getOrganizationSubscription(organizationId); + + if (result && role && organization) { + setUser({ id, name, email, role, organizationId }); + setOrganization({ + id: organization.id, + name: organization.name, + owner: organization.owner, + subscriptionId: organization.subscriptionId, + subscriptionStatus: organization.subscriptionStatus, + subscriptionExpirationDate: organization.subscriptionExpirationDate + }); } } } diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 1b0e7216..525d2805 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -12,8 +12,16 @@ interface UserInfo { name: string; email: string | null; role: string | undefined; - subscriptionStatus: string; - subscriptionId?: string; + organizationId?: string; +} + +interface OrganizationInfo { + id: string; + name: string; + owner: string; + subscriptionId: string | undefined; + subscriptionStatus: string | undefined; + subscriptionExpirationDate: number | undefined; } interface AppContextType { @@ -31,6 +39,8 @@ interface AppContextType { setDataHistory: Dispatch>; user: UserInfo; setUser: Dispatch>; + organization: OrganizationInfo; + setOrganization: Dispatch>; chatSelected: string; setChatSelected: Dispatch>; chatId: string; @@ -55,10 +65,18 @@ export const AppContext = createContext({ name: "anonymous", email: "anonymous@gmail.com", role: undefined, - subscriptionStatus: "inactive", - subscriptionId: undefined + organizationId: undefined }, setUser: () => {}, + organization: { + id: "00000000-0000-0000-0000-000000000000", + name: "no-organization", + owner: "no-owner", + subscriptionId: undefined, + subscriptionStatus: undefined, + subscriptionExpirationDate: undefined + }, + setOrganization: () => {}, dataConversation: [], setDataConversation: () => {}, chatId: "", @@ -88,8 +106,15 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => name: "anonymous", email: "anonymous@gmail.com", role: undefined, - subscriptionStatus: "inactive", - subscriptionId: undefined + organizationId: undefined + }); + const [organization, setOrganization] = useState({ + id: "00000000-0000-0000-0000-000000000000", + name: "no-organization", + owner: "no-owner", + subscriptionId: undefined, + subscriptionStatus: undefined, + subscriptionExpirationDate: undefined }); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); @@ -99,36 +124,32 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [newChatDeleted, setNewChatDeleted] = useState(false); const handleKeyDown = (event: KeyboardEvent) => { - - const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isCtrlOrCmd = event.ctrlKey || event.metaKey; const isAlt = event.altKey; - if (event.code === 'Slash' && isCtrlOrCmd && isAlt) { + if (event.code === "Slash" && isCtrlOrCmd && isAlt) { event.preventDefault(); setShowFeedbackRatingPanel(!showFeedbackRatingPanel); setSettingsPanel(false); setShowHistoryPanel(false); - } else if (event.code === 'Period' && isCtrlOrCmd && isAlt) { - event.preventDefault() + } else if (event.code === "Period" && isCtrlOrCmd && isAlt) { + event.preventDefault(); setShowHistoryPanel(!showHistoryPanel); setShowFeedbackRatingPanel(false); setSettingsPanel(false); - } else if (event.code === 'Comma' && isCtrlOrCmd && isAlt) { - event.preventDefault() + } else if (event.code === "Comma" && isCtrlOrCmd && isAlt) { + event.preventDefault(); setSettingsPanel(!settingsPanel); setShowHistoryPanel(false); setShowFeedbackRatingPanel(false); - } - else if(event.code === 'Digit0' && isCtrlOrCmd && isAlt){ - event.preventDefault() + } else if (event.code === "Digit0" && isCtrlOrCmd && isAlt) { + event.preventDefault(); window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; - } - else if(event.code === 'Digit9' && isCtrlOrCmd && isAlt){ - event.preventDefault() + } else if (event.code === "Digit9" && isCtrlOrCmd && isAlt) { + event.preventDefault(); window.location.href = "#/admin"; } }; - window.addEventListener("keydown", handleKeyDown); //If I add this on a useEffect it doesn't work, I don't know why @@ -147,6 +168,8 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => setDataHistory, user, setUser, + organization, + setOrganization, dataConversation, setDataConversation, chatId, From 9889a0c9d9b4ece72fe1229adfdbe1ebbf675854 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 7 Aug 2024 17:18:19 -0400 Subject: [PATCH 121/820] SFC-346-markdown-to-answers --- frontend/src/components/Answer/Answer.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Answer/Answer.tsx b/frontend/src/components/Answer/Answer.tsx index 7f767813..1544d9a7 100644 --- a/frontend/src/components/Answer/Answer.tsx +++ b/frontend/src/components/Answer/Answer.tsx @@ -1,6 +1,8 @@ import { useMemo } from "react"; import { Stack, IconButton } from "@fluentui/react"; import DOMPurify from "dompurify"; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm' import styles from "./Answer.module.css"; @@ -71,7 +73,10 @@ export const Answer = ({ -
+
+ + {sanitizedAnswerHtml} +
{!!parsedAnswer.citations.length && showSources && ( From f4ed17f639b803a74812cbfc526534a9210fcad2 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 7 Aug 2024 17:19:14 -0400 Subject: [PATCH 122/820] add packages --- frontend/package-lock.json | 1533 ++++++++++++++++++++++++++++++++++-- frontend/package.json | 2 + 2 files changed, 1453 insertions(+), 82 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2417240f..39eec0cf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,8 +23,10 @@ "react": "^18.2.0", "react-doc-viewer": "^0.1.5", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-router-dom": "^6.8.1", "react-toastify": "^10.0.5", + "remark-gfm": "^4.0.0", "universal-cookie": "^4.0.4", "use-file-picker": "^2.1.2" }, @@ -1720,6 +1722,14 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==" }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/dompurify": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", @@ -1732,8 +1742,36 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" }, "node_modules/@types/mustache": { "version": "4.2.5", @@ -1795,11 +1833,21 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, + "node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/@types/webrtc": { "version": "0.0.37", "resolved": "https://registry.npmjs.org/@types/webrtc/-/webrtc-0.0.37.tgz", "integrity": "sha512-JGAJC/ZZDhcrrmepU4sPLQLIOIAgs5oIK+Ieq90K8fdaNMhfdfqmYatJdgif1NDQtvrSlTOGJDUYHIDunuufOg==" }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, "node_modules/@vitejs/plugin-react": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", @@ -2048,6 +2096,15 @@ "styled-components": ">= 2" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2198,6 +2255,15 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", @@ -2227,6 +2293,42 @@ "node": ">=4" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2275,6 +2377,15 @@ "color-support": "bin.js" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2376,6 +2487,18 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", @@ -2420,6 +2543,18 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dingbat-to-unicode": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", @@ -2502,6 +2637,15 @@ "node": ">=0.8.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2558,6 +2702,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2715,6 +2864,44 @@ "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", "optional": true }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", + "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2728,6 +2915,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/html-url-attributes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.0.tgz", + "integrity": "sha512-/sXbVCWayk6GDVg3ctOX6nxaVj7So40FcFAnWlWGNAB1LpYKcV5Cd10APjPjW80O7zYW2MsjBV4zZ7IZO5fVow==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/https-proxy-agent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", @@ -2777,6 +2973,42 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/inline-style-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz", + "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2786,6 +3018,26 @@ "node": ">=8" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2864,6 +3116,15 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2894,101 +3155,911 @@ "get-func-name": "^2.0.1" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-cancellable-promise": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", + "funding": { + "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-event-props": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, + "node_modules/mammoth": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", + "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.1", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", + "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-remove-position": "^5.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge-refs": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", + "integrity": "sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==", + "funding": { + "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "yallist": "^3.0.2" + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-cancellable-promise": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", - "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", - "funding": { - "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" + "micromark-util-types": "^2.0.0" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "optional": true, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-event-props": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", - "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", - "funding": { - "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/mammoth": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.7.0.tgz", - "integrity": "sha512-ptFhft61dqieLffpdpHD7PUS0cX9YvHQIO3n3ejRhj1bi5Na+RL5wovtNHHXAK6Oj554XfGrVcyTuxgegN6umw==", + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "dependencies": { - "@xmldom/xmldom": "^0.8.6", - "argparse": "~1.0.3", - "base64-js": "^1.5.1", - "bluebird": "~3.4.0", - "dingbat-to-unicode": "^1.0.1", - "jszip": "^3.7.1", - "lop": "^0.4.1", - "path-is-absolute": "^1.0.0", - "underscore": "^1.13.1", - "xmlbuilder": "^10.0.0" - }, - "bin": { - "mammoth": "bin/mammoth" - }, - "engines": { - "node": ">=12.0.0" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/merge-refs": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", - "integrity": "sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==", - "funding": { - "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" } - } + ] }, - "node_modules/merge-stream": { + "node_modules/micromark-util-types": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, "node_modules/microsoft-cognitiveservices-speech-sdk": { "version": "1.36.0", @@ -3253,6 +4324,30 @@ "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3400,6 +4495,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3521,6 +4625,31 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "peer": true }, + "node_modules/react-markdown": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", + "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-pdf": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.0.tgz", @@ -3631,6 +4760,68 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3810,6 +5001,15 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -3849,6 +5049,19 @@ "node": ">=8" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3873,6 +5086,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.6.tgz", + "integrity": "sha512-khxq+Qm3xEyZfKd/y9L3oIWQimxuc4STrQKtQn8aSDRHb8mFgpukgX1hdzfrMEW6JCjyJ8p89x+IUMVnCBI1PA==", + "dependencies": { + "inline-style-parser": "0.2.3" + } + }, "node_modules/styled-components": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", @@ -4011,6 +5232,24 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "optional": true }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -4039,6 +5278,100 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universal-cookie": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-4.0.4.tgz", @@ -4116,6 +5449,33 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vfile": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", + "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", @@ -5389,6 +6749,15 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/frontend/package.json b/frontend/package.json index a85bf2b7..525f0ac4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,8 +24,10 @@ "react": "^18.2.0", "react-doc-viewer": "^0.1.5", "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", "react-router-dom": "^6.8.1", "react-toastify": "^10.0.5", + "remark-gfm": "^4.0.0", "universal-cookie": "^4.0.4", "use-file-picker": "^2.1.2" }, From 35fd750528dafaaa8d3de8e26ff25613e0604e07 Mon Sep 17 00:00:00 2001 From: Fabiola Rueda <30278070+fabiolayerim@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:58:12 -0400 Subject: [PATCH 123/820] SFC-143 add subscription shortcut (#102) & updated feedback shortcut --- frontend/src/providers/AppProviders.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 525d2805..54d21256 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -127,7 +127,7 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const isCtrlOrCmd = event.ctrlKey || event.metaKey; const isAlt = event.altKey; - if (event.code === "Slash" && isCtrlOrCmd && isAlt) { + if (event.code === "Digit8" && isCtrlOrCmd && isAlt) { event.preventDefault(); setShowFeedbackRatingPanel(!showFeedbackRatingPanel); setSettingsPanel(false); @@ -148,6 +148,9 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => } else if (event.code === "Digit9" && isCtrlOrCmd && isAlt) { event.preventDefault(); window.location.href = "#/admin"; + } else if (event.code === "Digit7" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + window.location.href = "#/payment"; } }; From 4037de0fd2853ea5aeb55e5ab189a69d7d058342 Mon Sep 17 00:00:00 2001 From: Fabiola Rueda <30278070+fabiolayerim@users.noreply.github.com> Date: Thu, 15 Aug 2024 18:45:49 -0400 Subject: [PATCH 124/820] SFC-352 add admin validation & redirect (#103) & deleted free plan --- .../components/PaymentGateway/ButtonPaymentGateway.tsx | 6 ++++-- .../src/components/PaymentGateway/PaymentGateway.tsx | 10 +--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx index 47ebb286..b4253b5b 100644 --- a/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/ButtonPaymentGateway.tsx @@ -9,12 +9,14 @@ export const ButtonPaymentGateway = () => { const { user, organization } = useContext(AppContext); const handleRedirect = () => { - window.location.href = "#/payment"; + if (organization.subscriptionId) { + window.location.href = "https://dashboard.stripe.com/dashboard"; + } else window.location.href = "#/payment"; }; return ( <> - {(organization.owner === user.id || !organization.subscriptionId) && ( + {user.role === "admin" && ( ); diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css index 0c43844f..b8cc499b 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.module.css +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -166,13 +166,6 @@ color: #ff3333; } -.profileButtonContainer { - position: absolute; - bottom: 30px; - right: 20px; - width: 240px; - height: 80px; -} .thumbButton { width: 60px; height: 60px; diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index 8d5d515e..a6b21644 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -5,7 +5,6 @@ import { AppContext } from "../../providers/AppProviders"; import { AddFilled, SaveFilled, ThumbLikeFilled, ThumbDislikeFilled } from "@fluentui/react-icons"; import { ThumbLikeRegular, ThumbDislikeRegular } from "@fluentui/react-icons"; import { postFeedbackRating } from "../../api/api"; -import { ProfileButton } from "../Profile"; const categoryOptions = [ { key: "1", text: "Incorrect data" }, @@ -144,9 +143,6 @@ export const FeedbackRating = () => { {errorMessage !== null &&

{errorMessage}

}
-
- -
); }; diff --git a/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx b/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx index b9cc5c14..26e292c3 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRatingButton.tsx @@ -1,5 +1,5 @@ import { Text } from "@fluentui/react"; -import { ChatBubblesQuestionFilled } from "@fluentui/react-icons"; +import { ChatBubblesQuestionRegular } from "@fluentui/react-icons"; import styles from "./FeedbackRatingButton.module.css"; import { AppContext } from "../../providers/AppProviders"; @@ -16,7 +16,7 @@ export const FeedbackRatingButton = ({ className, disabled, onClick }: Props) => const buttonContent = showFeedbackRatingPanel ? "Hide feedback panel" : "Show feedback panel"; return ( ); diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index a61cbb74..eae11e4f 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -3,7 +3,6 @@ import { AddFilled } from "@fluentui/react-icons"; import styles from "./ChatHistoryPannel.module.css"; import { AppContext } from "../../providers/AppProviders"; import { ChatHistoryPanelList } from "./ChatHistoryListItem"; -import { ProfileButton } from "../Profile"; interface ChatHistoryPanelProps { functionDeleteChat: () => void; @@ -32,9 +31,6 @@ export const ChatHistoryPanel: React.FC = ({ functionDele
-
- -
); }; diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 4d8f035c..7824fc8d 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -147,14 +147,6 @@ } } -.profileButtonContainer { - position: absolute; - bottom: 30px; - right: 20px; - width: 240px; - height: 80px; -} - .listContainer { margin-bottom: 80px; } \ No newline at end of file diff --git a/frontend/src/components/Profile/Profile.module.css b/frontend/src/components/Profile/Profile.module.css index ccee1559..301170a2 100644 --- a/frontend/src/components/Profile/Profile.module.css +++ b/frontend/src/components/Profile/Profile.module.css @@ -44,42 +44,60 @@ } .contactImage { - height: 60px; - width: 60px; + height: 50px; + width: 50px; border-radius: 50%; + margin-left: 20px; } .dropdownPlaceholder { - display: flex; + display: grid; + grid-template-columns: 2fr 1fr; align-items: center; cursor: pointer; - justify-content: space-between; } .placeholderSeparator { width: 10px; } -.dropdownPlaceholderTitle{ +.dropdownContent { display: flex; - flex: 1; - align-items: center; + flex-direction: column; + text-align: right; + padding: 10px 0px; +} + +.dropdownPlaceholderTitle { cursor: pointer; - font-size: 18px; + font-size: 16px; + text-align: right; + font-weight: bold; + text-transform: capitalize; + color: #0a0b0a; +} + +.dropdownPlaceholderSubtitle { + font-size: 12px; + color: rgba(0, 0, 0, 0.7); } .dropdownTitle { height: 100%; } -.stackDropdown div { +.imgContainer { + display: flex; + align-items: center; + justify-content: center; height: 100%; + width: 100%; } .stackDropdown span { - height: 100%; - background-color: #f2f2f2; - border: 0px; - padding: 0px; - margin: 0px; -} \ No newline at end of file + height: 80%; + background-color: transparent; + border: none; + line-height: normal; + margin-top: -2px; +} diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index f66d072e..9d3b8c86 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -36,9 +36,13 @@ const onRenderCaretDown = (): JSX.Element => { const onRenderPlaceholder = (props: any): JSX.Element => { return (
- -
   
- {props.placeholder} +
+ {props.placeholder} + {props.organizationId} +
+
+ +
); }; @@ -54,12 +58,11 @@ export const ProfileButton: React.FunctionComponent = () => { const { user } = useContext(AppContext); const placeholder = placeholderPrepare(user.name); - + const organizationId = user?.organizationId || "No organization"; const headerTitle = user.name; const options: IDropdownOption[] = [ { key: "Header", text: headerTitle || "Options", itemType: DropdownMenuItemType.Header }, - { key: "Admin", text: "Admin Panel", data: { icon: "SecurityGroup" } }, { key: "Logout", text: "Logout", data: { icon: "SkypeArrow" } } ]; @@ -78,8 +81,6 @@ export const ProfileButton: React.FunctionComponent = () => { if (selOption === "Logout") { window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; - } else if (selOption === "Admin") { - window.location.href = "#/admin"; } }; @@ -88,7 +89,7 @@ export const ProfileButton: React.FunctionComponent = () => { onRenderPlaceholder({ ...props, organizationId })} // onRenderTitle={onRenderTitle} onRenderOption={onRenderOption} onRenderCaretDown={onRenderCaretDown} @@ -98,16 +99,6 @@ export const ProfileButton: React.FunctionComponent = () => { // style={selectedOption ? { display: "none" } : {}} selectedKeys={selectedOption ? [selectedOption] : []} /> -
{user?.organizationId || "No organization"}
); }; diff --git a/frontend/src/components/SettingsButton/SettingsButton.tsx b/frontend/src/components/SettingsButton/SettingsButton.tsx index e0210f21..f4bd8561 100644 --- a/frontend/src/components/SettingsButton/SettingsButton.tsx +++ b/frontend/src/components/SettingsButton/SettingsButton.tsx @@ -1,5 +1,5 @@ import { Text, DefaultButton } from "@fluentui/react"; -import { SettingsFilled } from "@fluentui/react-icons"; +import { SettingsRegular } from "@fluentui/react-icons"; import styles from "./SettingsButton.module.css"; @@ -10,7 +10,7 @@ interface Props { export const SettingsButton = ({ onClick }: Props) => { return ( - + Settings ); diff --git a/frontend/src/components/SettingsPanel/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css index fa92cdeb..734c3c20 100644 --- a/frontend/src/components/SettingsPanel/SettingsModal.module.css +++ b/frontend/src/components/SettingsPanel/SettingsModal.module.css @@ -133,11 +133,3 @@ margin-top: -10px; padding-left: 30px; } - -.profileButtonContainer { - position: absolute; - bottom: 30px; - right: 20px; - width: 240px; - height: 80px; -} \ No newline at end of file diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 64871de2..61ce2e7f 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -6,7 +6,6 @@ import styles from "./SettingsModal.module.css"; import { getSettings, postSettings } from "../../api/api"; import { mergeStyles } from "@fluentui/react/lib/Styling"; import { AppContext } from "../../providers/AppProviders"; -import { ProfileButton } from "../Profile"; import { Dialog, DialogContent, PrimaryButton } from "@fluentui/react"; interface Props { @@ -121,7 +120,7 @@ export const SettingsPanel = () => { postSettings({ user, temperature: parsedTemperature - }).then((data) => { + }).then(data => { setTemperature(data.temperature); setIsDialogOpen(false); setIsLoadingSettings(false); @@ -235,9 +234,6 @@ export const SettingsPanel = () => { )} -
- -
); }; diff --git a/frontend/src/pages/layout/Layout.module.css b/frontend/src/pages/layout/Layout.module.css index fe9a1d2c..0c343241 100644 --- a/frontend/src/pages/layout/Layout.module.css +++ b/frontend/src/pages/layout/Layout.module.css @@ -101,6 +101,18 @@ height: 20px; } -.layoutOptions{ +.layoutOptions { display: flex; + align-items: center; +} + +.profileButtonContainer { + display: flex; + align-items: center; + cursor: pointer; + border-left: 2px solid #b5b3b3; + height: 50px; + width: 240px; + background-color: transparent; + margin-left: 10px; } diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 3ad7df99..f2b69b29 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -12,6 +12,7 @@ import { AppContext } from "../../providers/AppProviders"; import { SettingsButton } from "../../components/SettingsButton"; import { ButtonPaymentGateway } from "../../components/PaymentGateway/ButtonPaymentGateway"; import { SideMenu } from "../../components/SideMenu/SideMenu"; +import { ProfileButton } from "../../components/Profile"; const Layout = () => { const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = @@ -46,20 +47,18 @@ const Layout = () => {
- - Sales Factory logo -

-
{pathname === "/" && ( <> - )} +
+ +
From 82694e88c1adb9b1c6e382194601735a2c7cc898 Mon Sep 17 00:00:00 2001 From: Fabiola Rueda <30278070+fabiolayerim@users.noreply.github.com> Date: Thu, 22 Aug 2024 19:15:45 -0400 Subject: [PATCH 131/820] SFC-367-add email (#110) --- frontend/src/components/Profile/index.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index 9d3b8c86..3ca79684 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -38,10 +38,10 @@ const onRenderPlaceholder = (props: any): JSX.Element => {
{props.placeholder} - {props.organizationId} + {props.email}
- + Profile
); @@ -57,8 +57,9 @@ const placeholderPrepare = (placeholder: string) => { export const ProfileButton: React.FunctionComponent = () => { const { user } = useContext(AppContext); + console.log(user); const placeholder = placeholderPrepare(user.name); - const organizationId = user?.organizationId || "No organization"; + const email = user?.email || " "; const headerTitle = user.name; const options: IDropdownOption[] = [ @@ -89,7 +90,7 @@ export const ProfileButton: React.FunctionComponent = () => { onRenderPlaceholder({ ...props, organizationId })} + onRenderPlaceholder={props => onRenderPlaceholder({ ...props, email })} // onRenderTitle={onRenderTitle} onRenderOption={onRenderOption} onRenderCaretDown={onRenderCaretDown} From aafb8968bf61de920932b9a824a9995368698a7c Mon Sep 17 00:00:00 2001 From: Fabiola Rueda <30278070+fabiolayerim@users.noreply.github.com> Date: Fri, 23 Aug 2024 09:37:27 -0400 Subject: [PATCH 132/820] SFC-368 update chat history & settings panel styles (#111) * SFC-368 update chat history styles * SFC-368 expand options * SFC-368 update settings panel styles --- .../HistoryPannel/ChatHistoryListItem.tsx | 147 ++++++++++-------- .../HistoryPannel/ChatHistoryPanel.tsx | 2 +- .../ChatHistoryPannel.module.css | 87 +++++++---- .../SettingsPanel/SettingsModal.module.css | 73 ++++++--- .../src/components/SettingsPanel/index.tsx | 13 +- 5 files changed, 193 insertions(+), 129 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 036df7ef..7d731960 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -2,13 +2,10 @@ import styles from "./ChatHistoryPannel.module.css"; import { getChatHistory, getChatFromHistoryPannelById, deleteChatConversation } from "../../api"; import { useContext, useEffect, useState } from "react"; import { AppContext } from "../../providers/AppProviders"; -import trash from "../../assets/trash.png"; -import pencil from "../../assets/pencil.png"; -import yes from "../../assets/check.png"; -import no from "../../assets/close.png"; import { Spinner } from "@fluentui/react"; import { ToastContainer, toast } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; +import { DeleteRegular, CheckmarkRegular, DismissRegular, ChevronDownRegular, ChevronUpRegular } from "@fluentui/react-icons"; interface ChatHistoryPanelProps { onDeleteChat: () => void; @@ -21,6 +18,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const [deletingIsLoading, setDeletingIsLoading] = useState(false); const [confirmationDelete, setConfirmationDelete] = useState(null); const [conversationsIds, setConversationsIds] = useState([]); + const [expandedSections, setExpandedSections] = useState>(new Set()); const { dataHistory, setDataHistory, @@ -152,15 +150,17 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete { label: "This Month", filter: (itemDate: any) => itemDate >= startOfMonth && itemDate <= today }, { label: "Previous Months", filter: (itemDate: any) => itemDate < startOfMonth } ].map(({ label, filter }) => { - const filteredData = sortedDataByDate.filter(item => { - const itemDate = new Date(item.start_date); - if (!uniqueItems.has(item)) { - const matches = filter(itemDate); - if (matches) uniqueItems.add(item); - return matches; - } - return false; - }).reverse(); + const filteredData = sortedDataByDate + .filter(item => { + const itemDate = new Date(item.start_date); + if (!uniqueItems.has(item)) { + const matches = filter(itemDate); + if (matches) uniqueItems.add(item); + return matches; + } + return false; + }) + .reverse(); return { label, data: filteredData }; }); @@ -170,6 +170,18 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const isChatSelected = (conversationId: string) => chatSelected === conversationId; + const toggleSection = (sectionIndex: number) => { + setExpandedSections(prevExpandedSections => { + const newExpandedSections = new Set(prevExpandedSections); + if (newExpandedSections.has(sectionIndex)) { + newExpandedSections.delete(sectionIndex); + } else { + newExpandedSections.add(sectionIndex); + } + return newExpandedSections; + }); + }; + return (
@@ -186,64 +198,65 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete
{data.length > 0 && ( <> -

{label}

- {data.map((conversation, index) => ( -
handleMouseEnter(`${monthIndex}-${index}`)} - onMouseLeave={handleMouseLeave} - > -
+ {expandedSections.has(monthIndex) && + data.map((conversation, index) => ( +
fetchConversation(conversation.id)} + onMouseEnter={() => handleMouseEnter(`${monthIndex}-${index}`)} + onMouseLeave={handleMouseLeave} > - {isConfirmationDelete(conversation.id) ? "Do you want to delete this conversation?" : conversation.content} - - {hoveredItemIndex === `${monthIndex}-${index}` || - chatSelected === conversation.id || - chatId === conversation.id || - isConfirmationDelete(conversation.id) ? ( -
- Destroy setConfirmationDelete(null) - : () => setConfirmationDelete(conversation.id) - } - /> - {deletingIsLoading && isConfirmationDelete(conversation.id) ? ( - - ) : ( - <> - {isConfirmationDelete(conversation.id) && - fetchConversation(conversation.id)} + > + {isConfirmationDelete(conversation.id) ? "Do you want to delete this conversation?" : conversation.content} + + {hoveredItemIndex === `${monthIndex}-${index}` || + chatSelected === conversation.id || + chatId === conversation.id || + isConfirmationDelete(conversation.id) ? ( +
+ {isConfirmationDelete(conversation.id) ? ( + handleDeleteConversation(conversation.id) - : () => setConfirmationDelete(null) - } - />} - - )} -
- ) : ( - <> - )} -
- ))} + onClick={() => setConfirmationDelete(null)} + style={{ color: "red" }} + /> + ) : ( + setConfirmationDelete(conversation.id)} + /> + )} + + {deletingIsLoading && isConfirmationDelete(conversation.id) ? ( + + ) : ( + isConfirmationDelete(conversation.id) && ( + handleDeleteConversation(conversation.id)} + style={{ color: "green" }} + /> + ) + )} +
+ ) : ( + <> + )} +
+ ))} )}
diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index eae11e4f..b9fa7468 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -16,7 +16,7 @@ export const ChatHistoryPanel: React.FC = ({ functionDele }; return (
-
+
Chat history
diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 7824fc8d..8dc63ed5 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -1,17 +1,17 @@ -.clearButton{ +.clearButton { border: none; font-size: 25px; - color: #5da3cc; + color: #726c6c; background-color: transparent; cursor: pointer; padding: 10.2px 7.5px; margin-top: -15px; } -.closeButton{ +.closeButton { border: none; font-size: 25px; - color: #5da3cc; + color: #726c6c; background-color: transparent; cursor: pointer; transform: rotate(45deg); @@ -25,10 +25,14 @@ } .container { - margin-right: 10px; - margin-top: 5px; width: 250px; - padding: 0 10px; +} + +.card { + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); } .header { @@ -36,11 +40,29 @@ justify-content: space-between; align-items: center; position: sticky; + padding: 24px 15px 24px 24px; + border-bottom: 1px solid #e9e9e9; } .title { - font-weight: 600; + font-weight: 700; font-size: 18px; + color: #4f4f4f; + line-height: 120%; +} + +.subtitle { + font-weight: 700; + font-size: 14px; + color: #4f4f4f; + line-height: 120%; +} + +.timeContainer { + display: flex; + padding: 15.5px 12.8px 15.5px 15px; + justify-content: space-between; + align-items: center; } .buttons { @@ -53,6 +75,7 @@ display: flex; flex-direction: column; max-width: 100%; + padding: 0px 8px; } .buttonConversation{ @@ -71,12 +94,13 @@ .buttonConversationSelected{ border: none; - padding: 10px; + padding: 2px 0px; text-align: left; width: 100%; background-color: transparent; cursor: pointer; font-weight: bold; + line-height: normal; } .actionsButtons{ @@ -90,38 +114,41 @@ display: flex; border-radius: 5px; margin-bottom: 4px; + padding: 3px 2px 3px 15px; } -.conversationSelected{ - background-color: rgba(221, 221, 221, 0.349); +.conversationSelected { display: flex; + padding: 3px 2px 3px 15px; + justify-content: space-between; + align-items: center; border-radius: 5px; - margin-bottom: 4px; - transition: .2s; + background-color: #ffffff; + transition: 0.2s; } -.conversationContainer:hover{ - background-color: rgb(228, 228, 228); +.conversationContainer:hover { + background-color: #e3e3e3; } -.actionButton{ - height: 28px; +.actionButton { + width: 35px; + height: 35px; + padding: 6px; justify-content: center; align-items: center; - margin: auto; - background-color: white; - padding: 3px; - border-radius: 5px; + background-color: #f5f5f5; + border-radius: 6px; cursor: pointer; - border: 1.5px solid rgb(231, 231, 231); + border: 1px solid #e3e3e3; + transition: 0.3s; + margin: auto; margin-right: 5px; - transition: .3s; - width: 28px; } -.actionButton:hover{ - border: 1.5px solid rgb(212, 212, 212); - box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 0.34) inset; +.actionButton:hover { + background-color: #e3e3e3; + border: 1px solid #ffffff; } .loaderContainer{ @@ -143,10 +170,10 @@ @keyframes s2 { to { - transform: rotate(1turn) + transform: rotate(1turn); } } .listContainer { - margin-bottom: 80px; -} \ No newline at end of file + margin-bottom: 20px; +} diff --git a/frontend/src/components/SettingsPanel/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css index 734c3c20..4d23d1a8 100644 --- a/frontend/src/components/SettingsPanel/SettingsModal.module.css +++ b/frontend/src/components/SettingsPanel/SettingsModal.module.css @@ -7,7 +7,7 @@ border: 1.5px solid rgb(231, 231, 231); border-radius: 5px; padding: 4px 12px; - transition: .2s; + transition: 0.2s; background-color: white; font-size: 24px; width: 100%; @@ -24,36 +24,53 @@ .buttonText { font-weight: 500; - padding-left: 10px -} + padding-left: 10px; +} .disabled { - opacity: 0.4 + opacity: 0.4; } .answerContainer { - padding: 10px 20px 20px 20px; - background: rgb(249, 249, 249); - box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - outline: transparent solid 1px; - width: 260px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + width: 250px; +} + +.content { + display: flex; + padding: 12px 0px 20px 28px; + flex-direction: column; + align-items: flex-end; + align-self: stretch; } .saveButton { - background: rgb(0, 0, 0); - color: white; - border-radius: 8px; - padding: 10px; - font-size: 20px; - width: 100%; - border: none; + display: flex; + padding: 10px 24px 8px 24px; + justify-content: center; + align-items: center; + background: #e3e3e3; + border-radius: 4px; + border: 1px solid #9f9c9c; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + font-size: 12px; cursor: pointer; + text-transform: uppercase; + width: 85%; + font-weight: 500; margin-top: 20px; } .saveButton:hover { - background: rgb(0, 0, 0, 0.8); - color: white; + background: #f5f5f5; +} + +.saveIcon { + font-size: 15px; + margin-right: 5px; } .closeButton { @@ -78,8 +95,12 @@ } .header2 { - padding: 10px; display: flex; + justify-content: space-between; + align-items: center; + position: sticky; + padding: 24px 15px 24px 24px; + border-bottom: 1px solid #e9e9e9; } .modal-title { @@ -93,7 +114,7 @@ font-size: 50; height: 50; width: 50; - margin: '0 25px'; + margin: "0 25px"; } .w-100 { @@ -108,8 +129,10 @@ } .title { - font-weight: 600; - font-size: 24px; + font-weight: 700; + font-size: 18px; + color: #4f4f4f; + line-height: 120%; } .buttons { @@ -118,15 +141,15 @@ justify-content: center; } -.closeButtonContainer:active{ +.closeButtonContainer:active { background-color: rgba(214, 214, 214, 0.315); border-radius: 5px; } -.closeButton2{ +.closeButton2 { border: none; font-size: 25px; - color: #5da3cc; + color: #726c6c; background-color: transparent; cursor: pointer; transform: rotate(45deg); diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 61ce2e7f..08474ec1 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -28,7 +28,8 @@ const itemClass = mergeStyles({ display: "flex", alignItems: "center", justifyContent: "space-between", - padding: "10px 0" + padding: "10px 0", + width: "85%" }); const ConfirmationDialog = ({ loading, isOpen, onDismiss, onConfirm }: { loading: boolean; isOpen: boolean; onDismiss: () => void; onConfirm: () => void }) => { @@ -207,7 +208,7 @@ export const SettingsPanel = () => {

Loading your settings

) : ( -
+
Creativity Scale @@ -225,11 +226,11 @@ export const SettingsPanel = () => { onChange={e => handleSetTemperature(e)} aria-labelledby="temperature-slider" /> + setIsDialogOpen(true)} aria-label="Save settings"> + +   Save +
- setIsDialogOpen(true)} aria-label="Save settings"> - -   Save -
)} From 5a1aa9c745028e4f7f3538bb174522677d9eea59 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Fri, 23 Aug 2024 17:01:31 -0400 Subject: [PATCH 133/820] add endpoint calls --- backend/app.py | 75 ++++++++++++++++++++++++++++++++++------- frontend/src/api/api.ts | 19 +++++++++++ 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/backend/app.py b/backend/app.py index 4bc6bf83..bf2880e9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -29,6 +29,7 @@ HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/conversations" CHECK_USER_ENDPOINT = ORCHESTRATOR_URI + "/checkUser" SUBSCRIPTION_ENDPOINT = ORCHESTRATOR_URI + "/subscriptions" +INVITATIONS_ENDPOINT = ORCHESTRATOR_URI + "/invitations" STORAGE_ACCOUNT = os.getenv("STORAGE_ACCOUNT") # email @@ -255,8 +256,7 @@ def create_checkout_session(): ], mode="subscription", client_reference_id=userId, - metadata={"userId": userId, - "organizationId": organizationId}, + metadata={"userId": userId, "organizationId": organizationId}, success_url=success_url, cancel_url=cancel_url, automatic_tax={"enabled": True}, @@ -274,6 +274,7 @@ def create_checkout_session(): return jsonify({"url": checkout_session.url}) + @app.route("/api/stripe", methods=["GET"]) def getStripe(): try: @@ -284,6 +285,7 @@ def getStripe(): logging.exception("[webbackend] exception in /api/stripe") return jsonify({"error": str(e)}), 500 + @app.route("/webhook", methods=["POST"]) def webhook(): stripe.api_key = os.getenv("STRIPE_API_KEY") @@ -323,7 +325,9 @@ def webhook(): keySecretName = "orchestrator-host--subscriptions" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--subscriptions") + logging.exception( + "[webbackend] exception in /api/orchestrator-host--subscriptions" + ) return ( jsonify( { @@ -342,10 +346,13 @@ def webhook(): "subscriptionId": subscriptionId, "paymentStatus": paymentStatus, "organizationName": organizationName, - "expirationDate": expirationDate + "expirationDate": expirationDate, } ) - headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + headers = { + "Content-Type": "application/json", + "x-functions-key": functionKey, + } response = requests.request("POST", url, headers=headers, data=payload) logging.info(f"[webbackend] RESPONSE: {response.text[:500]}...") except Exception as e: @@ -357,6 +364,7 @@ def webhook(): return jsonify(success=True) + @app.route("/api/upload-blob", methods=["POST"]) def uploadBlob(): if "file" not in request.files: @@ -388,6 +396,7 @@ def uploadBlob(): logging.exception("[webbackend] exception in /api/upload-blob") return jsonify({"error": str(e)}), 500 + @app.route("/api/get-blob", methods=["POST"]) def getBlob(): logging.exception("------------------ENTRA ------------") @@ -408,6 +417,7 @@ def getBlob(): logging.exception(blob_name) return jsonify({"error": str(e)}), 500 + @app.route("/api/settings", methods=["GET"]) def getSettings(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -751,6 +761,43 @@ def sendEmail(): return jsonify({"error": str(e)}), 500 +@app.route("/api/createInvitation", methods=["POST"]) +def createInvitation(): + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + if not client_principal_id: + return ( + jsonify({"error": "Missing required parameters, client_principal_id"}), + 400, + ) + try: + keySecretName = "orchestrator-host--invitations" + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception( + "[webbackend] exception in /api/orchestrator-host--subscriptions" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + try: + organizationId = request.json["organizationId"] + invitedUserEmail = request.json["invitedUserEmail"] + url = INVITATIONS_ENDPOINT + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + payload = json.dumps({"invited_user_email": invitedUserEmail, "organization_id": organizationId}) + response = requests.request("POST", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text + except Exception as e: + logging.exception("[webbackend] exception in /getUser") + return jsonify({"error": str(e)}), 500 + + @app.route("/api/checkuser", methods=["POST"]) def checkUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -808,18 +855,16 @@ def getOrganization(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") if not client_principal_id: return ( - jsonify( - { - "error": "Missing required parameters, client_principal_id" - } - ), + jsonify({"error": "Missing required parameters, client_principal_id"}), 400, ) try: keySecretName = "orchestrator-host--subscriptions" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--subscriptions") + logging.exception( + "[webbackend] exception in /api/orchestrator-host--subscriptions" + ) return ( jsonify( { @@ -832,13 +877,16 @@ def getOrganization(): organizationId = request.args.get("organizationId") url = SUBSCRIPTION_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", url, headers=headers, params={"organizationId": organizationId}) + response = requests.request( + "GET", url, headers=headers, params={"organizationId": organizationId} + ) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: logging.exception("[webbackend] exception in /get-organization") return jsonify({"error": str(e)}), 500 - + + @app.route("/api/getUser", methods=["GET"]) def getUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -882,5 +930,6 @@ def getUser(): logging.exception("[webbackend] exception in /getUser") return jsonify({"error": str(e)}), 500 + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 537ee845..23bea0ad 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -306,6 +306,25 @@ export async function inviteUser({ username, email, organizationId }: any): Prom return { error: error }; } } +export async function createInvitation({organizationId, invitedUserEmail} : any): Promise { + try { + const response = await fetch("/api/createInvitation", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + organizationId, + invitedUserEmail + }) + }); + const fetchedData = await response.json(); + return fetchedData; + } catch (error) { + console.error("Error creating invitation", error); + return { error: error }; + } +} export async function getApiKeyPayment(): Promise { const response = await fetch("/api/stripe", { method: "GET", From 07bcc04455463f8a20fc8dad1b091f9cf2f7f247 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 26 Aug 2024 11:49:34 -0400 Subject: [PATCH 134/820] connect frontend to invitation endpoint --- frontend/src/api/api.ts | 5 +++-- frontend/src/pages/admin/Admin.tsx | 16 +++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 23bea0ad..47d8c3df 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -306,12 +306,13 @@ export async function inviteUser({ username, email, organizationId }: any): Prom return { error: error }; } } -export async function createInvitation({organizationId, invitedUserEmail} : any): Promise { +export async function createInvitation({organizationId, invitedUserEmail, userId} : any): Promise { try { const response = await fetch("/api/createInvitation", { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId }, body: JSON.stringify({ organizationId, diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 26c1055e..beff56f9 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -7,7 +7,7 @@ import { AddFilled } from "@fluentui/react-icons"; import { AppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; -import { checkUser, getUsers, inviteUser } from "../../api"; +import { checkUser, getUsers, inviteUser, createInvitation } from "../../api"; import styles from "./Admin.module.css"; @@ -45,13 +45,19 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; setLoading(true); const organizationId = user.organizationId; - inviteUser({ username, email, role, organizationId }).then(res => { + inviteUser({ username: sanitizedUsername, email: sanitizedEmail, role, organizationId }).then(res => { if (res.error) { setErrorMessage(res.error); } else { - setErrorMessage(""); - setLoading(false); - setSuccess(true); + createInvitation({ organizationId, invitedUserEmail: sanitizedEmail, userId: user.id }).then(res => { + if (res.error) { + setErrorMessage(res.error); + } else { + setErrorMessage(""); + setLoading(false); + setSuccess(true); + } + }); } }); }; From f13fc9f780f5966e4808677dd02e7892f735b038 Mon Sep 17 00:00:00 2001 From: Fabiola Rueda <30278070+fabiolayerim@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:03:20 -0400 Subject: [PATCH 135/820] SFC-369 update feedback panel styles (#112) --- .../FeedbackRating/FeedbackRating.module.css | 95 ++++++++++++------- .../FeedbackRating/FeedbackRating.tsx | 8 +- 2 files changed, 67 insertions(+), 36 deletions(-) diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css index b8cc499b..eba0dc2b 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.module.css +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -1,18 +1,17 @@ - -.clearButton{ +.clearButton { border: none; font-size: 25px; - color: #5da3cc; + color: #726c6c; background-color: transparent; cursor: pointer; padding: 10.2px 7.5px; margin-top: -15px; } -.closeButton{ +.closeButton { border: none; font-size: 25px; - color: #5da3cc; + color: #726c6c; background-color: transparent; cursor: pointer; transform: rotate(45deg); @@ -26,10 +25,14 @@ } .container { - margin-right: 10px; - margin-top: 5px; width: 250px; - padding: 0 10px; +} + +.cardFeedback { + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); } .header { @@ -37,11 +40,15 @@ justify-content: space-between; align-items: center; position: sticky; + padding: 24px 15px 24px 24px; + border-bottom: 1px solid #e9e9e9; } .title { - font-weight: 600; + font-weight: 700; font-size: 18px; + color: #4f4f4f; + line-height: 120%; } .buttons { @@ -54,6 +61,13 @@ display: flex; flex-direction: column; max-width: 100%; + padding: 0px 8px; +} + +.listContainer { + display: flex; + flex-direction: column; + padding: 15.5px 12.8px 12px 20px; } .buttonConversation{ @@ -84,7 +98,7 @@ display: flex; border-radius: 5px; margin-bottom: 4px; - transition: .2s; + transition: 0.2s; } .conversationContainer:hover{ @@ -102,7 +116,7 @@ cursor: pointer; border: 1.5px solid rgb(231, 231, 231); margin-right: 5px; - transition: .3s; + transition: 0.3s; } .actionButton:hover{ @@ -129,25 +143,38 @@ @keyframes s2 { to { - transform: rotate(1turn) + transform: rotate(1turn); } } .saveButton { - background: rgb(0, 0, 0); - color: white; - border-radius: 8px; - padding: 10px; - font-size: 20px; - width: 100%; - border: none; + display: flex; + padding: 10px 24px 8px 24px; + justify-content: center; + align-items: center; + background: #e3e3e3; + border-radius: 4px; + border: 1px solid #9f9c9c; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + font-size: 12px; cursor: pointer; - margin-top: 20px; + text-transform: uppercase; + width: 100%; + font-weight: 500; + margin-top: 5px; } .saveButton:hover { - background: rgb(0, 0, 0, 0.8); - color: white; + background: #f5f5f5; +} + +.sendIcon { + font-size: 15px; + margin-left: 5px; +} + +.icon { + font-size: 15px; } .rating { @@ -167,22 +194,26 @@ } .thumbButton { - width: 60px; - height: 60px; + width: 50px; + height: 50px; cursor: pointer; - margin: 0 10px; - outline: none; - border: none; - background: none; - padding: 0; + margin: 0 10px; + outline: none; + border: none; + background: none; + padding: 0; } .thumbButton svg { - width: 40px; - height: 40px; + width: 30px; + height: 30px; } .thumbButton:focus:not(:focus-visible) { outline: none; } .thumbButton:focus-visible { outline: 2px solid #000000; -} \ No newline at end of file +} + +.message { + margin-top: 10px; +} diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index a6b21644..741db492 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useContext } from "react"; import { Dropdown, TextField, Button, Spinner, DefaultButton } from "@fluentui/react"; import styles from "./FeedbackRating.module.css"; import { AppContext } from "../../providers/AppProviders"; -import { AddFilled, SaveFilled, ThumbLikeFilled, ThumbDislikeFilled } from "@fluentui/react-icons"; +import { AddFilled, SendRegular, ThumbLikeFilled, ThumbDislikeFilled } from "@fluentui/react-icons"; import { ThumbLikeRegular, ThumbDislikeRegular } from "@fluentui/react-icons"; import { postFeedbackRating } from "../../api/api"; @@ -96,7 +96,7 @@ export const FeedbackRating = () => { return (
-
+
Feedback
@@ -116,7 +116,7 @@ export const FeedbackRating = () => { )} - +
-   Send +
{errorMessage !== null &&

{errorMessage}

} From ded21f41d3b07f00c51154e2a2ae0a5836baafca Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 26 Aug 2024 23:28:50 -0400 Subject: [PATCH 136/820] SFC-363-delete-user-invitations --- backend/app.py | 72 ++++++++-- frontend/src/api/api.ts | 17 +++ frontend/src/pages/admin/Admin.tsx | 221 +++++++++++++++++++++-------- 3 files changed, 240 insertions(+), 70 deletions(-) diff --git a/backend/app.py b/backend/app.py index 4bc6bf83..83d849b3 100644 --- a/backend/app.py +++ b/backend/app.py @@ -255,8 +255,7 @@ def create_checkout_session(): ], mode="subscription", client_reference_id=userId, - metadata={"userId": userId, - "organizationId": organizationId}, + metadata={"userId": userId, "organizationId": organizationId}, success_url=success_url, cancel_url=cancel_url, automatic_tax={"enabled": True}, @@ -274,6 +273,7 @@ def create_checkout_session(): return jsonify({"url": checkout_session.url}) + @app.route("/api/stripe", methods=["GET"]) def getStripe(): try: @@ -284,6 +284,7 @@ def getStripe(): logging.exception("[webbackend] exception in /api/stripe") return jsonify({"error": str(e)}), 500 + @app.route("/webhook", methods=["POST"]) def webhook(): stripe.api_key = os.getenv("STRIPE_API_KEY") @@ -323,7 +324,9 @@ def webhook(): keySecretName = "orchestrator-host--subscriptions" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--subscriptions") + logging.exception( + "[webbackend] exception in /api/orchestrator-host--subscriptions" + ) return ( jsonify( { @@ -342,10 +345,13 @@ def webhook(): "subscriptionId": subscriptionId, "paymentStatus": paymentStatus, "organizationName": organizationName, - "expirationDate": expirationDate + "expirationDate": expirationDate, } ) - headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + headers = { + "Content-Type": "application/json", + "x-functions-key": functionKey, + } response = requests.request("POST", url, headers=headers, data=payload) logging.info(f"[webbackend] RESPONSE: {response.text[:500]}...") except Exception as e: @@ -357,6 +363,7 @@ def webhook(): return jsonify(success=True) + @app.route("/api/upload-blob", methods=["POST"]) def uploadBlob(): if "file" not in request.files: @@ -388,6 +395,7 @@ def uploadBlob(): logging.exception("[webbackend] exception in /api/upload-blob") return jsonify({"error": str(e)}), 500 + @app.route("/api/get-blob", methods=["POST"]) def getBlob(): logging.exception("------------------ENTRA ------------") @@ -408,6 +416,7 @@ def getBlob(): logging.exception(blob_name) return jsonify({"error": str(e)}), 500 + @app.route("/api/settings", methods=["GET"]) def getSettings(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -627,6 +636,42 @@ def getUsers(): return jsonify({"error": str(e)}), 500 +@app.route("/api/deleteuser", methods=["DELETE"]) +def deleteUser(): + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + + if not client_principal_id: + return ( + jsonify({"error": "Missing required parameters, client_principal_id"}), + 400, + ) + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = "orchestrator-host--checkuser" + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception("[webbackend] exception in /api/orchestrator-host--checkuser") + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + try: + userId = request.args.get("userId") + url = CHECK_USER_ENDPOINT + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + response = requests.request("DELETE", url, headers=headers,params={"id": userId}) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text + except Exception as e: + logging.exception("[webbackend] exception in /api/checkUser") + return jsonify({"error": str(e)}), 500 + + @app.route("/api/inviteUser", methods=["POST"]) def sendEmail(): if ( @@ -808,18 +853,16 @@ def getOrganization(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") if not client_principal_id: return ( - jsonify( - { - "error": "Missing required parameters, client_principal_id" - } - ), + jsonify({"error": "Missing required parameters, client_principal_id"}), 400, ) try: keySecretName = "orchestrator-host--subscriptions" functionKey = get_secret(keySecretName) except Exception as e: - logging.exception("[webbackend] exception in /api/orchestrator-host--subscriptions") + logging.exception( + "[webbackend] exception in /api/orchestrator-host--subscriptions" + ) return ( jsonify( { @@ -832,13 +875,16 @@ def getOrganization(): organizationId = request.args.get("organizationId") url = SUBSCRIPTION_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", url, headers=headers, params={"organizationId": organizationId}) + response = requests.request( + "GET", url, headers=headers, params={"organizationId": organizationId} + ) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: logging.exception("[webbackend] exception in /get-organization") return jsonify({"error": str(e)}), 500 - + + @app.route("/api/getUser", methods=["GET"]) def getUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 537ee845..a1782f21 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -37,6 +37,23 @@ export async function getUsers({ user }: any): Promise { } } +export async function deleteUser({ user, userId }: any): Promise { + try { + const response = await fetch(`/api/deleteuser?userId=${userId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user.id, + } + }); + const fetchedData = await response.json(); + return fetchedData; + } catch (error) { + console.error("Error deleting user", error); + return { error: error }; + } +} + export async function checkUser({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 26c1055e..d0197463 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState, useContext } from "react"; import { PrimaryButton, IconButton, Spinner, Dialog, DialogContent, Label, Dropdown, DefaultButton, MessageBar } from "@fluentui/react"; - +import { ToastContainer, toast } from "react-toastify"; import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; import { AddFilled } from "@fluentui/react-icons"; import { AppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; -import { checkUser, getUsers, inviteUser } from "../../api"; +import { checkUser, getUsers, inviteUser, deleteUser } from "../../api"; import styles from "./Admin.module.css"; @@ -223,15 +223,91 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; ); }; +export const DeleteUserDialog = ({ + isOpen, + onDismiss, + onConfirm +}: { + isOpen: boolean; + onDismiss: any; + onConfirm: any; +}) => { + return ( + + ); +}; + const Admin = () => { const { user } = useContext(AppContext); const [search, setSearch] = useState(""); const [filteredUsers, setFilteredUsers] = useState([]); + const [selectedUser, setSelectedUser] = useState({ + id: "", + data: { + name: "", + email: "", + role: "" + } + }); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [isOpen, setIsOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { const getUserList = async () => { @@ -257,8 +333,25 @@ const Admin = () => { } }, [search]); + const deleteUserFromOrganization = (id: string) => { + deleteUser({ user, userId: id }).then(res => { + if (res.error) { + console.log("error", res.error); + toast("There was an error deleting the user", { type: "error" }); + setIsDeleting(false); + } else { + const updatedUsers = users.filter((user: any) => user.id !== id); + setUsers(updatedUsers); + setFilteredUsers(updatedUsers); + setIsDeleting(false); + toast("User deleted successfully", { type: "success" }); + } + }); + }; + return (
+ {user.role !== "admin" &&

Access denied

} {user.role === "admin" && ( <> @@ -327,6 +420,15 @@ const Admin = () => {
+ { + setIsDeleting(false); + }} + onConfirm={() => { + deleteUserFromOrganization(selectedUser?.id); + }} + /> {loading ? ( { {filteredUsers.map((user: any, index) => { return ( - - + - {user.data.name} - - - {user.data.email} - - -
+ {user.data.name} + + + {user.data.email} + +
- {user.data.role} -
-
- - - { -
- {}} - /> - {}} - /> + > + {user.data.role} +
- } - - + + + { +
+ {}} + /> + { + setSelectedUser(user); + setIsDeleting(true); + }} + /> +
+ } + + + ); })} From 4375731865e5d48ded54b2a3abb17d0dd8f73b67 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 26 Aug 2024 23:49:10 -0400 Subject: [PATCH 137/820] SFC-375-user-organization-validation --- backend/app.py | 3 ++- frontend/src/api/api.ts | 3 ++- frontend/src/pages/chat/Chat.tsx | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/app.py b/backend/app.py index 4bc6bf83..b6e76046 100644 --- a/backend/app.py +++ b/backend/app.py @@ -617,9 +617,10 @@ def getUsers(): ) try: + organizationId = request.args.get("organizationId") url = CHECK_USER_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", url, headers=headers) + response = requests.request("GET", url, headers=headers, params={"organizationId": organizationId}) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 537ee845..7a75f424 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -16,8 +16,9 @@ import { export async function getUsers({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; + const user_organizationId = user ? user.organizationId : "00000000-0000-0000-0000-000000000000"; try { - const response = await fetch("/api/getusers", { + const response = await fetch("/api/getusers?organizationId=" + user_organizationId, { method: "GET", headers: { "Content-Type": "application/json", diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index bb5a19ca..f7511d57 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -13,7 +13,6 @@ import { ChatTurn, getUserInfo, checkUser, - getUsers, getOrganizationSubscription } from "../../api"; import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; From 6fa76f44d16082425045c02218e50276a1d063da Mon Sep 17 00:00:00 2001 From: fabiolayerim Date: Tue, 27 Aug 2024 02:58:24 -0400 Subject: [PATCH 138/820] SFC-373 update chat styles --- .../QuestionInput/QuestionInput.module.css | 20 +-- .../QuestionInput/QuestionInput.tsx | 26 ++-- frontend/src/pages/chat/Chat.module.css | 127 +++++++++++------- frontend/src/pages/chat/Chat.tsx | 24 ++-- frontend/src/pages/layout/Layout.module.css | 11 +- 5 files changed, 118 insertions(+), 90 deletions(-) diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index f5fa1c4c..af81ae9e 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -1,16 +1,15 @@ .questionInputContainer { - border-radius: 8px; - box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + border-radius: 6px; + border: 2px solid #9fc51d; height: 90px; width: 100%; padding: 15px; background: white; - border-bottom: 5px solid #a2e700; } .attachmentContainer { - height: '100%'; - justify-content: 'center'; + height: "100%"; + justify-content: "center"; align-items: center; display: flex; } @@ -22,14 +21,15 @@ .questionInputButtonsContainer { display: flex; - flex-direction: column; + flex-direction: row; justify-content: center; - align-content: center; + align-items: center; padding-left: 4px; } .questionInputSendButton { cursor: pointer; + padding-right: 8px; } .questionInputSendButtonDisabled { @@ -37,6 +37,8 @@ } .attachmentButton { - transform: rotate(-45deg); + transform: rotate(-135deg) scaleX(-1) translate(-5px, 0px); cursor: pointer; -} \ No newline at end of file + font-size: 24px; + color: #9f9c9c; +} diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index b9cea16c..e4832802 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -2,7 +2,7 @@ import { useState, useContext } from "react"; import { AppContext } from "../../providers/AppProviders"; import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; import { getTokenOrRefresh } from "./token_util"; -import { Send32Filled, Attach32Filled, SlideMicrophone32Filled } from "@fluentui/react-icons"; +import { Send24Filled, Mic24Regular, AttachRegular } from "@fluentui/react-icons"; import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from "microsoft-cognitiveservices-speech-sdk"; import { uploadFile } from "../../api"; @@ -162,7 +162,7 @@ export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url:

- +
); @@ -239,7 +239,6 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr return ( -
+
{ if (ev.key === "Enter") { ev.preventDefault(); - sendQuestion(); + sttFromMic(); } }} tabIndex={0} > - +
{ if (ev.key === "Enter") { ev.preventDefault(); - sttFromMic(); + sendQuestion(); } }} tabIndex={0} > - +
diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 0105fdbc..7824795e 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -1,17 +1,13 @@ .container { flex: 1; background-color: white; - border: 1px solid rgb(231, 231, 231); - border-radius: 10px; - margin: 0px 10px; - margin-bottom: 30px; - box-shadow: 0px 5px 4px -2px rgba(0, 0, 0, 0.2); + margin: 0px 10px 0px 0px; } -.mainContainer{ +.mainContainer { display: flex; flex-direction: row-reverse; - height: 100%; + height: 90%; overflow: hidden; } @@ -62,18 +58,18 @@ } } -.noneDisplay{ +.noneDisplay { display: none; } -.flexDescription{ +.flexDescription { display: flex; flex-direction: column; align-items: center; justify-content: center; } -.conversationIsLoading{ +.conversationIsLoading { flex-grow: 1; display: flex; flex-direction: column; @@ -87,7 +83,7 @@ flex-grow: 1; max-height: 1024px; max-width: 1028px; - width: 100%; + width: 90%; overflow-y: auto; padding-left: 24px; padding-right: 24px; @@ -113,68 +109,89 @@ bottom: 0; flex: 0 0 100px; padding-top: 12px; - padding-bottom: 24px; + padding-bottom: 20px; padding-left: 24px; padding-right: 24px; - width: 100%; + width: 90%; max-width: 1028px; - background: #ffffff; + background: transparent; display: flex; align-items: center; } -.newChatButton{ - color: #ffffff; +.newChatButton { + display: flex; + padding: 10px; + justify-content: center; + align-items: center; font-size: 24px; - border: none; - padding: 8px 7px 1px; - border-radius: 5px; - cursor: pointer; - background: radial-gradient(109.81% 107.82% at 100.1% 90.19%, #0F6CBD 33.63%, #2D87C3 70.31%, #8DDDD8 100%); - transition: .3s; + border: 1px solid #979797; + border-radius: 6px; + color: #979797; + background-color: #f5f5f5; + transition: 0.3s; + margin-right: 5px; } .newChatButton:hover { - color: #8DDDD8; + color: #000000; } -.newChatButtonDisabled{ - color: #a3a3a3; +.newChatButtonDisabled { + display: flex; + padding: 10px; + justify-content: center; + align-items: center; font-size: 24px; - border: none; - padding: 8px 7px 1px; - border-radius: 5px; - transition: .3s; -} - -.clearChatButtonDisabled{ - color: #a3a3a3; + border: 1px dashed #bfbfbf; + border-radius: 6px; + color: #bfbfbf; + background-color: #f0f0f0; + cursor: not-allowed; + opacity: 0.5; + transition: 0.3s; + margin-right: 5px; + font-style: italic; +} + +.clearChatButtonDisabled { + display: flex; + padding: 10px; + justify-content: center; + align-items: center; font-size: 24px; - border: none; - padding: 8px 7px 1px; - border-radius: 5px; - transition: .3s; - margin-bottom: 5px; -} - -.buttonsActions{ - margin-right: -10px; + border: 1px dashed #bfbfbf; + border-radius: 6px; + color: #bfbfbf; + background-color: #f0f0f0; + cursor: not-allowed; + opacity: 0.5; + transition: 0.3s; + margin-right: 5px; + font-style: italic; +} + +.buttonsActions { + display: flex; + flex-direction: row; + padding-right: 10px; } .clearChatButton { - color: #ffffff; + display: flex; + padding: 10px; + justify-content: center; + align-items: center; font-size: 24px; - border: none; - padding: 8px 7px 1px; - border-radius: 5px; - cursor: pointer; - background: radial-gradient(109.81% 107.82% at 100.1% 90.19%, #0F6CBD 33.63%, #2D87C3 70.31%, #8DDDD8 100%); - transition: .3s; - margin-bottom: 5px; + border: 1px solid #979797; + border-radius: 6px; + color: #9f9c9c; + background-color: #f5f5f5; + transition: 0.3s; } .clearChatButton:hover { - color: #8DDDD8; + color: #000000; } .chatAnalysisPanel { flex: 1; @@ -197,6 +214,8 @@ align-self: flex-start; height: 95%; overflow-y: auto; + margin-top: 10px; + margin-right: 10px; } .loadingData { @@ -206,7 +225,7 @@ overflow-y: hidden; } -.spinnerStyles{ +.spinnerStyles { position: absolute; } @@ -231,3 +250,7 @@ margin-right: 20px; margin-bottom: 20px; } + +.hidden { + display: none; +} diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index bb5a19ca..f034278d 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -471,13 +471,15 @@ const Chat = () => { return (
-
{showHistoryPanel && }
+
+ {showHistoryPanel && } +
-
{showFeedbackRatingPanel && }
+
{showFeedbackRatingPanel && }
-
{settingsPanel && }
+
{settingsPanel && }
@@ -575,13 +577,6 @@ const Chat = () => { )}
- +
Date: Tue, 27 Aug 2024 03:27:23 -0400 Subject: [PATCH 139/820] SFC-373 fix chat history height --- .../ChatHistoryPannel.module.css | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 8dc63ed5..aec524fd 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -76,9 +76,40 @@ flex-direction: column; max-width: 100%; padding: 0px 8px; + max-height: 650px; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + -ms-overflow-style: auto; } -.buttonConversation{ +.content::-webkit-scrollbar { + width: 8px; +} + +.content::-webkit-scrollbar-track { + background: transparent; +} + +.content::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 4px; +} + +.content::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.scrollIndicator { + position: sticky; + bottom: 10px; + text-align: center; + font-size: 24px; + color: #aaa; + pointer-events: none; +} + +.buttonConversation { border: none; padding: 10px 5px; text-align: left; From 09778bb6dcbe368318e3455a7b10232e7d052d82 Mon Sep 17 00:00:00 2001 From: pedrolabrador Date: Wed, 28 Aug 2024 09:08:02 -0400 Subject: [PATCH 140/820] SFC-343 fix more edge cases for citations --- .../src/components/Answer/AnswerParser.tsx | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/frontend/src/components/Answer/AnswerParser.tsx b/frontend/src/components/Answer/AnswerParser.tsx index 33dd5942..2d174be0 100644 --- a/frontend/src/components/Answer/AnswerParser.tsx +++ b/frontend/src/components/Answer/AnswerParser.tsx @@ -12,6 +12,40 @@ export function removeCitations(text: string): string { return newText; } +export function extractMarkdownLinks(text: string) { + const pattern = /\[([^\]]+)\]\(([^)]+)\)/g; + + const links = []; + let match; + while ((match = pattern.exec(text)) !== null) { + const description = match[1]; + const url = match[2]; + links.push({ description, url }); + } + + return links; +} + +export function replaceMarkdownLinks(text: string) { + const pattern = /\[([^\]]+)\]\(([^)]+)\)/g; + + const result = text.replace(pattern, (_, description, url) => { + return "[" + url + "]"; + }); + + return result; +} + +function replaceWrongFormattedNumbers(text: string) { + const pattern = /\[\^(\d+)\^\]/g; + + const result = text.replace(pattern, (_, number) => { + return `[${number}]`; + }); + + return result; +} + export function parseAnswerToHtml( answer: string, showSources: boolean, @@ -21,6 +55,11 @@ export function parseAnswerToHtml( const followupQuestions: string[] = []; var answerHtml: string = ""; + // check for any markdown links in the answer and replace them with the link text + answer = replaceMarkdownLinks(answer); + // Extract any wrong formatted numbers that might be in the answer + answer = replaceWrongFormattedNumbers(answer); + // Extract any follow-up questions that might be in the answer let parsedAnswer = answer.replace(/<<([^>>]+)>>/g, (match, content) => { followupQuestions.push(content); From f313d866193f663db47f43f6a10aa34e5d08a839 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 28 Aug 2024 13:43:13 -0400 Subject: [PATCH 141/820] SFC-381-organization-onboarding-flow --- frontend/src/components/Profile/index.tsx | 3 +- frontend/src/index.tsx | 90 +++++++++-------- frontend/src/pages/chat/Chat.tsx | 87 +---------------- .../pages/onboarding/Onboarding.module.css | 0 frontend/src/pages/onboarding/Onboarding.tsx | 18 ++++ frontend/src/providers/AppProviders.tsx | 96 +++++++++++++++++-- 6 files changed, 160 insertions(+), 134 deletions(-) create mode 100644 frontend/src/pages/onboarding/Onboarding.module.css create mode 100644 frontend/src/pages/onboarding/Onboarding.tsx diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index 3ca79684..562b30b8 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -56,8 +56,7 @@ const placeholderPrepare = (placeholder: string) => { export const ProfileButton: React.FunctionComponent = () => { const { user } = useContext(AppContext); - - console.log(user); + const placeholder = placeholderPrepare(user.name); const email = user?.email || " "; const headerTitle = user.name; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 701aa32a..365d31cc 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useContext } from "react"; import ReactDOM from "react-dom/client"; import { HashRouter, Routes, Route } from "react-router-dom"; import ProtectedRoute from "./router/ProtectedRoute"; @@ -11,58 +11,68 @@ import NoPage from "./pages/NoPage"; import AccessDenied from "./pages/AccesDenied"; import Chat from "./pages/chat/Chat"; import Admin from "./pages/admin/Admin"; +import Onboarding from "./pages/onboarding/Onboarding"; import { AppProvider } from "./providers/AppProviders"; import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; +import { AppContext } from "./providers/AppProviders"; + initializeIcons(); export default function App() { + const { user } = useContext(AppContext); + return ( - - - - }> - } /> - } /> - - }> - - - - } - /> - } /> - - }> - } /> + + + {!user.organizationId && ( + <> + } /> + } /> } /> - - }> - } - /> - } /> - - }> - } - /> - } /> - - - - + + )} + {user.organizationId && ( + <> + }> + } /> + } /> + + }> + + + + } + /> + } /> + + }> + } /> + } /> + + }> + } /> + } /> + + }> + } /> + } /> + + + )} + + ); } ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + + ); diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index f7511d57..e24463e5 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -43,7 +43,7 @@ if (userLanguage.startsWith("pt")) { const Chat = () => { // speech synthesis is disabled by default - const { user, organization } = useContext(AppContext); + const { organization } = useContext(AppContext); const speechSynthesisEnabled = false; const [placeholderText, setPlaceholderText] = useState(""); @@ -80,7 +80,6 @@ const Chat = () => { const [fileType, setFileType] = useState(""); const [isLoading, setIsLoading] = useState(false); - const [isLoadingUserInfo, setIsLoadingUserInfo] = useState(false); const [error, setError] = useState(); const [activeCitation, setActiveCitation] = useState(); @@ -238,72 +237,6 @@ const Chat = () => { } }; - useEffect(() => { - const getUserInfoList = async () => { - if (window.location.hostname !== "127.0.0.1") { - setIsLoadingUserInfo(true); - const userInfoList = await getUserInfo(); - if (userInfoList.length === 0) { - // setShowAuthMessage(true); - console.log("No user info found. Using anonymous user.", userInfoList); - } else { - // setShowAuthMessage(false); - console.log("User info found.", userInfoList); - - const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; - const keyName = "name"; - const keyEmail = "emails"; - - const _user = userInfoList.find(obj => { - const _id = obj?.user_claims?.some(claim => claim.typ === keyId); - const _name = obj?.user_claims?.some(claim => claim.typ === keyName); - return _id && _name; - }); - - if (_user) { - const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; - const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; - const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; - - if (id && name) { - setUser({ id, name, email, role: undefined, organizationId: undefined }); - } - - // register user if doesn't exist - - // const response = await getUsers({ user: { id, name, email } }); // to get all users - - // verifies if user exists and assigns the role - const result = await checkUser({ user: { id, name, email } }); - const role = result["role"] || undefined; - const organizationId = result["organizationId"] || undefined; - - const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); - - if (result && role) { - setUser({ id, name, email, role, organizationId }); - } - if (organization) { - setOrganization({ - id: organization.id, - name: organization.name, - owner: organization.owner, - subscriptionId: organization.subscriptionId, - subscriptionStatus: organization.subscriptionStatus, - subscriptionExpirationDate: organization.subscriptionExpirationDate - }); - } - setIsLoadingUserInfo(false); - } - } - } else { - console.log("Local"); - } - }; - - getUserInfoList(); - }, []); - useEffect(() => { chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }); if (dataConversation.length > 0) { @@ -432,23 +365,7 @@ const Chat = () => { //If I add this on a useEffect it doesn't work, I don't know why //maybe because it's a global event listener and is called multiple times - if (isLoadingUserInfo) { - return ( -
- -
- ); - } - - if (!user.organizationId) { + if (organization.subscriptionStatus === "inactive"){ if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { return (
{ + const [organization, setOrganization] = useState(''); + + const handleOrganizationChange = (event: React.ChangeEvent) => { + setOrganization(event.target.value); + }; + + return ( +
+

Welcome to Freddaid!

+

Let's get started with onboarding.

+
+ ); +}; + +export default Onboarding; \ No newline at end of file diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 54d21256..d8e1fd70 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,12 +1,7 @@ import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; - -interface SettingsType { - temperature: string; - presencePenalty: string; - frequencyPenalty: string; -} - +import { Spinner } from "@fluentui/react"; +import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, getUserInfo, checkUser, getOrganizationSubscription } from "../api"; interface UserInfo { id: string; name: string; @@ -158,6 +153,93 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => //If I add this on a useEffect it doesn't work, I don't know why //maybe because it's a global event listener and is called multiple times + const [loading, setLoading] = useState(true); + + if (!user.organizationId) { //CAMBIAR ESTO CON EL ORG ID + const getUserInfoList = async () => { + if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { + const userInfoList = await getUserInfo(); + if (userInfoList.length === 0) { + // setShowAuthMessage(true); + console.log("No user info found. Using anonymous user.", userInfoList); + } else { + // setShowAuthMessage(false); + console.log("User info found.", userInfoList); + + const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; + const keyName = "name"; + const keyEmail = "emails"; + + const _user = userInfoList.find(obj => { + const _id = obj?.user_claims?.some(claim => claim.typ === keyId); + const _name = obj?.user_claims?.some(claim => claim.typ === keyName); + return _id && _name; + }); + + if (_user) { + const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; + const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; + const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; + + if (id && name) { + setUser({ id, name, email, role: undefined, organizationId: undefined }); + } + + // register user if doesn't exist + + // const response = await getUsers({ user: { id, name, email } }); // to get all users + + // verifies if user exists and assigns the role + const result = await checkUser({ user: { id, name, email } }); + const role = result["role"] || undefined; + const organizationId = result["organizationId"] || undefined; + + const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); + + if (result && role) { + setUser({ id, name, email, role, organizationId }); + } + if (organization) { + setOrganization({ + id: organization.id, + name: organization.name, + owner: organization.owner, + subscriptionId: organization.subscriptionId, + subscriptionStatus: organization.subscriptionStatus, + subscriptionExpirationDate: organization.subscriptionExpirationDate + }); + } + if (loading) { + setLoading(false); + } + } + } + } else { + console.log("Local"); + setUser({...user, organizationId: "test-organization"}) + if (loading) { + setLoading(false); + } + } + }; + getUserInfoList(); + } + + if (loading) + return ( +
+ +
+ ); + return ( Date: Wed, 28 Aug 2024 16:01:34 -0400 Subject: [PATCH 142/820] SFC-377-add-role-checkuser --- backend/app.py | 3 ++- frontend/src/api/api.ts | 5 +++-- frontend/src/pages/admin/Admin.tsx | 2 +- frontend/src/providers/AppProviders.tsx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index 6d7e3a8d..9fff4206 100644 --- a/backend/app.py +++ b/backend/app.py @@ -824,9 +824,10 @@ def createInvitation(): try: organizationId = request.json["organizationId"] invitedUserEmail = request.json["invitedUserEmail"] + role = request.json["role"] url = INVITATIONS_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - payload = json.dumps({"invited_user_email": invitedUserEmail, "organization_id": organizationId}) + payload = json.dumps({"invited_user_email": invitedUserEmail, "organization_id": organizationId, "role": role}) response = requests.request("POST", url, headers=headers, data=payload) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index cd0ecf5d..d43d6b9e 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -324,7 +324,7 @@ export async function inviteUser({ username, email, organizationId }: any): Prom return { error: error }; } } -export async function createInvitation({organizationId, invitedUserEmail, userId} : any): Promise { +export async function createInvitation({organizationId, invitedUserEmail, userId, role} : any): Promise { try { const response = await fetch("/api/createInvitation", { method: "POST", @@ -334,7 +334,8 @@ export async function createInvitation({organizationId, invitedUserEmail, userId }, body: JSON.stringify({ organizationId, - invitedUserEmail + invitedUserEmail, + role }) }); const fetchedData = await response.json(); diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 753af370..6716e7c3 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -49,7 +49,7 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; if (res.error) { setErrorMessage(res.error); } else { - createInvitation({ organizationId, invitedUserEmail: sanitizedEmail, userId: user.id }).then(res => { + createInvitation({ organizationId, invitedUserEmail: sanitizedEmail, userId: user.id, role: role }).then(res => { if (res.error) { setErrorMessage(res.error); } else { diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index d8e1fd70..e98ef51e 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -155,7 +155,7 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [loading, setLoading] = useState(true); - if (!user.organizationId) { //CAMBIAR ESTO CON EL ORG ID + if (!user.organizationId) { const getUserInfoList = async () => { if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { const userInfoList = await getUserInfo(); From d161439ac12db1c0a80b0ca91c7b8026db622be4 Mon Sep 17 00:00:00 2001 From: fabiolayerim Date: Wed, 28 Aug 2024 17:59:07 -0400 Subject: [PATCH 143/820] SFC-379 update admin panel styles --- frontend/src/pages/admin/Admin.module.css | 76 +++++++-- frontend/src/pages/admin/Admin.tsx | 198 +++++++++++----------- 2 files changed, 164 insertions(+), 110 deletions(-) diff --git a/frontend/src/pages/admin/Admin.module.css b/frontend/src/pages/admin/Admin.module.css index 083ee1b9..50425a33 100644 --- a/frontend/src/pages/admin/Admin.module.css +++ b/frontend/src/pages/admin/Admin.module.css @@ -1,24 +1,32 @@ -.page_container{ +.page_container { display: flex; flex-direction: column; width: 100%; - padding: 20px; + padding: 100px 100px 100px 150px; } -.separator{ - font-size: '24px'; - height: '24px'; - width: '24px'; +.separator { + font-size: "24px"; + height: "24px"; + width: "24px"; } -.row{ +.row { display: flex; flex-direction: row; align-items: center; justify-content: space-between; - justify-items:center + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; } -.closeButton{ + +.title { + margin-bottom: 10px; + margin-left: 20px; +} + +.closeButton { position: fixed; top: 0; right: 0px; @@ -32,7 +40,7 @@ padding-top: 6px; } -.closeButtonContainer:active{ +.closeButtonContainer:active { background-color: rgba(214, 214, 214, 0.315); border-radius: 5px; } @@ -49,4 +57,50 @@ .questionInputTextArea { width: 100%; line-height: 40px; -} \ No newline at end of file +} + +.table { + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +.tableContainer { + border-radius: 10px; + overflow: hidden; + border: 1px solid #d9d9d9; + margin-top: 35px; +} + +.table thead { + background-color: #9fc51d; + color: white; +} + +.button { + background: none; + justify-content: center; + align-items: center; + border: none; + border-radius: 6px; + padding: 5px 5px 2px 5px; + cursor: pointer; + font-size: 20px; + margin-top: 5px; +} + +.button:hover { + background-color: #e3e3e3; +} + +.addIcon { + font-size: 15px; + margin-right: 5px; +} + +.searchIcon { + font-size: 18px; + margin-right: 5px; + transform: scaleX(-1); +} diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 6716e7c3..c41edcb3 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState, useContext } from "react"; -import { PrimaryButton, IconButton, Spinner, Dialog, DialogContent, Label, Dropdown, DefaultButton, MessageBar } from "@fluentui/react"; +import { PrimaryButton, Spinner, Dialog, DialogContent, Label, Dropdown, DefaultButton, MessageBar } from "@fluentui/react"; import { ToastContainer, toast } from "react-toastify"; import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; -import { AddFilled } from "@fluentui/react-icons"; +import { AddFilled, DeleteRegular, EditRegular, SearchRegular } from "@fluentui/react-icons"; import { AppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; @@ -115,7 +115,7 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; modalProps={{ isBlocking: true, onDismiss: onDismiss, - styles: { main: { maxWidth: 450 } } + styles: { main: { maxWidth: 450, borderRadius: "6px" } } }} > {loading && ( @@ -133,7 +133,8 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; style={{ display: "flex", justifyContent: "center", - gap: "10px" + gap: "10px", + marginBottom: "10px" }} >
- + {errorMessage && {errorMessage}}
- + { +export const DeleteUserDialog = ({ isOpen, onDismiss, onConfirm }: { isOpen: boolean; onDismiss: any; onConfirm: any }) => { return ( { - onConfirm() + onClick={() => { + onConfirm(); }} text="Delete user" /> @@ -361,21 +380,8 @@ const Admin = () => { {user.role !== "admin" &&

Access denied

} {user.role === "admin" && ( <> -
-
- -
-
-

Roles and access

+

Roles and access

{ }} > { color: "white" } }} - text="Create user" onClick={() => { setIsOpen(true); }} - /> + > + + Create user + { setSearch(newValue || ""); }} + iconProps={{ + iconName: "Search", + children: + }} />
@@ -444,37 +469,25 @@ const Admin = () => { }} /> ) : ( - - - - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - <> +
+
- Name - EmailRoleActions
+ + + + + + + + + + {filteredUsers.map((user: any, index) => { + return ( {
{
- - ); - })} - -
+ Name + EmailRoleActions
{
- {}} - /> - { - setSelectedUser(user); - setIsDeleting(true); - }} - /> + +
}
+ ); + })} + + +
)} )} From a36ff82a0359a65f924c1705470ac5f7e3f6c97f Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Wed, 28 Aug 2024 20:10:40 -0400 Subject: [PATCH 144/820] SFC-fix-user-info --- frontend/src/providers/AppProviders.tsx | 110 ++++++++++++------------ 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index e98ef51e..eeebde7d 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction } from "react"; +import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction, useEffect } from "react"; import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; import { Spinner } from "@fluentui/react"; import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, getUserInfo, checkUser, getOrganizationSubscription } from "../api"; @@ -155,75 +155,73 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => const [loading, setLoading] = useState(true); - if (!user.organizationId) { - const getUserInfoList = async () => { - if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { - const userInfoList = await getUserInfo(); - if (userInfoList.length === 0) { - // setShowAuthMessage(true); - console.log("No user info found. Using anonymous user.", userInfoList); - } else { - // setShowAuthMessage(false); - console.log("User info found.", userInfoList); + useEffect(() => { + if (!user.organizationId) { + const getUserInfoList = async () => { + if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { + const userInfoList = await getUserInfo(); + if (userInfoList.length === 0) { + // setShowAuthMessage(true); + console.log("No user info found. Using anonymous user.", userInfoList); + } else { + // setShowAuthMessage(false); + console.log("User info found.", userInfoList); - const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; - const keyName = "name"; - const keyEmail = "emails"; + const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; + const keyName = "name"; + const keyEmail = "emails"; - const _user = userInfoList.find(obj => { - const _id = obj?.user_claims?.some(claim => claim.typ === keyId); - const _name = obj?.user_claims?.some(claim => claim.typ === keyName); - return _id && _name; - }); + const _user = userInfoList.find(obj => { + const _id = obj?.user_claims?.some(claim => claim.typ === keyId); + const _name = obj?.user_claims?.some(claim => claim.typ === keyName); + return _id && _name; + }); - if (_user) { - const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; - const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; - const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; + if (_user) { + const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; + const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; + const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; - if (id && name) { - setUser({ id, name, email, role: undefined, organizationId: undefined }); - } + if (id && name) { + setUser({ id, name, email, role: undefined, organizationId: undefined }); + } - // register user if doesn't exist + // register user if doesn't exist - // const response = await getUsers({ user: { id, name, email } }); // to get all users + // const response = await getUsers({ user: { id, name, email } }); // to get all users - // verifies if user exists and assigns the role - const result = await checkUser({ user: { id, name, email } }); - const role = result["role"] || undefined; - const organizationId = result["organizationId"] || undefined; + // verifies if user exists and assigns the role + const result = await checkUser({ user: { id, name, email } }); + const role = result["role"] || undefined; + const organizationId = result["organizationId"] || undefined; - const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); + const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); - if (result && role) { - setUser({ id, name, email, role, organizationId }); - } - if (organization) { - setOrganization({ - id: organization.id, - name: organization.name, - owner: organization.owner, - subscriptionId: organization.subscriptionId, - subscriptionStatus: organization.subscriptionStatus, - subscriptionExpirationDate: organization.subscriptionExpirationDate - }); - } - if (loading) { + if (result && role) { + setUser({ id, name, email, role, organizationId }); + } + if (organization) { + setOrganization({ + id: organization.id, + name: organization.name, + owner: organization.owner, + subscriptionId: organization.subscriptionId, + subscriptionStatus: organization.subscriptionStatus, + subscriptionExpirationDate: organization.subscriptionExpirationDate + }); + } setLoading(false); } } - } - } else { - console.log("Local"); - setUser({...user, organizationId: "test-organization"}) - if (loading) { + } else { + console.log("Local"); + setUser({ ...user, organizationId: "test-organization" }); setLoading(false); } - } - }; - getUserInfoList(); - } + }; + getUserInfoList(); + } + }, []); if (loading) return ( From bb8f923f68d54aa29ee444f053c9e081fd3cba89 Mon Sep 17 00:00:00 2001 From: fabiolayerim Date: Thu, 29 Aug 2024 02:47:20 -0400 Subject: [PATCH 145/820] SFC-382 create organization form base --- .../pages/onboarding/Onboarding.module.css | 101 ++++++++++++++++++ frontend/src/pages/onboarding/Onboarding.tsx | 64 +++++++++-- 2 files changed, 159 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/onboarding/Onboarding.module.css b/frontend/src/pages/onboarding/Onboarding.module.css index e69de29b..f3811a21 100644 --- a/frontend/src/pages/onboarding/Onboarding.module.css +++ b/frontend/src/pages/onboarding/Onboarding.module.css @@ -0,0 +1,101 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #f5f5f5; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + max-width: 400px; + height: 45%; + padding: 20px; + background-color: white; + border-radius: 6px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; +} + +.logo img { + max-width: 50%; + height: auto; + transition: max-width 0.3s ease; +} + +.carousel { + transition: transform 0.5s ease-in-out; +} + +.carousel-enter { + transform: translateX(100%); +} + +.carousel-exit { + transform: translateX(-100%); +} + +.containerStep { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-evenly; + height: 80%; +} + +.organization { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; +} + +.buttonContainer { + width: 90%; + justify-content: space-between; + display: flex; +} + +.singleButtonContainer { + justify-content: flex-end; +} + +.button { + display: flex; + background-color: #9fc51d; + border-color: #9fc51d; + color: white; + border-radius: 6px; + padding: 5px 0px; + align-items: center; + justify-content: center; + width: 30%; +} + +.button:hover { + background-color: #8ab31f; + border-color: #8ab31f; +} + +.button:active { + background-color: #7ea91f; + border-color: #7ea91f; +} + +.icon { + font-size: 20px; +} diff --git a/frontend/src/pages/onboarding/Onboarding.tsx b/frontend/src/pages/onboarding/Onboarding.tsx index 79bb9871..e77cd007 100644 --- a/frontend/src/pages/onboarding/Onboarding.tsx +++ b/frontend/src/pages/onboarding/Onboarding.tsx @@ -1,18 +1,70 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; +import salesLogo from "../../img/logo.png"; +import styles from "./Onboarding.module.css"; +import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, Globe48Regular } from "@fluentui/react-icons"; const Onboarding: React.FC = () => { - const [organization, setOrganization] = useState(''); + const [organization, setOrganization] = useState(""); + const [step, setStep] = useState(0); + const maxSteps = 2; const handleOrganizationChange = (event: React.ChangeEvent) => { setOrganization(event.target.value); }; + const handleNextClick = () => { + if (step < maxSteps) { + setStep(prevStep => prevStep + 1); + } + }; + + const handlePreviousClick = () => { + if (step > 0) { + setStep(prevStep => prevStep - 1); + } + }; + return ( -
-

Welcome to Freddaid!

-

Let's get started with onboarding.

+
+
+ {step === 0 && ( +
+
+ Sales Factory logo +
+

Welcome to Freddaid!

+

Let's get started with onboarding.

+
+ )} + {step === 1 && ( +
+
+ +

Before we begin, let's create an organization for your new account.

+ {}} placeholder="Organization Name" /> +
+
+ )} + {step === 2 && ( +
+

Get a subscription

+
+ )} +
0 ? styles.buttonContainer : `${styles.buttonContainer} ${styles.singleButtonContainer}`}> + {step > 0 && ( + + )} + {step < maxSteps && ( + + )} +
+
); }; -export default Onboarding; \ No newline at end of file +export default Onboarding; From 254bd5a8a6ef982ffdb2992649db211dd5b34d92 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 29 Aug 2024 16:21:28 -0400 Subject: [PATCH 146/820] SFC-fix-admin-panel --- frontend/src/pages/admin/Admin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 6716e7c3..3b914ec4 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -317,7 +317,7 @@ const Admin = () => { useEffect(() => { const getUserList = async () => { - let usersList = await getUsers({}); + let usersList = await getUsers({user: {id: user.id, name: user.name, organizationId: user.organizationId}}); if (!Array.isArray(usersList)) { usersList = []; } From 6620fbf5d58f403d1a7f1b0225c7043100ce8055 Mon Sep 17 00:00:00 2001 From: fabiolayerim Date: Thu, 29 Aug 2024 20:40:21 -0400 Subject: [PATCH 147/820] SFC-382 update content --- .../pages/onboarding/Onboarding.module.css | 31 +++++++++++++++---- frontend/src/pages/onboarding/Onboarding.tsx | 20 +++++++----- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/onboarding/Onboarding.module.css b/frontend/src/pages/onboarding/Onboarding.module.css index f3811a21..5bf1dbdd 100644 --- a/frontend/src/pages/onboarding/Onboarding.module.css +++ b/frontend/src/pages/onboarding/Onboarding.module.css @@ -53,15 +53,14 @@ align-items: center; justify-content: space-evenly; height: 80%; + text-align: center; } -.organization { +.input { display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - width: 100%; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; } .buttonContainer { @@ -99,3 +98,23 @@ .icon { font-size: 20px; } + +.buttonPrev { + display: flex; + background-color: #f5f5f5; + border-color: #e3e3e3; + color: black; + border-radius: 6px; + padding: 5px 0px; + align-items: center; + justify-content: center; + width: 30%; +} + +.buttonPrev:hover { + background-color: #e3e3e3; +} + +.iconMoney { + font-size: 48px; +} diff --git a/frontend/src/pages/onboarding/Onboarding.tsx b/frontend/src/pages/onboarding/Onboarding.tsx index e77cd007..b40b1d45 100644 --- a/frontend/src/pages/onboarding/Onboarding.tsx +++ b/frontend/src/pages/onboarding/Onboarding.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import salesLogo from "../../img/logo.png"; import styles from "./Onboarding.module.css"; -import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, Globe48Regular } from "@fluentui/react-icons"; +import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, MoneySettingsRegular } from "@fluentui/react-icons"; const Onboarding: React.FC = () => { const [organization, setOrganization] = useState(""); @@ -24,6 +24,10 @@ const Onboarding: React.FC = () => { } }; + const handleSubscriptionRedirect = () => { + window.location.href = "#/payment"; + }; + return (
@@ -38,21 +42,23 @@ const Onboarding: React.FC = () => { )} {step === 1 && (
-
- -

Before we begin, let's create an organization for your new account.

- {}} placeholder="Organization Name" /> -
+ +

Before we begin, let's create an organization for your new account.

+
)} {step === 2 && (
+

Get a subscription

+
)}
0 ? styles.buttonContainer : `${styles.buttonContainer} ${styles.singleButtonContainer}`}> {step > 0 && ( - )} From 838056cb9807ef22974aa7b92a1c182398aafcca Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Fri, 30 Aug 2024 12:37:29 -0400 Subject: [PATCH 148/820] SFC-376-add-organization-creation-step --- backend/app.py | 66 +++++++++++++++++-- frontend/src/api/api.ts | 20 ++++++ .../PaymentGateway/PaymentGateway.tsx | 1 - frontend/src/index.tsx | 6 +- frontend/src/pages/chat/Chat.tsx | 19 ------ frontend/src/pages/onboarding/Onboarding.tsx | 43 ++++++++++-- frontend/src/providers/AppProviders.tsx | 1 + 7 files changed, 123 insertions(+), 33 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9fff4206..3e2db599 100644 --- a/backend/app.py +++ b/backend/app.py @@ -266,7 +266,7 @@ def create_checkout_session(): "label": {"type": "custom", "custom": "Organization Name"}, "type": "text", "text": {"minimum_length": 5, "maximum_length": 100}, - }, + } if organizationId == "" else {} ], ) except Exception as e: @@ -630,7 +630,9 @@ def getUsers(): organizationId = request.args.get("organizationId") url = CHECK_USER_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", url, headers=headers, params={"organizationId": organizationId}) + response = requests.request( + "GET", url, headers=headers, params={"organizationId": organizationId} + ) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: @@ -666,7 +668,9 @@ def deleteUser(): userId = request.args.get("userId") url = CHECK_USER_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("DELETE", url, headers=headers,params={"id": userId}) + response = requests.request( + "DELETE", url, headers=headers, params={"id": userId} + ) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: @@ -827,7 +831,13 @@ def createInvitation(): role = request.json["role"] url = INVITATIONS_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - payload = json.dumps({"invited_user_email": invitedUserEmail, "organization_id": organizationId, "role": role}) + payload = json.dumps( + { + "invited_user_email": invitedUserEmail, + "organization_id": organizationId, + "role": role, + } + ) response = requests.request("POST", url, headers=headers, data=payload) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text @@ -925,6 +935,54 @@ def getOrganization(): return jsonify({"error": str(e)}), 500 +@app.route("/api/create-organization", methods=["POST"]) +def createOrganization(): + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + + if not client_principal_id: + return ( + jsonify( + { + "error": "Missing required parameters, client_principal_id or client_principal_name" + } + ), + 400, + ) + + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = "orchestrator-host--subscriptions" + functionKey = get_secret(keySecretName) + except Exception as e: + logging.exception( + "[webbackend] exception in /api/orchestrator-host--subscriptions" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + try: + organizationName = request.json["organizationName"] + payload = json.dumps( + { + "id": client_principal_id, + "organizationName": organizationName, + } + ) + url = SUBSCRIPTION_ENDPOINT + headers = {"Content-Type": "application/json", "x-functions-key": functionKey} + response = requests.request("POST", url, headers=headers, data=payload) + logging.info(f"[webbackend] response: {response.text[:500]}...") + return response.text + except Exception as e: + logging.exception("[webbackend] exception in /post-organization") + return jsonify({"error": str(e)}), 500 + @app.route("/api/getUser", methods=["GET"]) def getUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index d43d6b9e..7573dc74 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -421,3 +421,23 @@ export async function getOrganizationSubscription({userId, organizationId} : any return subscription; } + +export const createOrganization = async ({ userId, organizationName }: any) => { + const response = await fetch("/api/create-organization", { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId, + }, + body: JSON.stringify({ + organizationName + }) + }); + + if (response.status > 299 || !response.ok) { + throw Error("Error creating organization"); + } + + const organization = await response.json(); + return organization; +}; diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 183dada6..ad6309a4 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -37,7 +37,6 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise cancelUrl: window.location.origin + "/", organizationId: user.organizationId || "" }); - console.log(url); window.location.href = url; }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 365d31cc..08752cd3 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -21,19 +21,19 @@ import { AppContext } from "./providers/AppProviders"; initializeIcons(); export default function App() { - const { user } = useContext(AppContext); + const { organization } = useContext(AppContext); return ( - {!user.organizationId && ( + {!organization.subscriptionId &&( <> } /> } /> } /> )} - {user.organizationId && ( + {organization.subscriptionId && ( <> }> } /> diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 3a5d10eb..3ddb6cc9 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -365,25 +365,6 @@ const Chat = () => { //If I add this on a useEffect it doesn't work, I don't know why //maybe because it's a global event listener and is called multiple times - if (organization.subscriptionStatus === "inactive"){ - if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { - return ( -
-

We are sorry, but your user is not assigned to any active subscription.

-

Contact the administrator of the organization to which you belong.

-
- ); - } - } - return (
diff --git a/frontend/src/pages/onboarding/Onboarding.tsx b/frontend/src/pages/onboarding/Onboarding.tsx index b40b1d45..e2613f2d 100644 --- a/frontend/src/pages/onboarding/Onboarding.tsx +++ b/frontend/src/pages/onboarding/Onboarding.tsx @@ -1,20 +1,40 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import salesLogo from "../../img/logo.png"; import styles from "./Onboarding.module.css"; import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, MoneySettingsRegular } from "@fluentui/react-icons"; +import { Spinner } from "@fluentui/react"; + +import { createOrganization, getOrganizationSubscription } from "../../api"; +import { AppContext } from "../../providers/AppProviders"; const Onboarding: React.FC = () => { - const [organization, setOrganization] = useState(""); + const { user, setUser, organization, setOrganization } = useContext(AppContext); + + const [organizationName, setOrganizationName] = useState(""); const [step, setStep] = useState(0); + const [isLoadingStep, setIsLoadingStep] = useState(false); const maxSteps = 2; const handleOrganizationChange = (event: React.ChangeEvent) => { - setOrganization(event.target.value); + setOrganizationName(event.target.value); }; - const handleNextClick = () => { + const handleCreateOrganization = async () => { + const newOrganization = await createOrganization({ userId: user.id, organizationName: organizationName }); + if (newOrganization.id) { + setOrganization(newOrganization); + setUser({ ...user, organizationId: newOrganization.id }); + } + }; + + const handleNextClick = async () => { if (step < maxSteps) { + setIsLoadingStep(true); + if (step === 1) { + await handleCreateOrganization(); + } setStep(prevStep => prevStep + 1); + setIsLoadingStep(false); } }; @@ -43,8 +63,14 @@ const Onboarding: React.FC = () => { {step === 1 && (
-

Before we begin, let's create an organization for your new account.

- +

Before we begin, let's create an organizationName for your new account.

+
)} {step === 2 && ( @@ -56,6 +82,11 @@ const Onboarding: React.FC = () => {
)} + {isLoadingStep && ( +
+ +
+ )}
0 ? styles.buttonContainer : `${styles.buttonContainer} ${styles.singleButtonContainer}`}> {step > 0 && (
); }; diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index f2b69b29..04d17bf7 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,4 +1,6 @@ import React, { useContext, useEffect, useState } from "react"; +import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react"; + import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; @@ -8,15 +10,14 @@ import github from "../../assets/github.svg"; import styles from "./Layout.module.css"; import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; import { FeedbackRatingButton } from "../../components/FeedbackRating/FeedbackRatingButton"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { SettingsButton } from "../../components/SettingsButton"; import { ButtonPaymentGateway } from "../../components/PaymentGateway/ButtonPaymentGateway"; import { SideMenu } from "../../components/SideMenu/SideMenu"; import { ProfileButton } from "../../components/Profile"; const Layout = () => { - const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = - useContext(AppContext); + const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); const { pathname } = useLocation(); const [isCollapsed, setIsCollapsed] = useState(false); @@ -40,32 +41,39 @@ const Layout = () => { }; return ( -
- -
-
-
- -
- {pathname === "/" && ( - <> - - - - - )} -
- + <> + +
+ +
+
+
+ +
+ {pathname === "/" && ( + <> + + + + + )} +
+ +
+
-
-
-
+ - -
-
+ +
+
+ + +
Please sign-in to see your profile information.
+
+ ); }; diff --git a/frontend/src/pages/onboarding/Onboarding.tsx b/frontend/src/pages/onboarding/Onboarding.tsx index e2613f2d..cdba1230 100644 --- a/frontend/src/pages/onboarding/Onboarding.tsx +++ b/frontend/src/pages/onboarding/Onboarding.tsx @@ -1,38 +1,43 @@ -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; +import { Navigate } from "react-router-dom"; import salesLogo from "../../img/logo.png"; import styles from "./Onboarding.module.css"; import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, MoneySettingsRegular } from "@fluentui/react-icons"; import { Spinner } from "@fluentui/react"; import { createOrganization, getOrganizationSubscription } from "../../api"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; const Onboarding: React.FC = () => { - const { user, setUser, organization, setOrganization } = useContext(AppContext); - + const { user, setUser, organization, setOrganization } = useAppContext(); const [organizationName, setOrganizationName] = useState(""); const [step, setStep] = useState(0); const [isLoadingStep, setIsLoadingStep] = useState(false); const maxSteps = 2; - const handleOrganizationChange = (event: React.ChangeEvent) => { setOrganizationName(event.target.value); }; const handleCreateOrganization = async () => { + if (!user) { + return null; + } const newOrganization = await createOrganization({ userId: user.id, organizationName: organizationName }); if (newOrganization.id) { setOrganization(newOrganization); setUser({ ...user, organizationId: newOrganization.id }); + return newOrganization; } }; const handleNextClick = async () => { if (step < maxSteps) { setIsLoadingStep(true); + let organization = null; if (step === 1) { - await handleCreateOrganization(); + organization = await handleCreateOrganization(); } + setStep(prevStep => prevStep + 1); setIsLoadingStep(false); } @@ -48,6 +53,9 @@ const Onboarding: React.FC = () => { window.location.href = "#/payment"; }; + if (user?.organizationId && organization?.subscriptionId) { + return ; + } return (
diff --git a/frontend/src/pages/organization/Organization.tsx b/frontend/src/pages/organization/Organization.tsx index 99307060..03821623 100644 --- a/frontend/src/pages/organization/Organization.tsx +++ b/frontend/src/pages/organization/Organization.tsx @@ -1,61 +1,63 @@ import React, { useContext } from "react"; import { Label } from "@fluentui/react"; import { Globe32Regular } from "@fluentui/react-icons"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import styles from "./Organization.module.css"; const Organization = () => { - const { organization } = useContext(AppContext); - const { user } = useContext(AppContext); + const { organization } = useAppContext(); + + if (!organization) { + return ( +
+

No Organization

+
+ ); + } return (
- {user.role !== "admin" &&

Access denied

} - {user.role === "admin" && ( - <> -
-

Organization

+
+

Organization

+
+
+
+ +
+
+ + {organization?.id} +
+
+
+
+ + {organization?.name} +
+
+ + {organization?.owner} +
+
+
+
+ + {organization?.subscriptionId} +
-
-
- -
-
- - {organization.id} -
-
-
-
- - {organization.name} -
-
- - {organization.owner} -
-
-
-
- - {organization.subscriptionId} -
-
-
-
- - {organization.subscriptionStatus} -
-
- - {organization.subscriptionExpirationDate} -
-
+
+
+ + {organization?.subscriptionStatus} +
+
+ + {organization?.subscriptionExpirationDate}
- - )} +
+
); }; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 1a1d5237..61a25aa8 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,12 +1,14 @@ -import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction, useEffect } from "react"; -import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; +import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode, Dispatch, SetStateAction } from "react"; import { Spinner } from "@fluentui/react"; -import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, getUserInfo, checkUser, getOrganizationSubscription } from "../api"; +import { checkUser, getOrganizationSubscription } from "../api"; +import { useMsal } from "@azure/msal-react"; + +// Define interfaces for UserInfo and OrganizationInfo interface UserInfo { id: string; name: string; email: string | null; - role: string | undefined; + role?: string; organizationId?: string; } @@ -14,28 +16,46 @@ interface OrganizationInfo { id: string; name: string; owner: string; - subscriptionId: string | undefined; - subscriptionStatus: string | undefined; - subscriptionExpirationDate: number | undefined; + subscriptionId?: string; + subscriptionStatus?: string; + subscriptionExpirationDate?: number; } +// In types.ts +export interface ConversationHistoryItem { + id: string; + start_date: string; + content: string; + // Add other properties as needed +} + +export interface ChatTurn { + user: string; + bot?: { + message: string; + thoughts: any; + } | null; + // Add other properties as needed +} + +// Define the shape of the context interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; - newChatDeleted: boolean; - setNewChatDeleted: Dispatch>; showFeedbackRatingPanel: boolean; setShowFeedbackRatingPanel: Dispatch>; - refreshFetchHistorial: boolean; - setRefreshFetchHistorial: Dispatch>; + settingsPanel: boolean; + setSettingsPanel: Dispatch>; + refreshFetchHistory: boolean; + setRefreshFetchHistory: Dispatch>; chatIsCleaned: boolean; setChatIsCleaned: Dispatch>; dataHistory: ConversationHistoryItem[]; setDataHistory: Dispatch>; - user: UserInfo; - setUser: Dispatch>; - organization: OrganizationInfo; - setOrganization: Dispatch>; + user: UserInfo | null; + setUser: Dispatch>; + organization: OrganizationInfo | null; + setOrganization: Dispatch>; chatSelected: string; setChatSelected: Dispatch>; chatId: string; @@ -44,187 +64,197 @@ interface AppContextType { setDataConversation: Dispatch>; conversationIsLoading: boolean; setConversationIsLoading: Dispatch>; - settingsPanel: boolean; - setSettingsPanel: Dispatch>; + newChatDeleted: boolean; + setNewChatDeleted: Dispatch>; } -export const AppContext = createContext({ - showHistoryPanel: true, - setShowHistoryPanel: () => {}, - showFeedbackRatingPanel: false, - setShowFeedbackRatingPanel: () => {}, - dataHistory: [], - setDataHistory: () => {}, - user: { - id: "00000000-0000-0000-0000-000000000000", - name: "anonymous", - email: "anonymous@gmail.com", - role: undefined, - organizationId: undefined - }, - setUser: () => {}, - organization: { - id: "00000000-0000-0000-0000-000000000000", - name: "no-organization", - owner: "no-owner", - subscriptionId: undefined, - subscriptionStatus: undefined, - subscriptionExpirationDate: undefined - }, - setOrganization: () => {}, - dataConversation: [], - setDataConversation: () => {}, - chatId: "", - setChatId: () => {}, - conversationIsLoading: false, - setConversationIsLoading: () => {}, - refreshFetchHistorial: false, - setRefreshFetchHistorial: () => {}, - chatSelected: "", - setChatSelected: () => {}, - chatIsCleaned: false, - setChatIsCleaned: () => {}, - settingsPanel: false, - setSettingsPanel: () => {}, - newChatDeleted: false, - setNewChatDeleted: () => {} -}); +// Create the context with a default value +export const AppContext = createContext(undefined); -export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { +// Create the provider component +export const AppProvider: React.FC<{ children: ReactNode; activeAccount: any }> = ({ children, activeAccount }) => { + // State variables const [showHistoryPanel, setShowHistoryPanel] = useState(true); const [showFeedbackRatingPanel, setShowFeedbackRatingPanel] = useState(false); - const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); + const [settingsPanel, setSettingsPanel] = useState(false); + const [refreshFetchHistory, setRefreshFetchHistory] = useState(false); + const [chatIsCleaned, setChatIsCleaned] = useState(false); const [dataHistory, setDataHistory] = useState([]); const [dataConversation, setDataConversation] = useState([]); - const [user, setUser] = useState({ - id: "00000000-0000-0000-0000-000000000000", - name: "anonymous", - email: "anonymous@gmail.com", - role: undefined, - organizationId: undefined - }); - const [organization, setOrganization] = useState({ - id: "00000000-0000-0000-0000-000000000000", - name: "no-organization", - owner: "no-owner", - subscriptionId: undefined, - subscriptionStatus: undefined, - subscriptionExpirationDate: undefined - }); + const [chatSelected, setChatSelected] = useState(""); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); - const [chatIsCleaned, setChatIsCleaned] = useState(false); - const [chatSelected, setChatSelected] = useState(""); - const [settingsPanel, setSettingsPanel] = useState(false); - const [newChatDeleted, setNewChatDeleted] = useState(false); + const [newChatDeleted, setNewChatDeleted] = useState(false); + const [user, setUser] = useState(null); + const [organization, setOrganization] = useState(null); + const [loading, setLoading] = useState(true); - const handleKeyDown = (event: KeyboardEvent) => { - const isCtrlOrCmd = event.ctrlKey || event.metaKey; - const isAlt = event.altKey; + // MSAL instance and active account - if (event.code === "Digit8" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - setShowFeedbackRatingPanel(!showFeedbackRatingPanel); - setSettingsPanel(false); - setShowHistoryPanel(false); - } else if (event.code === "Period" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - setShowHistoryPanel(!showHistoryPanel); - setShowFeedbackRatingPanel(false); - setSettingsPanel(false); - } else if (event.code === "Comma" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - setSettingsPanel(!settingsPanel); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - } else if (event.code === "Digit0" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; - } else if (event.code === "Digit9" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - window.location.href = "#/admin"; - } else if (event.code === "Digit7" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - window.location.href = "#/payment"; - } - }; + // Handle keyboard shortcuts + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isAlt = event.altKey; - window.addEventListener("keydown", handleKeyDown); - //If I add this on a useEffect it doesn't work, I don't know why - //maybe because it's a global event listener and is called multiple times + if (isCtrlOrCmd && isAlt) { + switch (event.code) { + case "Digit8": + event.preventDefault(); + setShowFeedbackRatingPanel(prev => !prev); + setSettingsPanel(false); + setShowHistoryPanel(false); + break; + case "Period": + event.preventDefault(); + setShowHistoryPanel(prev => !prev); + setShowFeedbackRatingPanel(false); + setSettingsPanel(false); + break; + case "Comma": + event.preventDefault(); + setSettingsPanel(prev => !prev); + setShowHistoryPanel(false); + setShowFeedbackRatingPanel(false); + break; + case "Digit0": + event.preventDefault(); + window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; + break; + case "Digit9": + event.preventDefault(); + window.location.href = "#/admin"; + break; + case "Digit7": + event.preventDefault(); + window.location.href = "#/payment"; + break; + default: + break; + } + } + }, + [setShowFeedbackRatingPanel, setSettingsPanel, setShowHistoryPanel] + ); - const [loading, setLoading] = useState(true); + // Add event listener for keyboard shortcuts + useEffect(() => { + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handleKeyDown]); + // Fetch user and organization info useEffect(() => { - if (!user.organizationId) { - const getUserInfoList = async () => { - if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { - const userInfoList = await getUserInfo(); - if (userInfoList.length === 0) { - // setShowAuthMessage(true); - console.log("No user info found. Using anonymous user.", userInfoList); - } else { - // setShowAuthMessage(false); - console.log("User info found.", userInfoList); + let isMounted = true; + const fetchUserInfo = async () => { + try { + if (activeAccount) { + const id = activeAccount.localAccountId; + const name = activeAccount.name || "User"; + const email = activeAccount.idTokenClaims?.emails?.[0] || activeAccount.username || null; + // Check if user exists and get role and organizationId + const result = await checkUser({ user: { id, name, email } }); + const role = result.role; + const organizationId = result.organizationId; - const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; - const keyName = "name"; - const keyEmail = "emails"; + // Set user state + if (isMounted) { + setUser({ id, name, email, role, organizationId }); + } - const _user = userInfoList.find(obj => { - const _id = obj?.user_claims?.some(claim => claim.typ === keyId); - const _name = obj?.user_claims?.some(claim => claim.typ === keyName); - return _id && _name; + // Get organization info + if (organizationId) { + const organizationData = await getOrganizationSubscription({ + userId: id, + organizationId }); - if (_user) { - const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; - const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; - const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; - - if (id && name) { - setUser({ id, name, email, role: undefined, organizationId: undefined }); - } - - // register user if doesn't exist - - // const response = await getUsers({ user: { id, name, email } }); // to get all users - - // verifies if user exists and assigns the role - const result = await checkUser({ user: { id, name, email } }); - const role = result["role"] || undefined; - const organizationId = result["organizationId"] || undefined; - - const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); - - if (result && role) { - setUser({ id, name, email, role, organizationId }); - } - if (organization) { - setOrganization({ - id: organization.id, - name: organization.name, - owner: organization.owner, - subscriptionId: organization.subscriptionId, - subscriptionStatus: organization.subscriptionStatus, - subscriptionExpirationDate: organization.subscriptionExpirationDate - }); - } - setLoading(false); + if (isMounted && organizationData) { + setOrganization({ + id: organizationData.id, + name: organizationData.name, + owner: organizationData.owner, + subscriptionId: organizationData.subscriptionId, + subscriptionStatus: organizationData.subscriptionStatus, + subscriptionExpirationDate: organizationData.subscriptionExpirationDate + }); } } } else { - console.log("Local"); - setUser({ ...user, organizationId: "test-organization" }); - setOrganization({ ...organization, subscriptionId: "test-subscriptionId" }); + // No active account, leave user and organization as null + if (isMounted) { + setUser(null); + setOrganization(null); + } + } + } catch (error) { + console.error("Error fetching user info:", error); + // Handle errors as needed + } finally { + if (isMounted) { setLoading(false); } - }; - getUserInfoList(); - } - }, []); + } + }; + + fetchUserInfo(); + + return () => { + isMounted = false; + }; + }, [activeAccount]); + + // Memoize context value to prevent unnecessary re-renders + const contextValue = useMemo( + () => ({ + showHistoryPanel, + setShowHistoryPanel, + showFeedbackRatingPanel, + setShowFeedbackRatingPanel, + settingsPanel, + setSettingsPanel, + refreshFetchHistory, + setRefreshFetchHistory, + chatIsCleaned, + setChatIsCleaned, + dataHistory, + setDataHistory, + user, + setUser, + organization, + setOrganization, + chatSelected, + setChatSelected, + chatId, + setChatId, + dataConversation, + setDataConversation, + conversationIsLoading, + setConversationIsLoading, + newChatDeleted, + setNewChatDeleted + }), + [ + showHistoryPanel, + showFeedbackRatingPanel, + settingsPanel, + refreshFetchHistory, + chatIsCleaned, + dataHistory, + user, + organization, + chatSelected, + chatId, + dataConversation, + conversationIsLoading, + newChatDeleted + ] + ); - if (loading) + // Render loading spinner if data is still loading + if (loading) { return (
= ({ children }) =>
); + } - return ( - - {children} - - ); + // Provide the context to child components + return {children}; +}; + +// Custom hook for consuming the context +export const useAppContext = () => { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error("useAppContext must be used within an AppProvider"); + } + return context; }; diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index e42a9011..c15cf77b 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -1,18 +1,60 @@ -import React, { useContext, ReactNode } from 'react'; -import { AppContext } from "../providers/AppProviders"; -import { Navigate } from 'react-router-dom'; +// src/ProtectedRoute.jsx +import React, { useContext } from "react"; +import { Outlet, Navigate } from "react-router-dom"; +import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react"; +import { useAppContext } from "../providers/AppProviders"; +import { InteractionType } from "@azure/msal-browser"; +import { loginRequest } from "../authConfig"; +import LoadingSpinner from "../components/LoadingSpinner/LoadingSpinner"; // Optional: Create a loading spinner component + +/** + * ProtectedRoute component ensures that only authenticated users with allowed roles can access certain routes. + * It uses MsalAuthenticationTemplate to handle authentication automatically. + * + * @param {Array} allowedRoles - Array of roles that are permitted to access the route. + */ interface ProtectedRouteProps { - children: ReactNode; - allowedRoles: string[]; + allowedRoles: string[]; } -const ProtectedRoute = ({ children, allowedRoles }: ProtectedRouteProps) => { - const { user } = useContext(AppContext); - if (!user.role || !allowedRoles.includes(user.role)) { - return ; - } - return (<>{children}); +const ProtectedRoute: React.FC = ({ allowedRoles }) => { + const { instance } = useMsal(); + const activeAccount = instance.getActiveAccount(); + const { user, organization } = useAppContext(); + const subscriptionId = organization?.subscriptionId + const hasActiveAccount = (): boolean => { + if (!activeAccount) { + return false; + } + return true; + }; + // Function to check if the user has at least one of the allowed roles + const hasRequiredRole = (): boolean => { + const roles = [user?.role]; + //const roles = activeAccount.idTokenClaims?.roles as string[] | undefined; + if (!roles) return false; + return allowedRoles.some(role => roles.includes(role)); + }; + + const hasRequiredSubscriptionID = (): boolean => { + if(!subscriptionId) return false; + return true; + } + + return ( + + { hasActiveAccount() && hasRequiredSubscriptionID() && hasRequiredRole() ? ( + + ) : hasActiveAccount() === false ? ( + + ) : hasActiveAccount() === true && hasRequiredSubscriptionID() === false ? ( + + ) : ( + + )} + + ); }; export default ProtectedRoute; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d711f0f9..77822aac 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,12 +11,30 @@ export default defineConfig({ }, server: { proxy: { - '/chatgpt': "http://localhost:8000", - '/api/get-speech-token': "http://localhost:8000", - '/api/get-storage-account': "http://localhost:8000", - '/api/get-blob': "http://localhost:8000", - '/api/settings': "http://localhost:8000", - '/api/conversations': "http://localhost:8000", + "/chatgpt": "http://localhost:8000", + "/api/stripe": "http://localhost:8000", + "/api/chat-history": "http://localhost:8000", + "/api/chat-conversation/": "http://localhost:8000", + "/api/chat-conversations/": "http://localhost:8000", + "/api/create-organization": "http://localhost:8000", + "/api/get-speech-token": "http://localhost:8000", + "/api/get-storage-account": "http://localhost:8000", + "/create-checkout-session": "http://localhost:8000", + "/webhook": "http://localhost:8000", + "/api/upload-blob": "http://localhost:8000", + "/api/get-blob": "http://localhost:8000", + "/api/settings": "http://localhost:8000", + "/api/feedback": "http://localhost:8000", + "/api/getusers": "http://localhost:8000", + "/api/deteleuser": "http://localhost:8000", + "/api/inviteUser": "http://localhost:8000", + "/api/getInvitations": "http://localhost:8000", + "/api/createInvitation": "http://localhost:8000", + "/api/checkuser": "http://localhost:8000", + "/api/get-organization-subscription": "http://localhost:8000", + "/api/getUser": "http://localhost:8000", + "/api/conversations": "http://localhost:8000", + "/api/chat": "http://localhost:8000" }, host: true } From 1b1b161c2727a9181cca9928952aff29063a05e7 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 25 Oct 2024 12:36:17 -0400 Subject: [PATCH 160/820] Revert "FA-87-SETUP-MSAL (#133)" (#134) This reverts commit 18ed7350a56220e2081f91cfa91e9a8eddb25b14. --- backend/app.py | 25 +- frontend/package-lock.json | 36 -- frontend/package.json | 2 - frontend/src/App.tsx | 163 ------- frontend/src/api/api.ts | 14 +- frontend/src/authConfig.ts | 74 ---- .../ChatHistoryButton/ChatHistoryButton.tsx | 4 +- .../FeedbackRating/FeedbackRating.tsx | 12 +- .../FeedbackRating/FeedbackRatingButton.tsx | 4 +- .../HistoryPannel/ChatHistoryListItem.tsx | 118 ++--- .../HistoryPannel/ChatHistoryPanel.tsx | 4 +- .../LoadingSpinner/LoadingSpinner.tsx | 11 - .../PaymentGateway/ButtonPaymentGateway.tsx | 28 +- .../PaymentGateway/PaymentGateway.tsx | 26 +- frontend/src/components/Profile/index.tsx | 11 +- .../QuestionInput/QuestionInput.tsx | 19 +- .../src/components/SettingsPanel/index.tsx | 46 +- frontend/src/components/SideMenu/SideMenu.tsx | 12 +- frontend/src/index.tsx | 137 ++++-- frontend/src/pages/Login/Login.tsx | 19 - frontend/src/pages/admin/Admin.tsx | 123 ++---- frontend/src/pages/chat/Chat.tsx | 24 +- .../src/pages/invitations/Invitations.tsx | 244 +++++----- frontend/src/pages/layout/Layout.tsx | 62 ++- frontend/src/pages/onboarding/Onboarding.tsx | 20 +- .../src/pages/organization/Organization.tsx | 92 ++-- frontend/src/providers/AppProviders.tsx | 418 +++++++++--------- frontend/src/router/ProtectedRoute.tsx | 64 +-- frontend/vite.config.ts | 30 +- 29 files changed, 669 insertions(+), 1173 deletions(-) delete mode 100644 frontend/src/App.tsx delete mode 100644 frontend/src/authConfig.ts delete mode 100644 frontend/src/components/LoadingSpinner/LoadingSpinner.tsx delete mode 100644 frontend/src/pages/Login/Login.tsx diff --git a/backend/app.py b/backend/app.py index 0ec6cdce..6e7726ac 100644 --- a/backend/app.py +++ b/backend/app.py @@ -262,16 +262,12 @@ def create_checkout_session(): cancel_url=cancel_url, automatic_tax={"enabled": True}, custom_fields=[ - ( - { - "key": "organization_name", - "label": {"type": "custom", "custom": "Organization Name"}, - "type": "text", - "text": {"minimum_length": 5, "maximum_length": 100}, - } - if organizationId == "" - else {} - ) + { + "key": "organization_name", + "label": {"type": "custom", "custom": "Organization Name"}, + "type": "text", + "text": {"minimum_length": 5, "maximum_length": 100}, + } if organizationId == "" else {} ], ) except Exception as e: @@ -806,7 +802,6 @@ def sendEmail(): logging.error("Something went wrong...", e) return jsonify({"error": str(e)}), 500 - @app.route("/api/getInvitations", methods=["GET"]) def getInvitations(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -834,16 +829,13 @@ def getInvitations(): organizationId = request.args.get("organizationId") url = INVITATIONS_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request( - "GET", url, headers=headers, params={"organizationId": organizationId} - ) + response = requests.request("GET", url, headers=headers,params={"organizationId": organizationId}) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: logging.exception("[webbackend] exception in /get-organization") return jsonify({"error": str(e)}), 500 - @app.route("/api/createInvitation", methods=["POST"]) def createInvitation(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -998,7 +990,7 @@ def createOrganization(): functionKey = get_secret(keySecretName) except Exception as e: logging.exception( - f"[webbackend] exception in /api/orchestrator-host--subscriptions {e}" + "[webbackend] exception in /api/orchestrator-host--subscriptions" ) return ( jsonify( @@ -1025,7 +1017,6 @@ def createOrganization(): logging.exception("[webbackend] exception in /post-organization") return jsonify({"error": str(e)}), 500 - @app.route("/api/getUser", methods=["GET"]) def getUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 68a1c0fc..68a25057 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,6 @@ "name": "frontend", "version": "1.0.0", "dependencies": { - "@azure/msal-browser": "^3.26.1", - "@azure/msal-react": "^2.1.1", "@cyntler/react-doc-viewer": "^1.14.1", "@fluentui/react": "^8.105.3", "@fluentui/react-icons": "^2.0.195", @@ -56,40 +54,6 @@ "node": ">=6.0.0" } }, - "node_modules/@azure/msal-browser": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.26.1.tgz", - "integrity": "sha512-y78sr9g61aCAH9fcLO1um+oHFXc1/5Ap88RIsUSuzkm0BHzFnN+PXGaQeuM1h5Qf5dTnWNOd6JqkskkMPAhh7Q==", - "license": "MIT", - "dependencies": { - "@azure/msal-common": "14.15.0" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "14.15.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", - "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-react": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-2.1.1.tgz", - "integrity": "sha512-XOBgAR0fbkfUUkQZyhIlwAZiy8WIzNWxAAFacJgyL8LiE2Y5pgM9var9X0Jh7Oz/1Oy5lhTQDtCYWSMHmTZ84Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@azure/msal-browser": "^3.25.0", - "react": "^16.8.0 || ^17 || ^18" - } - }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index d074fd40..16d81df4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,6 @@ "watch": "tsc && vite build --watch" }, "dependencies": { - "@azure/msal-browser": "^3.26.1", - "@azure/msal-react": "^2.1.1", "@cyntler/react-doc-viewer": "^1.14.1", "@fluentui/react": "^8.105.3", "@fluentui/react-icons": "^2.0.195", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx deleted file mode 100644 index 28a9ce39..00000000 --- a/frontend/src/App.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Routes, Route, useNavigate } from "react-router-dom"; -import ProtectedRoute from "./router/ProtectedRoute"; -import Layout from "./pages/layout/Layout"; -import NoPage from "./pages/NoPage"; -import AccessDenied from "./pages/AccesDenied"; -import Chat from "./pages/chat/Chat"; -import Admin from "./pages/admin/Admin"; -import Onboarding from "./pages/onboarding/Onboarding"; -import Invitations from "./pages/invitations/Invitations"; -import Organization from "./pages/organization/Organization"; -import Login from "./pages/Login/Login"; -import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; -import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; -import { MsalProvider, useMsal } from "@azure/msal-react"; -import { - EventType, - PublicClientApplication, - AccountInfo, - EventMessage, - AuthenticationResult, - NavigationClient, - NavigationOptions, - SsoSilentRequest -} from "@azure/msal-browser"; -import { b2cPolicies } from "./authConfig"; -import { AppProvider } from "./providers/AppProviders"; - -interface AppProps { - pca: PublicClientApplication; -} - -export default function App({ pca }: AppProps) { - return ( - - - - - - ); -} - -interface ClientSideNavigationProps { - pca: PublicClientApplication; - children: React.ReactNode; -} - -function ClientSideNavigation({ pca, children }: ClientSideNavigationProps) { - const navigate = useNavigate(); - const [firstRender, setFirstRender] = useState(true); - - useEffect(() => { - class CustomNavigationClient extends NavigationClient { - navigateInternal(url: string, options: NavigationOptions): Promise { - navigate(url); - return Promise.resolve(false); - } - } - - pca.setNavigationClient(new CustomNavigationClient()); - setFirstRender(false); - }, [pca, navigate]); - - if (firstRender) { - return null; - } - - return <>{children}; -} - -function Pages() { - const { instance, accounts, inProgress } = useMsal(); - const [status, setStatus] = useState(null); - - useEffect(() => { - const callbackId = instance.addEventCallback((event: EventMessage) => { - if ((event.eventType === EventType.LOGIN_SUCCESS || event.eventType === EventType.ACQUIRE_TOKEN_SUCCESS) && event.payload) { - const authResult = event.payload as AuthenticationResult; - const account = authResult.account; - const idTokenClaims = authResult.idTokenClaims as any; - - if (idTokenClaims["tfp"] === b2cPolicies.names.editProfile) { - const originalSignInAccount = instance - .getAllAccounts() - .find( - (accountItem: AccountInfo) => - accountItem.idTokenClaims?.oid === account.idTokenClaims?.oid && - accountItem.idTokenClaims?.sub === account.idTokenClaims?.sub && - accountItem.idTokenClaims?.tfp === b2cPolicies.names.signUpSignIn - ); - - if (originalSignInAccount) { - const signUpSignInFlowRequest: SsoSilentRequest = { - authority: b2cPolicies.authorities.signUpSignIn.authority, - account: originalSignInAccount - }; - - // Silently login again with the signUpSignIn policy - instance.ssoSilent(signUpSignInFlowRequest).catch(error => { - // Handle error - console.error(error); - }); - } - } - } - - if (event.eventType === EventType.SSO_SILENT_SUCCESS && event.payload) { - setStatus("ssoSilent success"); - } - }); - - return () => { - if (callbackId) { - instance.removeEventCallback(callbackId); - } - }; - }, [instance]); - const activeAccount = instance.getActiveAccount(); - if (inProgress !== "none") { - return
loading
; - } - - return ( - - - {/* Public Routes */} - } /> - } /> - } /> - - {/* Access Denied Route */} - } /> - - {/* Protected Routes for Authenticated Users (Regular and Admin) */} - }> - }> - {/* Regular User and Admin Routes */} - } /> - } /> - - - - {/* Protected Routes for Admin Only */} - }> - }> - } /> - } /> - } /> - - - - {/* Catch-All Route for Undefined Paths */} - } /> - - - ); -} - -// Define Organization interface based on your application's structure -interface Organization { - subscriptionId: string | null; - // Add other properties as needed -} diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 6584a05c..3de7f7b7 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -44,7 +44,7 @@ export async function deleteUser({ user, userId }: any): Promise { method: "DELETE", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": user.id + "X-MS-CLIENT-PRINCIPAL-ID": user.id, } }); const fetchedData = await response.json(); @@ -70,6 +70,7 @@ export async function checkUser({ user }: any): Promise { email: user.email }) }); + const parsedResponse = await response.json(); if (response.status > 299 || !response.ok) { throw Error("Unknown error in checkUser"); @@ -323,7 +324,7 @@ export async function inviteUser({ username, email, organizationId }: any): Prom return { error: error }; } } -export async function createInvitation({ organizationId, invitedUserEmail, userId, role }: any): Promise { +export async function createInvitation({organizationId, invitedUserEmail, userId, role} : any): Promise { try { const response = await fetch("/api/createInvitation", { method: "POST", @@ -385,7 +386,7 @@ export async function createCheckoutSession({ userId, priceId, successUrl, cance const response = await fetch("/create-checkout-session", { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify({ userId, @@ -428,8 +429,8 @@ export async function getOrganizationSubscription({userId, organizationId} : any method: "GET", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId - } + "X-MS-CLIENT-PRINCIPAL-ID": userId, + }, }); if (response.status > 299 || !response.ok) { @@ -438,6 +439,7 @@ export async function getOrganizationSubscription({userId, organizationId} : any const subscription = await response.json(); return subscription; + } export const createOrganization = async ({ userId, organizationName }: any) => { @@ -445,7 +447,7 @@ export const createOrganization = async ({ userId, organizationName }: any) => { method: "POST", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId + "X-MS-CLIENT-PRINCIPAL-ID": userId, }, body: JSON.stringify({ organizationName diff --git a/frontend/src/authConfig.ts b/frontend/src/authConfig.ts deleted file mode 100644 index 3847f71c..00000000 --- a/frontend/src/authConfig.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { LogLevel, Configuration } from "@azure/msal-browser"; - -const tenantName: string = "salesfactoryai2"; // Replace with your tenant name - -const policyNames: { signUpSignIn: string; editProfile: string } = { - signUpSignIn: "B2C_1_signupsignin1", - editProfile: "B2C_1_edit_profile" -}; - -export const b2cPolicies = { - names: policyNames, - authorities: { - signUpSignIn: { - authority: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${policyNames.signUpSignIn}` - }, - editProfile: { - authority: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${policyNames.editProfile}` - } - }, - authorityDomain: `${tenantName}.b2clogin.com` -}; - -const loggerCallback = (level: LogLevel, message: string, containsPii: boolean): void => { - if (containsPii) { - return; - } - switch (level) { - case LogLevel.Error: - console.error(message); - break; - case LogLevel.Info: - console.info(message); - break; - case LogLevel.Verbose: - console.debug(message); - break; - case LogLevel.Warning: - console.warn(message); - break; - default: - break; - } -}; - -export const msalConfig: Configuration = { - auth: { - clientId: "9c0c6caa-5553-410e-aaa1-12379dd7bec2", // Replace with your client ID - authority: b2cPolicies.authorities.signUpSignIn.authority, - knownAuthorities: [b2cPolicies.authorityDomain], - redirectUri: "/", - postLogoutRedirectUri: "/" - }, - cache: { - cacheLocation: "localStorage", - storeAuthStateInCookie: false // Set to true if you have issues on IE11 or Edge - }, - system: { - allowNativeBroker: false, - loggerOptions: { - loggerCallback: loggerCallback, - logLevel: LogLevel.Info, - piiLoggingEnabled: false - } - } -}; - -export const loginRequest: { scopes: string[] } = { - scopes: ["openid", "profile", "offline_access"] -}; - -export const apiConfig: { scopes: string[]; uri: string } = { - scopes: ["YOUR_API_SCOPE"], // Replace with your API scopes - uri: "YOUR_API_URI" // Replace with your API URI -}; diff --git a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx index 562744b8..9f54399f 100644 --- a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx +++ b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx @@ -2,7 +2,7 @@ import { Text } from "@fluentui/react"; import { HistoryRegular } from "@fluentui/react-icons"; import styles from "./ChatHistoryButton.module.css"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import { useContext } from "react"; interface Props { @@ -12,7 +12,7 @@ interface Props { } export const ChatHistoryButton = ({ className, disabled, onClick }: Props) => { - const { showHistoryPanel } = useAppContext(); + const { showHistoryPanel } = useContext(AppContext); const buttonContent = showHistoryPanel ? "Hide chat history" : "Show chat history"; return ( + <> + {user.role === "admin" && ( + + )} + ); }; diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 25dfd4cd..18785fe1 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -2,27 +2,30 @@ import React, { useState, useEffect, useContext } from "react"; import { loadStripe, Stripe } from "@stripe/stripe-js"; import { Elements } from "@stripe/react-stripe-js"; import styles from "./PaymentGateway.module.css"; -import { getApiKeyPayment, createCheckoutSession, getProductPrices } from "../../api"; -import { useAppContext } from "../../providers/AppProviders"; +import { getApiKeyPayment, createCheckoutSession, getProductPrices} from "../../api"; +import { AppContext } from "../../providers/AppProviders"; import { Spinner } from "@fluentui/react"; import { ChartPerson48Regular } from "@fluentui/react-icons"; + const fetchApiKey = async () => { const apiKey = await getApiKeyPayment(); return apiKey; }; export const SubscriptionPlans: React.FC<{ stripePromise: Promise }> = ({ stripePromise }) => { - const { user, organization } = useAppContext(); + const { user, organization } = useContext(AppContext); + const [currentPlan, setCurrentPlan] = useState(organization.subscriptionId ? 1 : 0); const [prices, setPrices] = useState([]); const [error, setError] = useState(null); + useEffect(() => { // Fetch product prices when the component mounts async function fetchPrices() { try { - const data = await getProductPrices({ user }); + const data = await getProductPrices({user}); setPrices(data.prices); } catch (err) { console.error("Failed to fetch product prices:", err); @@ -38,14 +41,6 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise } const handleCheckout = async (priceId: string) => { - if (!user) { - // Handle the case when user is null - // You might redirect to a login page or show an error message - console.error("User is not authenticated."); - // Optionally, you can redirect the user or display a notification - return; - } - const { url } = await createCheckoutSession({ userId: user.id, priceId, @@ -65,10 +60,11 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise {prices.map((price, index) => ( <>
+ {currentPlan === index &&
Current Subscription
}

{price.nickname}

- ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval} + ${(price.unit_amount/100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval}

{price.description}

{price.id !== "free_plan" && ( @@ -78,9 +74,9 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise role="button" aria-label={`Subscribe to ${price.nickname}`} > - {organization?.subscriptionId && organization.subscriptionStatus === "inactive" + {organization.subscriptionId && organization.subscriptionStatus === "inactive" ? "Reactivate subscription" - : organization?.subscriptionStatus === "active" + : organization.subscriptionStatus === "active" ? "Edit payment information" : "Subscribe"} diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index 78495a89..562b30b8 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useContext } from "react"; import { Dropdown, DropdownMenuItemType, IDropdownOption, IDropdown } from "@fluentui/react/lib/Dropdown"; import { Icon } from "@fluentui/react/lib/Icon"; import { IStackTokens, Stack } from "@fluentui/react/lib/Stack"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import styles from "./Profile.module.css"; import person from "../../assets/person.png"; @@ -55,12 +55,11 @@ const placeholderPrepare = (placeholder: string) => { }; export const ProfileButton: React.FunctionComponent = () => { - const { user } = useAppContext(); - - const userName = user?.name || ""; // Default to empty string if user or user.name is null - const placeholder = placeholderPrepare(userName); + const { user } = useContext(AppContext); + + const placeholder = placeholderPrepare(user.name); const email = user?.email || " "; - const headerTitle = userName; + const headerTitle = user.name; const options: IDropdownOption[] = [ { key: "Header", text: headerTitle || "Options", itemType: DropdownMenuItemType.Header }, diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index b1814768..e4832802 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,5 +1,5 @@ import { useState, useContext } from "react"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; import { getTokenOrRefresh } from "./token_util"; import { Send24Filled, Mic24Regular, AttachRegular } from "@fluentui/react-icons"; @@ -169,19 +169,13 @@ export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: }; export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { - const { user, organization } = useAppContext(); + const { user, organization } = useContext(AppContext); const [question, setQuestion] = useState(""); const [fileBlobUrl, setFileBlobUrl] = useState(null); const sendQuestion = () => { - if ( - disabled || - !question.trim() || - !organization || // Check if organization is null or undefined - organization.subscriptionStatus === "inactive" || - !organization.subscriptionId - ) { + if (disabled || !question.trim() || organization.subscriptionStatus === "inactive" || !organization.subscriptionId) { return; } @@ -241,12 +235,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr } }; - const sendQuestionDisabled = - disabled || - !question.trim() || - !organization || // Check if organization is null - organization.subscriptionStatus === "inactive" || - !organization.subscriptionId; + const sendQuestionDisabled = disabled || !question.trim() || organization.subscriptionStatus === "inactive" || !organization.subscriptionId; return ( diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index ca381acd..08474ec1 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -5,7 +5,7 @@ import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Modal, Stack import styles from "./SettingsModal.module.css"; import { getSettings, postSettings } from "../../api/api"; import { mergeStyles } from "@fluentui/react/lib/Styling"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import { Dialog, DialogContent, PrimaryButton } from "@fluentui/react"; interface Props { @@ -81,7 +81,7 @@ const ConfirmationDialog = ({ loading, isOpen, onDismiss, onConfirm }: { loading }; export const SettingsPanel = () => { - const { user, setSettingsPanel, settingsPanel } = useAppContext(); + const { user, setSettingsPanel, settingsPanel } = useContext(AppContext); const [temperature, setTemperature] = useState("0"); const [loading, setLoading] = useState(true); @@ -94,31 +94,21 @@ export const SettingsPanel = () => { useEffect(() => { const fetchData = async () => { - if (!user) { - // User is not logged in; handle accordingly - setLoading(false); - return; - } - - setLoading(true); - - try { - const data = await getSettings({ - user: { - id: user.id, - name: user.name - } - }); - setTemperature(data.temperature); - } catch (error) { - console.error("Error fetching settings:", error); - } finally { - setLoading(false); - } + getSettings({ + user: { + id: user.id, + name: user.name + } + }) + .then(data => { + setTemperature(data.temperature); + setLoading(false); + }) + .catch(error => setLoading(false)); }; - + setLoading(true); fetchData(); - }, [user]); + }, []); const handleSubmit = () => { const parsedTemperature = parseFloat(temperature); @@ -180,12 +170,6 @@ export const SettingsPanel = () => { setSettingsPanel(false); }; - if (!user) { - setLoading(false); - // Display a message or render a different component - return
Please log in to view your settings.
; - } - return (
= ({ isCollapsed, setIsCollapsed }) => { const [activeOption, setActiveOption] = useState("Chat"); - const { user, organization } = useAppContext(); + const { user, organization } = useContext(AppContext); const handleSubscriptionRedirect = (e: React.MouseEvent) => { e.preventDefault(); setActiveOption("Subscription"); - if (organization?.subscriptionId) { + if (organization.subscriptionId) { window.location.href = "https://dashboard.stripe.com/dashboard"; } else { window.location.href = "#/payment"; @@ -60,7 +60,7 @@ export const SideMenu: React.FC = ({ isCollapsed, setIsCollapsed {!isCollapsed && "Roles and access"} - {user?.role === "admin" && ( + {user.role === "admin" && (
  • = ({ isCollapsed, setIsCollapsed
  • )} - {user?.role === "admin" && ( + {user.role === "admin" && (
  • = ({ isCollapsed, setIsCollapsed
  • )} - {user?.role === "admin" && ( + {user.role === "admin" && (
  • { - // Set active account if none is active but accounts are available - const activeAccount = msalInstance.getActiveAccount(); - const accounts = msalInstance.getAllAccounts(); - - if (!activeAccount && accounts.length > 0) { - msalInstance.setActiveAccount(accounts[0]); - } - - // Enable account storage events to synchronize auth state across tabs/windows - msalInstance.enableAccountStorageEvents(); - - // Add event callback to handle successful login and token acquisition - msalInstance.addEventCallback(event => { - if ( - event.eventType === EventType.LOGIN_SUCCESS || - event.eventType === EventType.ACQUIRE_TOKEN_SUCCESS || - event.eventType === EventType.SSO_SILENT_SUCCESS - ) { - const account = (event.payload as { account: AccountInfo }).account; - msalInstance.setActiveAccount(account); - } - }); - - const container = document.getElementById("root"); +export default function App() { + const { organization } = useContext(AppContext); - // Type assertion to ensure `container` is not null - if (container) { - const root = ReactDOM.createRoot(container); + return ( + + + {!organization.subscriptionId &&( + <> + } /> + } /> + } /> + + )} + {organization.subscriptionId && ( + <> + }> + } /> + } /> + + }> + + + + } + /> + } /> + + }> + } /> + } /> + + }> + } /> + } /> + + }> + } /> + } /> + + }> + + + + } + /> + } /> + + }> + + + + } + /> + } /> + + + )} + + + ); +} - root.render( - - - - - - ); - } else { - console.error("Failed to find the root element to mount React application."); - } -}); +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + + + +); diff --git a/frontend/src/pages/Login/Login.tsx b/frontend/src/pages/Login/Login.tsx deleted file mode 100644 index 71426b32..00000000 --- a/frontend/src/pages/Login/Login.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// src/pages/Login.tsx - -import React, { useEffect } from "react"; -import { useMsal } from "@azure/msal-react"; -import { loginRequest } from "../../authConfig"; - -const Login: React.FC = () => { - const { instance } = useMsal(); - - useEffect(() => { - instance.loginRedirect(loginRequest).catch(error => { - console.error("Login Redirect Error:", error); - }); - }, [instance]); - - return null; // Optionally, render a loading indicator -}; - -export default Login; diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 0de671c1..bb3851fa 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -4,7 +4,7 @@ import { ToastContainer, toast } from "react-toastify"; import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; import { AddFilled, DeleteRegular, EditRegular, SearchRegular } from "@fluentui/react-icons"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; import { checkUser, getUsers, inviteUser, createInvitation, deleteUser } from "../../api"; @@ -16,7 +16,8 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [role, setRole] = useState("user"); - const { user } = useAppContext(); + const { user } = useContext(AppContext); + const [errorMessage, setErrorMessage] = useState(""); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); @@ -36,63 +37,29 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; return users.some((user: any) => user.data.email === sanitizedEmail); }; - const handleSubmit = async () => { - // Sanitize inputs + const handleSubmit = () => { const sanitizedUsername = DOMPurify.sanitize(username); const sanitizedEmail = DOMPurify.sanitize(email); - - // Validate inputs if (!isValidated(sanitizedUsername, sanitizedEmail)) return; - - // Check if user already exists - if (alreadyExists(sanitizedEmail)) { - setErrorMessage("User with this email already exists"); - return; - } - - // Check if `user` is not null - if (!user) { - // Handle unauthenticated user - setErrorMessage("You must be logged in to invite a user."); - return; - } - + if (alreadyExists(sanitizedEmail)) return setErrorMessage("User with this email already exists"); setLoading(true); - try { - const organizationId = user.organizationId; - const inviteResponse = await inviteUser({ - username: sanitizedUsername, - email: sanitizedEmail, - role, - organizationId - }); - - if (inviteResponse.error) { - setErrorMessage(inviteResponse.error); - setLoading(false); - return; - } - - const invitationResponse = await createInvitation({ - organizationId, - invitedUserEmail: sanitizedEmail, - userId: user.id, - role - }); - - if (invitationResponse.error) { - setErrorMessage(invitationResponse.error); + const organizationId = user.organizationId; + inviteUser({ username: sanitizedUsername, email: sanitizedEmail, role, organizationId }).then(res => { + if (res.error) { + setErrorMessage(res.error); } else { - setErrorMessage(""); - setSuccess(true); + createInvitation({ organizationId, invitedUserEmail: sanitizedEmail, userId: user.id, role: role }).then(res => { + if (res.error) { + setErrorMessage(res.error); + } else { + setErrorMessage(""); + setLoading(false); + setSuccess(true); + } + }); } - } catch (error) { - console.error("Error during invitation process:", error); - setErrorMessage("An unexpected error occurred. Please try again."); - } finally { - setLoading(false); - } + }); }; const onUserNameChange = (_ev: React.FormEvent, newValue?: string) => { @@ -349,7 +316,7 @@ export const DeleteUserDialog = ({ isOpen, onDismiss, onConfirm }: { isOpen: boo }; const Admin = () => { - const { user } = useAppContext(); + const { user } = useContext(AppContext); const [search, setSearch] = useState(""); const [filteredUsers, setFilteredUsers] = useState([]); const [selectedUser, setSelectedUser] = useState({ @@ -367,49 +334,18 @@ const Admin = () => { const [isOpen, setIsOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); - if (!user) { - return
    Please log in to view the user list.
    ; - } - - useEffect(() => { const getUserList = async () => { - if (!user) { - // User is not logged in - setUsers([]); - setFilteredUsers([]); - setLoading(false); - return; - } - - setLoading(true); - - try { - let usersList = await getUsers({ - user: { - id: user.id, - name: user.name, - organizationId: user.organizationId - } - }); - - if (!Array.isArray(usersList)) { - usersList = []; - } - - setUsers(usersList); - setFilteredUsers(usersList); - } catch (error) { - console.error("Error fetching user list:", error); - setUsers([]); - setFilteredUsers([]); - } finally { - setLoading(false); + let usersList = await getUsers({user: {id: user.id, name: user.name, organizationId: user.organizationId}}); + if (!Array.isArray(usersList)) { + usersList = []; } + setUsers(usersList); + setFilteredUsers(usersList); + setLoading(false); }; - getUserList(); - }, [user]); + }, []); useEffect(() => { if (!search) { @@ -457,7 +393,6 @@ const Admin = () => { > { }} />
  • - - {loading?null:} + + { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 01b14753..3ddb6cc9 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -4,7 +4,17 @@ import { AddRegular, BroomRegular, SparkleFilled, TabDesktopMultipleBottomRegula import styles from "./Chat.module.css"; -import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, ChatTurn, getUserInfo, checkUser, getOrganizationSubscription } from "../../api"; +import { + chatApiGpt, + Approaches, + AskResponse, + ChatRequest, + ChatRequestGpt, + ChatTurn, + getUserInfo, + checkUser, + getOrganizationSubscription +} from "../../api"; import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; import { QuestionInput } from "../../components/QuestionInput"; import { ExampleList } from "../../components/Example"; @@ -15,7 +25,7 @@ import { getTokenOrRefresh } from "../../components/QuestionInput/token_util"; import { SpeechConfig, AudioConfig, SpeechSynthesizer, ResultReason } from "microsoft-cognitiveservices-speech-sdk"; import { getFileType } from "../../utils/functions"; import salesLogo from "../../img/logo.png"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; import { FeedbackRating } from "../../components/FeedbackRating/FeedbackRating"; import { SettingsPanel } from "../../components/SettingsPanel"; @@ -33,7 +43,7 @@ if (userLanguage.startsWith("pt")) { const Chat = () => { // speech synthesis is disabled by default - const { organization } = useAppContext(); + const { organization } = useContext(AppContext); const speechSynthesisEnabled = false; const [placeholderText, setPlaceholderText] = useState(""); @@ -54,7 +64,7 @@ const Chat = () => { setDataConversation, chatId, conversationIsLoading, - setRefreshFetchHistory, + setRefreshFetchHistorial, setChatId, setChatSelected, setChatIsCleaned, @@ -62,7 +72,7 @@ const Chat = () => { settingsPanel, setUser, setOrganization - } = useAppContext(); + } = useContext(AppContext); const lastQuestionRef = useRef(""); const lastFileBlobUrl = useRef(""); @@ -116,10 +126,10 @@ const Chat = () => { const result = await chatApiGpt(request); const conditionOne = answers.map(a => ({ user: a[0] })); if (conditionOne.length <= 0) { - setRefreshFetchHistory(true); + setRefreshFetchHistorial(true); setChatId(result.conversation_id); } else { - setRefreshFetchHistory(false); + setRefreshFetchHistorial(false); } setAnswers([...answers, [question, result]]); setUserId(result.conversation_id); diff --git a/frontend/src/pages/invitations/Invitations.tsx b/frontend/src/pages/invitations/Invitations.tsx index e1b63d58..e3f64622 100644 --- a/frontend/src/pages/invitations/Invitations.tsx +++ b/frontend/src/pages/invitations/Invitations.tsx @@ -3,7 +3,7 @@ import { Spinner } from "@fluentui/react"; import { TextField } from "@fluentui/react/lib/TextField"; import { SearchRegular } from "@fluentui/react-icons"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import { getInvitations } from "../../api"; @@ -17,7 +17,7 @@ interface User { } const Invitations = () => { - const { user } = useAppContext(); + const { user } = useContext(AppContext); const [search, setSearch] = useState(""); const [filteredUsers, setFilteredUsers] = useState([]); const [users, setUsers] = useState([]); @@ -25,41 +25,16 @@ const Invitations = () => { useEffect(() => { const getUserList = async () => { - if (!user) { - setUsers([]); - setFilteredUsers([]); - setLoading(false); - return; - } - - setLoading(true); - - try { - let invitationsList = await getInvitations({ - user: { - id: user.id, - username: user.name, - organizationId: user.organizationId - } - }); - - if (!Array.isArray(invitationsList)) { - invitationsList = []; - } - - setUsers(invitationsList); - setFilteredUsers(invitationsList); - } catch (error) { - console.error("Error fetching invitations:", error); - setUsers([]); - setFilteredUsers([]); - } finally { - setLoading(false); + let invitationsList = await getInvitations({ user: { id: user.id, username: user.name, organizationId: user.organizationId } }); + if (!Array.isArray(invitationsList)) { + invitationsList = []; } + setUsers(invitationsList); + setFilteredUsers(invitationsList); + setLoading(false); }; - getUserList(); - }, [user]); + }, []); useEffect(() => { if (!search) { @@ -72,114 +47,109 @@ const Invitations = () => { } }, [search]); - if (!user) { - return
    Please log in to view your invitations.
    ; - } - - if (loading) { - return
    Loading invitations...
    ; - } - return (
    - <> -
    -

    Invitations

    -
    -
    - Access denied} + {user.role === "admin" && ( + <> +
    +

    Invitations

    +
    +
    + { - setSearch(newValue || ""); - }} - iconProps={{ - iconName: "Search", - children: - }} - /> -
    - {loading ? ( - - ) : ( -
    - - - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - - - + + ); + })} + +
    EmailRoleStatus
    {user.invited_user_email} -
    -
    - {user.role} + }} + onChange={(_ev, newValue) => { + setSearch(newValue || ""); + }} + iconProps={{ + iconName: "Search", + children: + }} + /> +
    + {loading ? ( + + ) : ( +
    + + + + + + + + + + {filteredUsers.map((user: any, index) => { + return ( + + + - + - - ); - })} - -
    EmailRoleStatus
    {user.invited_user_email} +
    +
    + {user.role} +
    - -
    -
    -
    - {user.active ? "Active" : "Inactive"} +
    +
    +
    + {user.active ? "Active" : "Inactive"} +
    - -
    -
    - )} - +
    +
    + )} + + )}
    ); }; diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index 04d17bf7..f2b69b29 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,6 +1,4 @@ import React, { useContext, useEffect, useState } from "react"; -import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react"; - import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; @@ -10,14 +8,15 @@ import github from "../../assets/github.svg"; import styles from "./Layout.module.css"; import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; import { FeedbackRatingButton } from "../../components/FeedbackRating/FeedbackRatingButton"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import { SettingsButton } from "../../components/SettingsButton"; import { ButtonPaymentGateway } from "../../components/PaymentGateway/ButtonPaymentGateway"; import { SideMenu } from "../../components/SideMenu/SideMenu"; import { ProfileButton } from "../../components/Profile"; const Layout = () => { - const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); + const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = + useContext(AppContext); const { pathname } = useLocation(); const [isCollapsed, setIsCollapsed] = useState(false); @@ -41,39 +40,32 @@ const Layout = () => { }; return ( - <> - -
    - -
    -
    -
    - -
    - {pathname === "/" && ( - <> - - - - - )} -
    - -
    -
    +
    + +
    +
    +
    + +
    + {pathname === "/" && ( + <> + + + + + )} +
    +
    -
    +
    +
    +
    - -
    -
    -
    - -
    Please sign-in to see your profile information.
    -
    - + + +
    ); }; diff --git a/frontend/src/pages/onboarding/Onboarding.tsx b/frontend/src/pages/onboarding/Onboarding.tsx index cdba1230..e2613f2d 100644 --- a/frontend/src/pages/onboarding/Onboarding.tsx +++ b/frontend/src/pages/onboarding/Onboarding.tsx @@ -1,43 +1,38 @@ -import React, { useState } from "react"; -import { Navigate } from "react-router-dom"; +import React, { useContext, useState } from "react"; import salesLogo from "../../img/logo.png"; import styles from "./Onboarding.module.css"; import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, MoneySettingsRegular } from "@fluentui/react-icons"; import { Spinner } from "@fluentui/react"; import { createOrganization, getOrganizationSubscription } from "../../api"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; const Onboarding: React.FC = () => { - const { user, setUser, organization, setOrganization } = useAppContext(); + const { user, setUser, organization, setOrganization } = useContext(AppContext); + const [organizationName, setOrganizationName] = useState(""); const [step, setStep] = useState(0); const [isLoadingStep, setIsLoadingStep] = useState(false); const maxSteps = 2; + const handleOrganizationChange = (event: React.ChangeEvent) => { setOrganizationName(event.target.value); }; const handleCreateOrganization = async () => { - if (!user) { - return null; - } const newOrganization = await createOrganization({ userId: user.id, organizationName: organizationName }); if (newOrganization.id) { setOrganization(newOrganization); setUser({ ...user, organizationId: newOrganization.id }); - return newOrganization; } }; const handleNextClick = async () => { if (step < maxSteps) { setIsLoadingStep(true); - let organization = null; if (step === 1) { - organization = await handleCreateOrganization(); + await handleCreateOrganization(); } - setStep(prevStep => prevStep + 1); setIsLoadingStep(false); } @@ -53,9 +48,6 @@ const Onboarding: React.FC = () => { window.location.href = "#/payment"; }; - if (user?.organizationId && organization?.subscriptionId) { - return ; - } return (
    diff --git a/frontend/src/pages/organization/Organization.tsx b/frontend/src/pages/organization/Organization.tsx index 03821623..99307060 100644 --- a/frontend/src/pages/organization/Organization.tsx +++ b/frontend/src/pages/organization/Organization.tsx @@ -1,63 +1,61 @@ import React, { useContext } from "react"; import { Label } from "@fluentui/react"; import { Globe32Regular } from "@fluentui/react-icons"; -import { useAppContext } from "../../providers/AppProviders"; +import { AppContext } from "../../providers/AppProviders"; import styles from "./Organization.module.css"; const Organization = () => { - const { organization } = useAppContext(); - - if (!organization) { - return ( -
    -

    No Organization

    -
    - ); - } + const { organization } = useContext(AppContext); + const { user } = useContext(AppContext); return (
    -
    -

    Organization

    -
    -
    -
    - -
    -
    - - {organization?.id} -
    -
    -
    -
    - - {organization?.name} -
    -
    - - {organization?.owner} -
    -
    -
    -
    - - {organization?.subscriptionId} -
    + {user.role !== "admin" &&

    Access denied

    } + {user.role === "admin" && ( + <> +
    +

    Organization

    -
    -
    - - {organization?.subscriptionStatus} -
    -
    - - {organization?.subscriptionExpirationDate} +
    +
    + +
    +
    + + {organization.id} +
    +
    +
    +
    + + {organization.name} +
    +
    + + {organization.owner} +
    +
    +
    +
    + + {organization.subscriptionId} +
    +
    +
    +
    + + {organization.subscriptionStatus} +
    +
    + + {organization.subscriptionExpirationDate} +
    +
    -
    -
    + + )}
    ); }; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 61a25aa8..1a1d5237 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,14 +1,12 @@ -import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode, Dispatch, SetStateAction } from "react"; +import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction, useEffect } from "react"; +import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; import { Spinner } from "@fluentui/react"; -import { checkUser, getOrganizationSubscription } from "../api"; -import { useMsal } from "@azure/msal-react"; - -// Define interfaces for UserInfo and OrganizationInfo +import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, getUserInfo, checkUser, getOrganizationSubscription } from "../api"; interface UserInfo { id: string; name: string; email: string | null; - role?: string; + role: string | undefined; organizationId?: string; } @@ -16,46 +14,28 @@ interface OrganizationInfo { id: string; name: string; owner: string; - subscriptionId?: string; - subscriptionStatus?: string; - subscriptionExpirationDate?: number; + subscriptionId: string | undefined; + subscriptionStatus: string | undefined; + subscriptionExpirationDate: number | undefined; } -// In types.ts -export interface ConversationHistoryItem { - id: string; - start_date: string; - content: string; - // Add other properties as needed -} - -export interface ChatTurn { - user: string; - bot?: { - message: string; - thoughts: any; - } | null; - // Add other properties as needed -} - -// Define the shape of the context interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; + newChatDeleted: boolean; + setNewChatDeleted: Dispatch>; showFeedbackRatingPanel: boolean; setShowFeedbackRatingPanel: Dispatch>; - settingsPanel: boolean; - setSettingsPanel: Dispatch>; - refreshFetchHistory: boolean; - setRefreshFetchHistory: Dispatch>; + refreshFetchHistorial: boolean; + setRefreshFetchHistorial: Dispatch>; chatIsCleaned: boolean; setChatIsCleaned: Dispatch>; dataHistory: ConversationHistoryItem[]; setDataHistory: Dispatch>; - user: UserInfo | null; - setUser: Dispatch>; - organization: OrganizationInfo | null; - setOrganization: Dispatch>; + user: UserInfo; + setUser: Dispatch>; + organization: OrganizationInfo; + setOrganization: Dispatch>; chatSelected: string; setChatSelected: Dispatch>; chatId: string; @@ -64,197 +44,187 @@ interface AppContextType { setDataConversation: Dispatch>; conversationIsLoading: boolean; setConversationIsLoading: Dispatch>; - newChatDeleted: boolean; - setNewChatDeleted: Dispatch>; + settingsPanel: boolean; + setSettingsPanel: Dispatch>; } -// Create the context with a default value -export const AppContext = createContext(undefined); +export const AppContext = createContext({ + showHistoryPanel: true, + setShowHistoryPanel: () => {}, + showFeedbackRatingPanel: false, + setShowFeedbackRatingPanel: () => {}, + dataHistory: [], + setDataHistory: () => {}, + user: { + id: "00000000-0000-0000-0000-000000000000", + name: "anonymous", + email: "anonymous@gmail.com", + role: undefined, + organizationId: undefined + }, + setUser: () => {}, + organization: { + id: "00000000-0000-0000-0000-000000000000", + name: "no-organization", + owner: "no-owner", + subscriptionId: undefined, + subscriptionStatus: undefined, + subscriptionExpirationDate: undefined + }, + setOrganization: () => {}, + dataConversation: [], + setDataConversation: () => {}, + chatId: "", + setChatId: () => {}, + conversationIsLoading: false, + setConversationIsLoading: () => {}, + refreshFetchHistorial: false, + setRefreshFetchHistorial: () => {}, + chatSelected: "", + setChatSelected: () => {}, + chatIsCleaned: false, + setChatIsCleaned: () => {}, + settingsPanel: false, + setSettingsPanel: () => {}, + newChatDeleted: false, + setNewChatDeleted: () => {} +}); -// Create the provider component -export const AppProvider: React.FC<{ children: ReactNode; activeAccount: any }> = ({ children, activeAccount }) => { - // State variables +export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { const [showHistoryPanel, setShowHistoryPanel] = useState(true); const [showFeedbackRatingPanel, setShowFeedbackRatingPanel] = useState(false); - const [settingsPanel, setSettingsPanel] = useState(false); - const [refreshFetchHistory, setRefreshFetchHistory] = useState(false); - const [chatIsCleaned, setChatIsCleaned] = useState(false); + const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); const [dataHistory, setDataHistory] = useState([]); const [dataConversation, setDataConversation] = useState([]); - const [chatSelected, setChatSelected] = useState(""); + const [user, setUser] = useState({ + id: "00000000-0000-0000-0000-000000000000", + name: "anonymous", + email: "anonymous@gmail.com", + role: undefined, + organizationId: undefined + }); + const [organization, setOrganization] = useState({ + id: "00000000-0000-0000-0000-000000000000", + name: "no-organization", + owner: "no-owner", + subscriptionId: undefined, + subscriptionStatus: undefined, + subscriptionExpirationDate: undefined + }); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); - const [newChatDeleted, setNewChatDeleted] = useState(false); - const [user, setUser] = useState(null); - const [organization, setOrganization] = useState(null); - const [loading, setLoading] = useState(true); + const [chatIsCleaned, setChatIsCleaned] = useState(false); + const [chatSelected, setChatSelected] = useState(""); + const [settingsPanel, setSettingsPanel] = useState(false); + const [newChatDeleted, setNewChatDeleted] = useState(false); - // MSAL instance and active account + const handleKeyDown = (event: KeyboardEvent) => { + const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isAlt = event.altKey; - // Handle keyboard shortcuts - const handleKeyDown = useCallback( - (event: KeyboardEvent) => { - const isCtrlOrCmd = event.ctrlKey || event.metaKey; - const isAlt = event.altKey; + if (event.code === "Digit8" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + setShowFeedbackRatingPanel(!showFeedbackRatingPanel); + setSettingsPanel(false); + setShowHistoryPanel(false); + } else if (event.code === "Period" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + setShowHistoryPanel(!showHistoryPanel); + setShowFeedbackRatingPanel(false); + setSettingsPanel(false); + } else if (event.code === "Comma" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + setSettingsPanel(!settingsPanel); + setShowHistoryPanel(false); + setShowFeedbackRatingPanel(false); + } else if (event.code === "Digit0" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; + } else if (event.code === "Digit9" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + window.location.href = "#/admin"; + } else if (event.code === "Digit7" && isCtrlOrCmd && isAlt) { + event.preventDefault(); + window.location.href = "#/payment"; + } + }; - if (isCtrlOrCmd && isAlt) { - switch (event.code) { - case "Digit8": - event.preventDefault(); - setShowFeedbackRatingPanel(prev => !prev); - setSettingsPanel(false); - setShowHistoryPanel(false); - break; - case "Period": - event.preventDefault(); - setShowHistoryPanel(prev => !prev); - setShowFeedbackRatingPanel(false); - setSettingsPanel(false); - break; - case "Comma": - event.preventDefault(); - setSettingsPanel(prev => !prev); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - break; - case "Digit0": - event.preventDefault(); - window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; - break; - case "Digit9": - event.preventDefault(); - window.location.href = "#/admin"; - break; - case "Digit7": - event.preventDefault(); - window.location.href = "#/payment"; - break; - default: - break; - } - } - }, - [setShowFeedbackRatingPanel, setSettingsPanel, setShowHistoryPanel] - ); + window.addEventListener("keydown", handleKeyDown); + //If I add this on a useEffect it doesn't work, I don't know why + //maybe because it's a global event listener and is called multiple times - // Add event listener for keyboard shortcuts - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [handleKeyDown]); + const [loading, setLoading] = useState(true); - // Fetch user and organization info useEffect(() => { - let isMounted = true; - const fetchUserInfo = async () => { - try { - if (activeAccount) { - const id = activeAccount.localAccountId; - const name = activeAccount.name || "User"; - const email = activeAccount.idTokenClaims?.emails?.[0] || activeAccount.username || null; - // Check if user exists and get role and organizationId - const result = await checkUser({ user: { id, name, email } }); - const role = result.role; - const organizationId = result.organizationId; + if (!user.organizationId) { + const getUserInfoList = async () => { + if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { + const userInfoList = await getUserInfo(); + if (userInfoList.length === 0) { + // setShowAuthMessage(true); + console.log("No user info found. Using anonymous user.", userInfoList); + } else { + // setShowAuthMessage(false); + console.log("User info found.", userInfoList); - // Set user state - if (isMounted) { - setUser({ id, name, email, role, organizationId }); - } + const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; + const keyName = "name"; + const keyEmail = "emails"; - // Get organization info - if (organizationId) { - const organizationData = await getOrganizationSubscription({ - userId: id, - organizationId + const _user = userInfoList.find(obj => { + const _id = obj?.user_claims?.some(claim => claim.typ === keyId); + const _name = obj?.user_claims?.some(claim => claim.typ === keyName); + return _id && _name; }); - if (isMounted && organizationData) { - setOrganization({ - id: organizationData.id, - name: organizationData.name, - owner: organizationData.owner, - subscriptionId: organizationData.subscriptionId, - subscriptionStatus: organizationData.subscriptionStatus, - subscriptionExpirationDate: organizationData.subscriptionExpirationDate - }); + if (_user) { + const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; + const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; + const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; + + if (id && name) { + setUser({ id, name, email, role: undefined, organizationId: undefined }); + } + + // register user if doesn't exist + + // const response = await getUsers({ user: { id, name, email } }); // to get all users + + // verifies if user exists and assigns the role + const result = await checkUser({ user: { id, name, email } }); + const role = result["role"] || undefined; + const organizationId = result["organizationId"] || undefined; + + const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); + + if (result && role) { + setUser({ id, name, email, role, organizationId }); + } + if (organization) { + setOrganization({ + id: organization.id, + name: organization.name, + owner: organization.owner, + subscriptionId: organization.subscriptionId, + subscriptionStatus: organization.subscriptionStatus, + subscriptionExpirationDate: organization.subscriptionExpirationDate + }); + } + setLoading(false); } } } else { - // No active account, leave user and organization as null - if (isMounted) { - setUser(null); - setOrganization(null); - } - } - } catch (error) { - console.error("Error fetching user info:", error); - // Handle errors as needed - } finally { - if (isMounted) { + console.log("Local"); + setUser({ ...user, organizationId: "test-organization" }); + setOrganization({ ...organization, subscriptionId: "test-subscriptionId" }); setLoading(false); } - } - }; - - fetchUserInfo(); - - return () => { - isMounted = false; - }; - }, [activeAccount]); - - // Memoize context value to prevent unnecessary re-renders - const contextValue = useMemo( - () => ({ - showHistoryPanel, - setShowHistoryPanel, - showFeedbackRatingPanel, - setShowFeedbackRatingPanel, - settingsPanel, - setSettingsPanel, - refreshFetchHistory, - setRefreshFetchHistory, - chatIsCleaned, - setChatIsCleaned, - dataHistory, - setDataHistory, - user, - setUser, - organization, - setOrganization, - chatSelected, - setChatSelected, - chatId, - setChatId, - dataConversation, - setDataConversation, - conversationIsLoading, - setConversationIsLoading, - newChatDeleted, - setNewChatDeleted - }), - [ - showHistoryPanel, - showFeedbackRatingPanel, - settingsPanel, - refreshFetchHistory, - chatIsCleaned, - dataHistory, - user, - organization, - chatSelected, - chatId, - dataConversation, - conversationIsLoading, - newChatDeleted - ] - ); + }; + getUserInfoList(); + } + }, []); - // Render loading spinner if data is still loading - if (loading) { + if (loading) return (
    ); - } - - // Provide the context to child components - return {children}; -}; -// Custom hook for consuming the context -export const useAppContext = () => { - const context = useContext(AppContext); - if (context === undefined) { - throw new Error("useAppContext must be used within an AppProvider"); - } - return context; + return ( + + {children} + + ); }; diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index c15cf77b..e42a9011 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -1,60 +1,18 @@ -// src/ProtectedRoute.jsx -import React, { useContext } from "react"; -import { Outlet, Navigate } from "react-router-dom"; -import { MsalAuthenticationTemplate, useMsal } from "@azure/msal-react"; -import { useAppContext } from "../providers/AppProviders"; +import React, { useContext, ReactNode } from 'react'; +import { AppContext } from "../providers/AppProviders"; +import { Navigate } from 'react-router-dom'; -import { InteractionType } from "@azure/msal-browser"; -import { loginRequest } from "../authConfig"; -import LoadingSpinner from "../components/LoadingSpinner/LoadingSpinner"; // Optional: Create a loading spinner component - -/** - * ProtectedRoute component ensures that only authenticated users with allowed roles can access certain routes. - * It uses MsalAuthenticationTemplate to handle authentication automatically. - * - * @param {Array} allowedRoles - Array of roles that are permitted to access the route. - */ interface ProtectedRouteProps { - allowedRoles: string[]; + children: ReactNode; + allowedRoles: string[]; } -const ProtectedRoute: React.FC = ({ allowedRoles }) => { - const { instance } = useMsal(); - const activeAccount = instance.getActiveAccount(); - const { user, organization } = useAppContext(); - const subscriptionId = organization?.subscriptionId - const hasActiveAccount = (): boolean => { - if (!activeAccount) { - return false; - } - return true; - }; - // Function to check if the user has at least one of the allowed roles - const hasRequiredRole = (): boolean => { - const roles = [user?.role]; - //const roles = activeAccount.idTokenClaims?.roles as string[] | undefined; - if (!roles) return false; - return allowedRoles.some(role => roles.includes(role)); - }; - - const hasRequiredSubscriptionID = (): boolean => { - if(!subscriptionId) return false; - return true; - } - - return ( - - { hasActiveAccount() && hasRequiredSubscriptionID() && hasRequiredRole() ? ( - - ) : hasActiveAccount() === false ? ( - - ) : hasActiveAccount() === true && hasRequiredSubscriptionID() === false ? ( - - ) : ( - - )} - - ); +const ProtectedRoute = ({ children, allowedRoles }: ProtectedRouteProps) => { + const { user } = useContext(AppContext); + if (!user.role || !allowedRoles.includes(user.role)) { + return ; + } + return (<>{children}); }; export default ProtectedRoute; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 77822aac..d711f0f9 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,30 +11,12 @@ export default defineConfig({ }, server: { proxy: { - "/chatgpt": "http://localhost:8000", - "/api/stripe": "http://localhost:8000", - "/api/chat-history": "http://localhost:8000", - "/api/chat-conversation/": "http://localhost:8000", - "/api/chat-conversations/": "http://localhost:8000", - "/api/create-organization": "http://localhost:8000", - "/api/get-speech-token": "http://localhost:8000", - "/api/get-storage-account": "http://localhost:8000", - "/create-checkout-session": "http://localhost:8000", - "/webhook": "http://localhost:8000", - "/api/upload-blob": "http://localhost:8000", - "/api/get-blob": "http://localhost:8000", - "/api/settings": "http://localhost:8000", - "/api/feedback": "http://localhost:8000", - "/api/getusers": "http://localhost:8000", - "/api/deteleuser": "http://localhost:8000", - "/api/inviteUser": "http://localhost:8000", - "/api/getInvitations": "http://localhost:8000", - "/api/createInvitation": "http://localhost:8000", - "/api/checkuser": "http://localhost:8000", - "/api/get-organization-subscription": "http://localhost:8000", - "/api/getUser": "http://localhost:8000", - "/api/conversations": "http://localhost:8000", - "/api/chat": "http://localhost:8000" + '/chatgpt': "http://localhost:8000", + '/api/get-speech-token': "http://localhost:8000", + '/api/get-storage-account': "http://localhost:8000", + '/api/get-blob': "http://localhost:8000", + '/api/settings': "http://localhost:8000", + '/api/conversations': "http://localhost:8000", }, host: true } From b584fbeb77115bf6b120ba4163a12b07f8fabdce Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Wed, 30 Oct 2024 14:47:03 -0400 Subject: [PATCH 161/820] FA 127 Add pull request template (#136) --- .github/pull_request_template.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..f3b7e429 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,10 @@ +## JIRA Ticket +[PROJ-XXX](link-to-ticket) + +## Description +[Describe your changes here] + +## Checklist +- [ ] Code review requested +- [ ] Tests completed +- [ ] Documentation updated From edbcc0f959581e834a959231559e5be5abdc1983 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 31 Oct 2024 16:33:08 -0400 Subject: [PATCH 162/820] Fa 87 septu msal (#135) * FA 87 add msal authentication flow * FA-87-add-role-access * FA-87-Replace context user data front msal user data * FA-87 include api patch proxys redirections * FA-87 revert replace get user data from msal * FA-87 Verification user * FA-87 refactor provider and types * FA-87 Refactor Navigation * FA-87 Fix for Roles and Access not loading * FA-87 Refactor Navigation and Verification User * FA-87 Fix Roles and Access Button * Fix conflicts * FA-87 Create atuh config GET endpoint * FA-87 remove MSAL client side react authentication code * FA-87 setup ms identity python web app library * FA-87 retrive user profile * FA - 87 get organization data * set user paylod for request * FA-87 set proper step on oboarding unfinished * Implement retry in check_user_authorization; add missing line in vite.config.ts * Include tenacity in requirements.txt --------- Co-authored-by: ramirezmorac2 --- .gitignore | 4 +- README.md | 37 +- backend/app.py | 438 +++++++++++++++++- backend/app_config.py | 27 ++ backend/auth.py | 129 ++++++ backend/models.py | 31 ++ backend/requirements.txt | 8 +- backend/test_auth.py | 41 ++ frontend/package-lock.json | 36 ++ frontend/package.json | 2 + frontend/src/App.tsx | 54 +++ frontend/src/api/api.ts | 14 +- .../ChatHistoryButton/ChatHistoryButton.tsx | 4 +- .../FeedbackRating/FeedbackRating.tsx | 12 +- .../FeedbackRating/FeedbackRatingButton.tsx | 4 +- .../HistoryPannel/ChatHistoryListItem.tsx | 118 +++-- .../HistoryPannel/ChatHistoryPanel.tsx | 4 +- .../LoadingSpinner/LoadingSpinner.tsx | 11 + .../PaymentGateway/ButtonPaymentGateway.tsx | 28 +- .../PaymentGateway/PaymentGateway.tsx | 26 +- frontend/src/components/Profile/index.tsx | 11 +- .../QuestionInput/QuestionInput.tsx | 19 +- .../src/components/SettingsPanel/index.tsx | 46 +- frontend/src/components/SideMenu/SideMenu.tsx | 12 +- frontend/src/index.tsx | 112 +---- frontend/src/pages/admin/Admin.tsx | 123 +++-- frontend/src/pages/chat/Chat.tsx | 24 +- .../src/pages/invitations/Invitations.tsx | 244 +++++----- frontend/src/pages/layout/Layout.tsx | 55 +-- frontend/src/pages/onboarding/Onboarding.tsx | 24 +- .../src/pages/organization/Organization.tsx | 92 ++-- frontend/src/providers/AppProviders.tsx | 428 +++++++++-------- frontend/src/router/ProtectedRoute.tsx | 54 ++- frontend/vite.config.ts | 32 +- 34 files changed, 1609 insertions(+), 695 deletions(-) create mode 100644 backend/app_config.py create mode 100644 backend/auth.py create mode 100644 backend/models.py create mode 100644 backend/test_auth.py create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/components/LoadingSpinner/LoadingSpinner.tsx diff --git a/.gitignore b/.gitignore index b9ea4e13..af1eb595 100644 --- a/.gitignore +++ b/.gitignore @@ -152,4 +152,6 @@ npm-debug.log* node_modules static/ -settings.json \ No newline at end of file +settings.json + +flask_session/ \ No newline at end of file diff --git a/README.md b/README.md index 0c6bb923..f6957793 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Part of [GPT-RAG](https://github.com/Azure/gpt-rag) - Zip command - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) -- Node.js 16+ [windows/mac](https://nodejs.dev/en/download/) [linux/wsl](https://nodejs.dev/en/download/package-manager/) +- Node.js 16+ [windows/mac](https://nodejs.dev/en/download/) [linux/wsl](https://nodejs.dev/en/download/package-manager/) - Install ZIP in WSL/Linux: sudo apt-get install zip - + **1) Clone the Repository** ``` @@ -31,12 +31,12 @@ npm install npm run build ``` -**3) Deploy to Azure** +**3) Deploy to Azure** Execute the following commands in the terminal to deploy your function: 2.1. Enter backend folder - + ``` cd .. cd backend @@ -45,17 +45,19 @@ cd backend 2.2. Remove backend_env if you have tested it locally ``` -rm -rf backend_env +rm -rf backend_env ``` 2.3. Zip source code Linux or Mac: + ``` zip -r ../deploy.zip * ``` Windows: + ``` tar -a -c -f ../deploy.zip * ``` @@ -64,16 +66,16 @@ tar -a -c -f ../deploy.zip * ``` cd .. -az webapp deploy --subscription [SUBSCRIPTION_ID] --resource-group [RESOURCE_GROUP_NAME] --name [WEB_APP_NAME] --src-path deploy.zip --type zip --async true +az webapp deploy --subscription e261fb0a-3d87-49c1-8d3c-32b2bc93b6ff --resource-group rg-develop-clew --name webgpt0-vm2b2htvuuclm --src-path deploy.zip --type zip --async true ``` -## **(Optional) Test locally** +## **(Optional) Test locally** -1) rename ```.env.template``` to ```.env``` updating the variables accordingly. +1. rename `.env.template` to `.env` updating the variables accordingly. -2) run ```azd auth login``` or ```az login``` +2. run `azd auth login` or `az login` -3) run ```./start.sh``` or ```./startwin.sh``` for windows +3. run `./start.sh` or `./startwin.sh` for windows ## Frontend customizations @@ -83,13 +85,13 @@ Optionally you can customize some items in the frontend. Update page's title -file: ```frontend/src/pages/layout/Layout.tsx``` +file: `frontend/src/pages/layout/Layout.tsx` ```

    Chat On Your Data/h4> ``` -file: ```frontend/src/pages/layout/index.html``` +file: `frontend/src/pages/layout/index.html` ``` Chat Chat On Your Data | Demo @@ -99,9 +101,10 @@ file: ```frontend/src/pages/layout/index.html``` Update frontend logo -file: ```frontend/src/pages/layout/Layout.tsx``` +file: `frontend/src/pages/layout/Layout.tsx` Example: + ``` @@ -113,7 +116,7 @@ Example: You can remove citations from the answers if you do not want them. Just set showSources to {false} -file: ```frontend/src/pages/chat/Chat.tsx``` +file: `frontend/src/pages/chat/Chat.tsx` ``` onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} onFollowupQuestionClicked={q => makeApiRequestGpt(q)} showFollowupQuestions={false} - showSources={false} + showSources={false} /> ``` @@ -133,7 +136,7 @@ file: ```frontend/src/pages/chat/Chat.tsx``` To enable speech synthesis change speechSynthesisEnabled variable to true. -file: ```frontend/src/pages/chat/Chat.tsx``` +file: `frontend/src/pages/chat/Chat.tsx` ``` const speechSynthesisEnabled = true; @@ -141,7 +144,7 @@ const speechSynthesisEnabled = true; ## Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. diff --git a/backend/app.py b/backend/app.py index 6e7726ac..7c8a4c48 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,12 +1,18 @@ import os import re -import mimetypes -import time import logging import requests import json import stripe -from flask import Flask, request, jsonify, Response, redirect +from flask import ( + Flask, + request, + jsonify, + Response, + send_from_directory, + redirect, + url_for, +) from flask_cors import CORS from dotenv import load_dotenv from azure.keyvault.secrets import SecretClient @@ -14,10 +20,17 @@ from azure.storage.blob import BlobServiceClient from urllib.parse import unquote import uuid +from identity.flask import Auth +from datetime import timedelta, datetime import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +import app_config +import logging +from functools import wraps +from typing import Dict, Any, Tuple, Optional +from tenacity import retry, wait_fixed, stop_after_attempt load_dotenv() @@ -67,13 +80,391 @@ def get_secret(secretName): AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING") AZURE_CSV_STORAGE_NAME = os.getenv("AZURE_CSV_STORAGE_CONTAINER", "files") +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + app = Flask(__name__) +app.config.from_object(app_config) CORS(app) -@app.route("/", defaults={"path": "index.html"}) + +auth = Auth( + app, + client_id=os.getenv("AAD_CLIENT_ID"), + client_credential=os.getenv("AAD_CLIENT_SECRET"), + redirect_uri=os.getenv("AAD_REDIRECT_URI"), + b2c_tenant_name=os.getenv("AAD_TENANT_NAME"), + b2c_signup_signin_user_flow=os.getenv("AAD_POLICY_NAME"), + b2c_edit_profile_user_flow=os.getenv("EDITPROFILE_USER_FLOW"), +) + + +def handle_auth_error(func): + """Decorator to handle authentication errors consistently""" + + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + logger.exception("[auth] Error in user authentication") + return ( + jsonify( + { + "error": "Authentication error", + "message": str(e), + "status": "error", + } + ), + 500, + ) + + return wrapper + + +class UserService: + """Service class to handle user-related operations""" + + @staticmethod + def validate_user_context( + user_context: Dict[str, Any] + ) -> Tuple[bool, Optional[str]]: + """ + Validate the user context from B2C + + Args: + user_context: The user context from B2C + + Returns: + Tuple of (is_valid: bool, error_message: Optional[str]) + """ + required_fields = { + "sub": "User ID", + "name": "User Name", + "emails": "Email Address", + } + + for field, display_name in required_fields.items(): + if field not in user_context: + return False, f"Missing {display_name}" + if field == "emails" and not user_context[field]: + return False, "Email address list is empty" + + return True, None + + @staticmethod + @retry(wait=wait_fixed(2), stop=stop_after_attempt(3)) + def check_user_authorization( + client_principal_id: str, + client_principal_name: str, + email: str, + function_key: str, + check_user_endpoint: str, + timeout: int = 10, + ) -> Dict[str, Any]: + """ + Check user authorization with the orchestrator using POST request + + Args: + client_principal_id: The user's principal ID from Azure B2C + client_principal_name: The user's principal name from Azure B2C + email: The user's email address + function_key: The function key for authentication + check_user_endpoint: The endpoint URL for checking user authorization + timeout: Request timeout in seconds (default: 10) + + Returns: + Dict containing the authorization response + + Raises: + requests.RequestException: If the request fails + ValueError: If the response format is invalid + TimeoutError: If the request times out + """ + try: + # Prepare request payload + payload = { + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + "id": client_principal_id, + "name": client_principal_name, + "email": email, + } + + # Prepare headers + headers = { + "Content-Type": "application/json", + "x-functions-key": function_key, + } + + logger.info( + f"[auth] Checking authorization for user {client_principal_id} " + f"with email {email} at {datetime.utcnow().isoformat()}" + ) + + # Make the request using a session for better performance + with requests.Session() as session: + response = session.post( + check_user_endpoint, + headers=headers, + data=json.dumps(payload), + timeout=timeout, + ) + + # Log the response (truncated for security) + truncated_response = ( + response.text[:500] + "..." + if len(response.text) > 500 + else response.text + ) + logger.info(f"[auth] Authorization response: {truncated_response}") + + # Raise an exception for bad status codes + response.raise_for_status() + + # Parse response + try: + data = response.json() + except json.JSONDecodeError as e: + logger.error(f"[auth] Failed to parse JSON response: {str(e)}") + raise ValueError("Invalid JSON response") from e + + # Validate response format if needed + if not data: + raise ValueError("Empty response received") + + return data + + except requests.Timeout as e: + logger.error( + f"[auth] Request timed out after {timeout} seconds for " + f"user {client_principal_id}: {str(e)}" + ) + raise TimeoutError(f"Request timed out after {timeout} seconds") from e + + except requests.RequestException as e: + logger.error( + f"[auth] Request failed for user {client_principal_id}: {str(e)}" + ) + raise + + except Exception as e: + logger.error( + f"[auth] Unexpected error checking authorization for " + f"user {client_principal_id}: {str(e)}" + ) + raise + + +@app.route("/") +@auth.login_required +def index(*, context): + """ + Endpoint to get the current user's data from Microsoft Graph API + """ + logger.debug(f"User context: {context}") + return send_from_directory("static", "index.html") + + +# route for other static files @app.route("/") -def static_file(path): - return app.send_static_file(path) +def static_files(path): + # Don't require authentication for static assets + return send_from_directory("static", path) + + +@app.route("/auth-response") +def auth_response(): + try: + return auth.complete_log_in(request.args) + except Exception as e: + logger.error(f"Authentication error: {str(e)}") + return redirect(url_for("index")) + + +@app.route("/api/auth/config") +def get_auth_config(): + """Return Azure AD B2C configuration for frontend""" + return jsonify( + { + "clientId": os.getenv("AAD_CLIENT_ID"), + "authority": f"https://{os.getenv('AAD_TENANT_NAME')}.b2clogin.com/{os.getenv('AAD_TENANT_NAME')}.onmicrosoft.com/{os.getenv('AAD_POLICY_NAME')}", + "redirectUri": "http://localhost:8000", + "scopes": ["openid", "profile"], + } + ) + + +# Constants and Configuration + + +@app.route("/api/auth/user") +@auth.login_required +@handle_auth_error +def get_user(*, context: Dict[str, Any]) -> Tuple[Dict[str, Any], int]: + """ + Get authenticated user information and profile from authorization service. + + Args: + context: The authentication context from B2C containing user claims + + Returns: + Tuple[Dict[str, Any], int]: User profile data and HTTP status code + + Raises: + ValueError: If required secrets or user data is missing + RequestException: If authorization service call fails + """ + try: + # Validate user context + is_valid, error_message = UserService.validate_user_context(context["user"]) + if not is_valid: + logger.error(f"[auth] Invalid user context: {error_message}") + return ( + jsonify( + { + "error": "Invalid user context", + "message": error_message, + "status": "error", + } + ), + 400, + ) + + # Get user ID early to include in logs + client_principal_id = context["user"].get("sub") + logger.info(f"[auth] Processing request for user {client_principal_id}") + + # Get function key from Key Vault + key_secret_name = "orchestrator-host--checkuser" + function_key = get_secret(key_secret_name) + if not function_key: + raise ValueError(f"Secret {key_secret_name} not found in Key Vault") + + check_user_endpoint = CHECK_USER_ENDPOINT + client_principal_name = context["user"]["name"] + email = context["user"]["emails"][0] + # Check user authorization + user_profile = UserService.check_user_authorization( + client_principal_id, + client_principal_name, + email, + function_key, + check_user_endpoint, + timeout=10, + ) + + # Validate user profile response + if not user_profile: + logger.error(f"[auth] Invalid user profile response: {user_profile}") + return ( + jsonify( + { + "error": "Invalid user profile", + "message": "User profile data is missing or invalid", + "status": "error", + } + ), + 500, + ) + + # Validate required fields in user profile + required_profile_fields = ["role", "organizationId"] + for field in required_profile_fields: + if field not in user_profile: + logger.error(f"[auth] Missing required field in user profile: {field}") + return ( + jsonify( + { + "error": "Invalid user profile", + "message": f"Missing required field: {field}", + "status": "error", + } + ), + 500, + ) + + # Log successful profile retrieval + logger.info( + f"[auth] Successfully retrieved profile for user {client_principal_id} " + f"with role {user_profile['role']}" + ) + + # Construct and return response + return ( + jsonify( + { + "status": "success", + "authenticated": True, + "user": { + "id": context["user"]["sub"], + "name": context["user"]["name"], + "email": context["user"]["emails"][0], + "role": user_profile["role"], + "organizationId": user_profile["organizationId"], + }, + } + ), + 200, + ) + + except ValueError as e: + logger.error(f"[auth] Key Vault error for user {client_principal_id}: {str(e)}") + return ( + jsonify( + { + "error": "Configuration error", + "message": "Failed to retrieve necessary configuration", + "status": "error", + } + ), + 500, + ) + + except requests.RequestException as e: + logger.error( + f"[auth] User authorization check failed for user {client_principal_id}: {str(e)}" + ) + return ( + jsonify( + { + "error": "Authorization check failed", + "message": "Failed to verify user authorization", + "status": "error", + } + ), + 500, + ) + + except KeyError as e: + logger.error( + f"[auth] Missing required data in response for user {client_principal_id}: {str(e)}" + ) + return ( + jsonify( + { + "error": "Data error", + "message": "Missing required user data", + "status": "error", + } + ), + 500, + ) + + except Exception as e: + logger.exception( + f"[auth] Unexpected error in get_user for user {client_principal_id}" + ) + return ( + jsonify( + { + "error": "Internal server error", + "message": "An unexpected error occurred", + "status": "error", + } + ), + 500, + ) @app.route("/chatgpt", methods=["POST"]) @@ -262,12 +653,16 @@ def create_checkout_session(): cancel_url=cancel_url, automatic_tax={"enabled": True}, custom_fields=[ - { - "key": "organization_name", - "label": {"type": "custom", "custom": "Organization Name"}, - "type": "text", - "text": {"minimum_length": 5, "maximum_length": 100}, - } if organizationId == "" else {} + ( + { + "key": "organization_name", + "label": {"type": "custom", "custom": "Organization Name"}, + "type": "text", + "text": {"minimum_length": 5, "maximum_length": 100}, + } + if organizationId == "" + else {} + ) ], ) except Exception as e: @@ -802,6 +1197,7 @@ def sendEmail(): logging.error("Something went wrong...", e) return jsonify({"error": str(e)}), 500 + @app.route("/api/getInvitations", methods=["GET"]) def getInvitations(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -829,13 +1225,16 @@ def getInvitations(): organizationId = request.args.get("organizationId") url = INVITATIONS_ENDPOINT headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", url, headers=headers,params={"organizationId": organizationId}) + response = requests.request( + "GET", url, headers=headers, params={"organizationId": organizationId} + ) logging.info(f"[webbackend] response: {response.text[:500]}...") return response.text except Exception as e: logging.exception("[webbackend] exception in /get-organization") return jsonify({"error": str(e)}), 500 + @app.route("/api/createInvitation", methods=["POST"]) def createInvitation(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -990,7 +1389,7 @@ def createOrganization(): functionKey = get_secret(keySecretName) except Exception as e: logging.exception( - "[webbackend] exception in /api/orchestrator-host--subscriptions" + f"[webbackend] exception in /api/orchestrator-host--subscriptions {e}" ) return ( jsonify( @@ -1017,6 +1416,7 @@ def createOrganization(): logging.exception("[webbackend] exception in /post-organization") return jsonify({"error": str(e)}), 500 + @app.route("/api/getUser", methods=["GET"]) def getUser(): client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") @@ -1065,25 +1465,24 @@ def get_product_prices(product_id): if not product_id: raise ValueError("Product ID is required to fetch prices") - + try: # Fetch all prices associated with a product prices = stripe.Price.list( - product=product_id, - active=True # Optionally filter only active prices + product=product_id, active=True # Optionally filter only active prices ) return prices.data except Exception as e: logging.error(f"Error fetching prices: {e}") raise + @app.route("/api/prices", methods=["GET"]) def get_product_prices_endpoint(): - product_id = request.args.get('product_id', PRODUCT_ID_DEFAULT) + product_id = request.args.get("product_id", PRODUCT_ID_DEFAULT) if not product_id: return jsonify({"error": "Missing product_id parameter"}), 400 - try: prices = get_product_prices(product_id) @@ -1094,5 +1493,6 @@ def get_product_prices_endpoint(): logging.error(f"Failed to retrieve prices: {e}") return jsonify({"error": str(e)}), 500 + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/app_config.py b/backend/app_config.py new file mode 100644 index 00000000..422238db --- /dev/null +++ b/backend/app_config.py @@ -0,0 +1,27 @@ +# app_config.py +import os + +# Flask configuration +SECRET_KEY = os.getenv("FLASK_SECRET_KEY", "default-secret-key-change-in-production") +SESSION_TYPE = "filesystem" + +# Azure AD B2C details +B2C_TENANT_NAME = os.getenv("AAD_TENANT_NAME") # e.g. "contoso" +SIGNUPSIGNIN_USER_FLOW = os.getenv("AAD_POLICY_NAME") # e.g. "B2C_1_signupsignin1" +EDITPROFILE_USER_FLOW = os.getenv( + "EDITPROFILE_USER_FLOW" +) # e.g. "B2C_1_profileediting1" +RESETPASSWORD_USER_FLOW = os.getenv( + "RESETPASSWORD_USER_FLOW" +) # e.g. "B2C_1_passwordreset1" + +# Application (client) registration details +CLIENT_ID = os.getenv("AAD_CLIENT_ID") +CLIENT_SECRET = os.getenv("AAD_CLIENT_SECRET") + +# Endpoint configuration +AUTHORITY = f"https://{B2C_TENANT_NAME}.b2clogin.com/{B2C_TENANT_NAME}.onmicrosoft.com" +REDIRECT_PATH = "/" # The absolute URL must match your app's redirect_uri + +# B2C policy configuration +B2C_POLICY = SIGNUPSIGNIN_USER_FLOW # Default policy diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 00000000..5c24fac0 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,129 @@ +from flask import request, jsonify +from functools import wraps +import jwt +import json +import requests +from datetime import datetime +from jwt import PyJWTError +import os +from cachetools import TTLCache +from cryptography.x509 import load_pem_x509_certificate +from cryptography.hazmat.backends import default_backend +import base64 + +# Cache for storing JWKS (JSON Web Key Set) +jwks_cache = TTLCache(maxsize=1, ttl=86400) + + +class AuthConfig: + def __init__(self): + self.tenant_name = os.getenv("AAD_TENANT_NAME") + self.client_id = os.getenv("AAD_CLIENT_ID") + self.policy_name = os.getenv("AAD_POLICY_NAME", "B2C_1_signupsignin") + + # Build the authority and JWKS URLs + self.authority = f"https://{self.tenant_name}.b2clogin.com/{self.tenant_name}.onmicrosoft.com/{self.policy_name}" + self.jwks_url = f"{self.authority}/discovery/v2.0/keys" + self.issuer = f"https://{self.tenant_name}.b2clogin.com/{os.getenv('AAD_TENANT_ID')}/v2.0/" + + +auth_config = AuthConfig() + + +class AuthError(Exception): + """Custom exception for authentication errors""" + + pass + + +def get_jwks(): + """Fetch and cache the JSON Web Key Set from Azure AD B2C""" + if "keys" not in jwks_cache: + try: + response = requests.get(auth_config.jwks_url) + response.raise_for_status() + jwks_cache["keys"] = response.json()["keys"] + except requests.exceptions.RequestException as e: + print(f"Error fetching JWKS: {e}") + raise + return jwks_cache["keys"] + + +def get_key_by_kid(kid): + """Get the public key matching the key ID from the JWKS""" + keys = get_jwks() + for key_data in keys: + if key_data["kid"] == kid: + return key_data + return None + + +def verify_token(token): + """Verify the JWT token from Azure AD B2C""" + try: + # Get the header without verification + header = jwt.get_unverified_header(token) + + # Get the key matching the kid from the token header + key_data = get_key_by_kid(header["kid"]) + if not key_data: + raise AuthError("Invalid token: Key ID not found") + + # Construct the public key from the JWKS data + if key_data["kty"] == "RSA": + # Convert the modulus and exponent to a public key + from cryptography.hazmat.primitives.asymmetric import rsa, padding + from cryptography.hazmat.primitives import serialization + + # Create public key in PEM format + public_numbers = rsa.RSAPublicNumbers( + n=int.from_bytes( + base64.urlsafe_b64decode(key_data["n"] + "=="), byteorder="big" + ), + e=int.from_bytes( + base64.urlsafe_b64decode(key_data["e"] + "=="), byteorder="big" + ), + ) + public_key = public_numbers.public_key(default_backend()) + + # Verify and decode the token + decoded = jwt.decode( + token, + key=public_key, + algorithms=["RS256"], + audience=auth_config.client_id, + issuer=auth_config.issuer, + options={"verify_exp": True, "verify_aud": True, "verify_iss": True}, + ) + + return decoded + + else: + raise AuthError("Unsupported key type") + + except PyJWTError as e: + raise AuthError(f"Token verification failed: {str(e)}") + + +def require_auth(f): + """Decorator to require authentication on endpoints""" + + @wraps(f) + def decorated(*args, **kwargs): + auth_header = request.headers.get("Authorization", None) + if not auth_header: + return jsonify({"error": "No authorization header"}), 401 + + try: + # Extract token from "Bearer " + token = auth_header.split()[1] + claims = verify_token(token) + # Add verified claims to request context + request.auth_claims = claims + return f(*args, **kwargs) + except AuthError as e: + return jsonify({"error": str(e)}), 401 + except Exception as e: + return jsonify({"error": "Invalid authorization header"}), 401 + + return decorated diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 00000000..f9bef2bb --- /dev/null +++ b/backend/models.py @@ -0,0 +1,31 @@ +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime + +db = SQLAlchemy() + + +class Organization(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + subscription_plan = db.Column(db.String(50)) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + users = db.relationship("User", backref="organization", lazy=True) + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), unique=True, nullable=False) + azure_id = db.Column(db.String(200), unique=True, nullable=False) + is_owner = db.Column(db.Boolean, default=False) + organization_id = db.Column(db.Integer, db.ForeignKey("organization.id")) + onboarding_completed = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + +class Invitation(db.Model): + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(120), nullable=False) + organization_id = db.Column(db.Integer, db.ForeignKey("organization.id")) + token = db.Column(db.String(200), unique=True, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + expired = db.Column(db.Boolean, default=False) diff --git a/backend/requirements.txt b/backend/requirements.txt index 4f4e9c74..8fa3be5a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,4 +7,10 @@ python-dotenv==1.0.0 azure-identity azure-keyvault-secrets azure-storage-blob==12.19.0 -stripe==10.5.0 \ No newline at end of file +stripe==10.5.0 +cachetools>=5.3.3,<5.6.0 +Flask-SQLAlchemy==2.5.1 +PyJWT==2.8.0 +python-jose==3.3.0 +ms_identity_python[flask] @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip +tenacity==8.5.0 diff --git a/backend/test_auth.py b/backend/test_auth.py new file mode 100644 index 00000000..aa656521 --- /dev/null +++ b/backend/test_auth.py @@ -0,0 +1,41 @@ +import requests +import webbrowser +from urllib.parse import urlencode +import json + +BASE_URL = "http://localhost:8000" + + +def test_auth_flow(): + # Get login URL + login_response = requests.get(f"{BASE_URL}/api/auth/login") + login_url = login_response.json()["loginUrl"] + + print("Opening browser for login...") + print(login_url) + webbrowser.open(login_url) + + # Wait for user to complete login + print("\nAfter logging in, copy the code from the URL and paste it here:") + code = input("Code: ") + + # Complete authentication + callback_url = f"{BASE_URL}/api/auth/callback?code={code}" + callback_response = requests.get(callback_url) + + if callback_response.status_code == 200: + token = callback_response.json()["token"] + + # Test protected route + protected_response = requests.get( + f"{BASE_URL}/api/protected", headers={"Authorization": f"Bearer {token}"} + ) + + print("\nProtected Route Response:") + print(json.dumps(protected_response.json(), indent=2)) + else: + print("Authentication failed:", callback_response.json()) + + +if __name__ == "__main__": + test_auth_flow() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 68a25057..68a1c0fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,8 @@ "name": "frontend", "version": "1.0.0", "dependencies": { + "@azure/msal-browser": "^3.26.1", + "@azure/msal-react": "^2.1.1", "@cyntler/react-doc-viewer": "^1.14.1", "@fluentui/react": "^8.105.3", "@fluentui/react-icons": "^2.0.195", @@ -54,6 +56,40 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/msal-browser": { + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.26.1.tgz", + "integrity": "sha512-y78sr9g61aCAH9fcLO1um+oHFXc1/5Ap88RIsUSuzkm0BHzFnN+PXGaQeuM1h5Qf5dTnWNOd6JqkskkMPAhh7Q==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.15.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", + "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-react": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@azure/msal-react/-/msal-react-2.1.1.tgz", + "integrity": "sha512-XOBgAR0fbkfUUkQZyhIlwAZiy8WIzNWxAAFacJgyL8LiE2Y5pgM9var9X0Jh7Oz/1Oy5lhTQDtCYWSMHmTZ84Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@azure/msal-browser": "^3.25.0", + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 16d81df4..d074fd40 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "watch": "tsc && vite build --watch" }, "dependencies": { + "@azure/msal-browser": "^3.26.1", + "@azure/msal-react": "^2.1.1", "@cyntler/react-doc-viewer": "^1.14.1", "@fluentui/react": "^8.105.3", "@fluentui/react-icons": "^2.0.195", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..dec85a4c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,54 @@ +import React, { useState, useEffect } from "react"; +import { Routes, Route, useNavigate } from "react-router-dom"; +import ProtectedRoute from "./router/ProtectedRoute"; +import Layout from "./pages/layout/Layout"; +import NoPage from "./pages/NoPage"; +import AccessDenied from "./pages/AccesDenied"; +import Chat from "./pages/chat/Chat"; +import Admin from "./pages/admin/Admin"; +import Onboarding from "./pages/onboarding/Onboarding"; +import Invitations from "./pages/invitations/Invitations"; +import Organization from "./pages/organization/Organization"; + +import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; +import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; + +export default function App() { + return ( + + {/* Public Routes */} + } /> + } /> + + {/* Access Denied Route */} + } /> + + {/* Protected Routes for Authenticated Users (Regular and Admin) */} + }> + }> + {/* Regular User and Admin Routes */} + } /> + } /> + + + + {/* Protected Routes for Admin Only */} + }> + }> + } /> + } /> + } /> + + + + {/* Catch-All Route for Undefined Paths */} + } /> + + ); +} + +// Define Organization interface based on your application's structure +interface Organization { + subscriptionId: string | null; + // Add other properties as needed +} diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 3de7f7b7..6584a05c 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -44,7 +44,7 @@ export async function deleteUser({ user, userId }: any): Promise { method: "DELETE", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": user.id, + "X-MS-CLIENT-PRINCIPAL-ID": user.id } }); const fetchedData = await response.json(); @@ -70,7 +70,6 @@ export async function checkUser({ user }: any): Promise { email: user.email }) }); - const parsedResponse = await response.json(); if (response.status > 299 || !response.ok) { throw Error("Unknown error in checkUser"); @@ -324,7 +323,7 @@ export async function inviteUser({ username, email, organizationId }: any): Prom return { error: error }; } } -export async function createInvitation({organizationId, invitedUserEmail, userId, role} : any): Promise { +export async function createInvitation({ organizationId, invitedUserEmail, userId, role }: any): Promise { try { const response = await fetch("/api/createInvitation", { method: "POST", @@ -386,7 +385,7 @@ export async function createCheckoutSession({ userId, priceId, successUrl, cance const response = await fetch("/create-checkout-session", { method: "POST", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json" }, body: JSON.stringify({ userId, @@ -429,8 +428,8 @@ export async function getOrganizationSubscription({userId, organizationId} : any method: "GET", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId, - }, + "X-MS-CLIENT-PRINCIPAL-ID": userId + } }); if (response.status > 299 || !response.ok) { @@ -439,7 +438,6 @@ export async function getOrganizationSubscription({userId, organizationId} : any const subscription = await response.json(); return subscription; - } export const createOrganization = async ({ userId, organizationName }: any) => { @@ -447,7 +445,7 @@ export const createOrganization = async ({ userId, organizationName }: any) => { method: "POST", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId, + "X-MS-CLIENT-PRINCIPAL-ID": userId }, body: JSON.stringify({ organizationName diff --git a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx index 9f54399f..562744b8 100644 --- a/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx +++ b/frontend/src/components/ChatHistoryButton/ChatHistoryButton.tsx @@ -2,7 +2,7 @@ import { Text } from "@fluentui/react"; import { HistoryRegular } from "@fluentui/react-icons"; import styles from "./ChatHistoryButton.module.css"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { useContext } from "react"; interface Props { @@ -12,7 +12,7 @@ interface Props { } export const ChatHistoryButton = ({ className, disabled, onClick }: Props) => { - const { showHistoryPanel } = useContext(AppContext); + const { showHistoryPanel } = useAppContext(); const buttonContent = showHistoryPanel ? "Hide chat history" : "Show chat history"; return ( - )} - + ); }; diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 18785fe1..25dfd4cd 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -2,30 +2,27 @@ import React, { useState, useEffect, useContext } from "react"; import { loadStripe, Stripe } from "@stripe/stripe-js"; import { Elements } from "@stripe/react-stripe-js"; import styles from "./PaymentGateway.module.css"; -import { getApiKeyPayment, createCheckoutSession, getProductPrices} from "../../api"; -import { AppContext } from "../../providers/AppProviders"; +import { getApiKeyPayment, createCheckoutSession, getProductPrices } from "../../api"; +import { useAppContext } from "../../providers/AppProviders"; import { Spinner } from "@fluentui/react"; import { ChartPerson48Regular } from "@fluentui/react-icons"; - const fetchApiKey = async () => { const apiKey = await getApiKeyPayment(); return apiKey; }; export const SubscriptionPlans: React.FC<{ stripePromise: Promise }> = ({ stripePromise }) => { - const { user, organization } = useContext(AppContext); + const { user, organization } = useAppContext(); - const [currentPlan, setCurrentPlan] = useState(organization.subscriptionId ? 1 : 0); const [prices, setPrices] = useState([]); const [error, setError] = useState(null); - useEffect(() => { // Fetch product prices when the component mounts async function fetchPrices() { try { - const data = await getProductPrices({user}); + const data = await getProductPrices({ user }); setPrices(data.prices); } catch (err) { console.error("Failed to fetch product prices:", err); @@ -41,6 +38,14 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise } const handleCheckout = async (priceId: string) => { + if (!user) { + // Handle the case when user is null + // You might redirect to a login page or show an error message + console.error("User is not authenticated."); + // Optionally, you can redirect the user or display a notification + return; + } + const { url } = await createCheckoutSession({ userId: user.id, priceId, @@ -60,11 +65,10 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise {prices.map((price, index) => ( <>
    - {currentPlan === index &&
    Current Subscription
    }

    {price.nickname}

    - ${(price.unit_amount/100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval} + ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval}

    {price.description}

    {price.id !== "free_plan" && ( @@ -74,9 +78,9 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise role="button" aria-label={`Subscribe to ${price.nickname}`} > - {organization.subscriptionId && organization.subscriptionStatus === "inactive" + {organization?.subscriptionId && organization.subscriptionStatus === "inactive" ? "Reactivate subscription" - : organization.subscriptionStatus === "active" + : organization?.subscriptionStatus === "active" ? "Edit payment information" : "Subscribe"} diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index 562b30b8..78495a89 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useContext } from "react"; import { Dropdown, DropdownMenuItemType, IDropdownOption, IDropdown } from "@fluentui/react/lib/Dropdown"; import { Icon } from "@fluentui/react/lib/Icon"; import { IStackTokens, Stack } from "@fluentui/react/lib/Stack"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import styles from "./Profile.module.css"; import person from "../../assets/person.png"; @@ -55,11 +55,12 @@ const placeholderPrepare = (placeholder: string) => { }; export const ProfileButton: React.FunctionComponent = () => { - const { user } = useContext(AppContext); - - const placeholder = placeholderPrepare(user.name); + const { user } = useAppContext(); + + const userName = user?.name || ""; // Default to empty string if user or user.name is null + const placeholder = placeholderPrepare(userName); const email = user?.email || " "; - const headerTitle = user.name; + const headerTitle = userName; const options: IDropdownOption[] = [ { key: "Header", text: headerTitle || "Options", itemType: DropdownMenuItemType.Header }, diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index e4832802..b1814768 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -1,5 +1,5 @@ import { useState, useContext } from "react"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; import { getTokenOrRefresh } from "./token_util"; import { Send24Filled, Mic24Regular, AttachRegular } from "@fluentui/react-icons"; @@ -169,13 +169,19 @@ export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: }; export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { - const { user, organization } = useContext(AppContext); + const { user, organization } = useAppContext(); const [question, setQuestion] = useState(""); const [fileBlobUrl, setFileBlobUrl] = useState(null); const sendQuestion = () => { - if (disabled || !question.trim() || organization.subscriptionStatus === "inactive" || !organization.subscriptionId) { + if ( + disabled || + !question.trim() || + !organization || // Check if organization is null or undefined + organization.subscriptionStatus === "inactive" || + !organization.subscriptionId + ) { return; } @@ -235,7 +241,12 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr } }; - const sendQuestionDisabled = disabled || !question.trim() || organization.subscriptionStatus === "inactive" || !organization.subscriptionId; + const sendQuestionDisabled = + disabled || + !question.trim() || + !organization || // Check if organization is null + organization.subscriptionStatus === "inactive" || + !organization.subscriptionId; return ( diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 08474ec1..ca381acd 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -5,7 +5,7 @@ import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Modal, Stack import styles from "./SettingsModal.module.css"; import { getSettings, postSettings } from "../../api/api"; import { mergeStyles } from "@fluentui/react/lib/Styling"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { Dialog, DialogContent, PrimaryButton } from "@fluentui/react"; interface Props { @@ -81,7 +81,7 @@ const ConfirmationDialog = ({ loading, isOpen, onDismiss, onConfirm }: { loading }; export const SettingsPanel = () => { - const { user, setSettingsPanel, settingsPanel } = useContext(AppContext); + const { user, setSettingsPanel, settingsPanel } = useAppContext(); const [temperature, setTemperature] = useState("0"); const [loading, setLoading] = useState(true); @@ -94,21 +94,31 @@ export const SettingsPanel = () => { useEffect(() => { const fetchData = async () => { - getSettings({ - user: { - id: user.id, - name: user.name - } - }) - .then(data => { - setTemperature(data.temperature); - setLoading(false); - }) - .catch(error => setLoading(false)); + if (!user) { + // User is not logged in; handle accordingly + setLoading(false); + return; + } + + setLoading(true); + + try { + const data = await getSettings({ + user: { + id: user.id, + name: user.name + } + }); + setTemperature(data.temperature); + } catch (error) { + console.error("Error fetching settings:", error); + } finally { + setLoading(false); + } }; - setLoading(true); + fetchData(); - }, []); + }, [user]); const handleSubmit = () => { const parsedTemperature = parseFloat(temperature); @@ -170,6 +180,12 @@ export const SettingsPanel = () => { setSettingsPanel(false); }; + if (!user) { + setLoading(false); + // Display a message or render a different component + return
    Please log in to view your settings.
    ; + } + return (
    = ({ isCollapsed, setIsCollapsed }) => { const [activeOption, setActiveOption] = useState("Chat"); - const { user, organization } = useContext(AppContext); + const { user, organization } = useAppContext(); const handleSubscriptionRedirect = (e: React.MouseEvent) => { e.preventDefault(); setActiveOption("Subscription"); - if (organization.subscriptionId) { + if (organization?.subscriptionId) { window.location.href = "https://dashboard.stripe.com/dashboard"; } else { window.location.href = "#/payment"; @@ -60,7 +60,7 @@ export const SideMenu: React.FC = ({ isCollapsed, setIsCollapsed {!isCollapsed && "Roles and access"} - {user.role === "admin" && ( + {user?.role === "admin" && (
  • = ({ isCollapsed, setIsCollapsed
  • )} - {user.role === "admin" && ( + {user?.role === "admin" && (
  • = ({ isCollapsed, setIsCollapsed
  • )} - {user.role === "admin" && ( + {user?.role === "admin" && (
  • - - {!organization.subscriptionId &&( - <> - } /> - } /> - } /> - - )} - {organization.subscriptionId && ( - <> - }> - } /> - } /> - - }> - - - - } - /> - } /> - - }> - } /> - } /> - - }> - } /> - } /> - - }> - } /> - } /> - - }> - - - - } - /> - } /> - - }> - - - - } - /> - } /> - - - )} - - + root.render( + + + + + + + ); +} else { + console.error("Failed to find the root element to mount React application."); } - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - -); diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index bb3851fa..0de671c1 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -4,7 +4,7 @@ import { ToastContainer, toast } from "react-toastify"; import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; import { AddFilled, DeleteRegular, EditRegular, SearchRegular } from "@fluentui/react-icons"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; import { checkUser, getUsers, inviteUser, createInvitation, deleteUser } from "../../api"; @@ -16,8 +16,7 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [role, setRole] = useState("user"); - const { user } = useContext(AppContext); - + const { user } = useAppContext(); const [errorMessage, setErrorMessage] = useState(""); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); @@ -37,29 +36,63 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; return users.some((user: any) => user.data.email === sanitizedEmail); }; - const handleSubmit = () => { + const handleSubmit = async () => { + // Sanitize inputs const sanitizedUsername = DOMPurify.sanitize(username); const sanitizedEmail = DOMPurify.sanitize(email); + + // Validate inputs if (!isValidated(sanitizedUsername, sanitizedEmail)) return; - if (alreadyExists(sanitizedEmail)) return setErrorMessage("User with this email already exists"); + + // Check if user already exists + if (alreadyExists(sanitizedEmail)) { + setErrorMessage("User with this email already exists"); + return; + } + + // Check if `user` is not null + if (!user) { + // Handle unauthenticated user + setErrorMessage("You must be logged in to invite a user."); + return; + } + setLoading(true); - const organizationId = user.organizationId; - inviteUser({ username: sanitizedUsername, email: sanitizedEmail, role, organizationId }).then(res => { - if (res.error) { - setErrorMessage(res.error); + try { + const organizationId = user.organizationId; + const inviteResponse = await inviteUser({ + username: sanitizedUsername, + email: sanitizedEmail, + role, + organizationId + }); + + if (inviteResponse.error) { + setErrorMessage(inviteResponse.error); + setLoading(false); + return; + } + + const invitationResponse = await createInvitation({ + organizationId, + invitedUserEmail: sanitizedEmail, + userId: user.id, + role + }); + + if (invitationResponse.error) { + setErrorMessage(invitationResponse.error); } else { - createInvitation({ organizationId, invitedUserEmail: sanitizedEmail, userId: user.id, role: role }).then(res => { - if (res.error) { - setErrorMessage(res.error); - } else { - setErrorMessage(""); - setLoading(false); - setSuccess(true); - } - }); + setErrorMessage(""); + setSuccess(true); } - }); + } catch (error) { + console.error("Error during invitation process:", error); + setErrorMessage("An unexpected error occurred. Please try again."); + } finally { + setLoading(false); + } }; const onUserNameChange = (_ev: React.FormEvent, newValue?: string) => { @@ -316,7 +349,7 @@ export const DeleteUserDialog = ({ isOpen, onDismiss, onConfirm }: { isOpen: boo }; const Admin = () => { - const { user } = useContext(AppContext); + const { user } = useAppContext(); const [search, setSearch] = useState(""); const [filteredUsers, setFilteredUsers] = useState([]); const [selectedUser, setSelectedUser] = useState({ @@ -334,18 +367,49 @@ const Admin = () => { const [isOpen, setIsOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); + if (!user) { + return
    Please log in to view the user list.
    ; + } + + useEffect(() => { const getUserList = async () => { - let usersList = await getUsers({user: {id: user.id, name: user.name, organizationId: user.organizationId}}); - if (!Array.isArray(usersList)) { - usersList = []; + if (!user) { + // User is not logged in + setUsers([]); + setFilteredUsers([]); + setLoading(false); + return; + } + + setLoading(true); + + try { + let usersList = await getUsers({ + user: { + id: user.id, + name: user.name, + organizationId: user.organizationId + } + }); + + if (!Array.isArray(usersList)) { + usersList = []; + } + + setUsers(usersList); + setFilteredUsers(usersList); + } catch (error) { + console.error("Error fetching user list:", error); + setUsers([]); + setFilteredUsers([]); + } finally { + setLoading(false); } - setUsers(usersList); - setFilteredUsers(usersList); - setLoading(false); }; + getUserList(); - }, []); + }, [user]); useEffect(() => { if (!search) { @@ -393,6 +457,7 @@ const Admin = () => { > { }} />
  • - - + + {loading?null:} { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 3ddb6cc9..01b14753 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -4,17 +4,7 @@ import { AddRegular, BroomRegular, SparkleFilled, TabDesktopMultipleBottomRegula import styles from "./Chat.module.css"; -import { - chatApiGpt, - Approaches, - AskResponse, - ChatRequest, - ChatRequestGpt, - ChatTurn, - getUserInfo, - checkUser, - getOrganizationSubscription -} from "../../api"; +import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, ChatTurn, getUserInfo, checkUser, getOrganizationSubscription } from "../../api"; import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; import { QuestionInput } from "../../components/QuestionInput"; import { ExampleList } from "../../components/Example"; @@ -25,7 +15,7 @@ import { getTokenOrRefresh } from "../../components/QuestionInput/token_util"; import { SpeechConfig, AudioConfig, SpeechSynthesizer, ResultReason } from "microsoft-cognitiveservices-speech-sdk"; import { getFileType } from "../../utils/functions"; import salesLogo from "../../img/logo.png"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; import { FeedbackRating } from "../../components/FeedbackRating/FeedbackRating"; import { SettingsPanel } from "../../components/SettingsPanel"; @@ -43,7 +33,7 @@ if (userLanguage.startsWith("pt")) { const Chat = () => { // speech synthesis is disabled by default - const { organization } = useContext(AppContext); + const { organization } = useAppContext(); const speechSynthesisEnabled = false; const [placeholderText, setPlaceholderText] = useState(""); @@ -64,7 +54,7 @@ const Chat = () => { setDataConversation, chatId, conversationIsLoading, - setRefreshFetchHistorial, + setRefreshFetchHistory, setChatId, setChatSelected, setChatIsCleaned, @@ -72,7 +62,7 @@ const Chat = () => { settingsPanel, setUser, setOrganization - } = useContext(AppContext); + } = useAppContext(); const lastQuestionRef = useRef(""); const lastFileBlobUrl = useRef(""); @@ -126,10 +116,10 @@ const Chat = () => { const result = await chatApiGpt(request); const conditionOne = answers.map(a => ({ user: a[0] })); if (conditionOne.length <= 0) { - setRefreshFetchHistorial(true); + setRefreshFetchHistory(true); setChatId(result.conversation_id); } else { - setRefreshFetchHistorial(false); + setRefreshFetchHistory(false); } setAnswers([...answers, [question, result]]); setUserId(result.conversation_id); diff --git a/frontend/src/pages/invitations/Invitations.tsx b/frontend/src/pages/invitations/Invitations.tsx index e3f64622..e1b63d58 100644 --- a/frontend/src/pages/invitations/Invitations.tsx +++ b/frontend/src/pages/invitations/Invitations.tsx @@ -3,7 +3,7 @@ import { Spinner } from "@fluentui/react"; import { TextField } from "@fluentui/react/lib/TextField"; import { SearchRegular } from "@fluentui/react-icons"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { getInvitations } from "../../api"; @@ -17,7 +17,7 @@ interface User { } const Invitations = () => { - const { user } = useContext(AppContext); + const { user } = useAppContext(); const [search, setSearch] = useState(""); const [filteredUsers, setFilteredUsers] = useState([]); const [users, setUsers] = useState([]); @@ -25,16 +25,41 @@ const Invitations = () => { useEffect(() => { const getUserList = async () => { - let invitationsList = await getInvitations({ user: { id: user.id, username: user.name, organizationId: user.organizationId } }); - if (!Array.isArray(invitationsList)) { - invitationsList = []; + if (!user) { + setUsers([]); + setFilteredUsers([]); + setLoading(false); + return; + } + + setLoading(true); + + try { + let invitationsList = await getInvitations({ + user: { + id: user.id, + username: user.name, + organizationId: user.organizationId + } + }); + + if (!Array.isArray(invitationsList)) { + invitationsList = []; + } + + setUsers(invitationsList); + setFilteredUsers(invitationsList); + } catch (error) { + console.error("Error fetching invitations:", error); + setUsers([]); + setFilteredUsers([]); + } finally { + setLoading(false); } - setUsers(invitationsList); - setFilteredUsers(invitationsList); - setLoading(false); }; + getUserList(); - }, []); + }, [user]); useEffect(() => { if (!search) { @@ -47,109 +72,114 @@ const Invitations = () => { } }, [search]); + if (!user) { + return
    Please log in to view your invitations.
    ; + } + + if (loading) { + return
    Loading invitations...
    ; + } + return (
    - {user.role !== "admin" &&

    Access denied

    } - {user.role === "admin" && ( - <> -
    -

    Invitations

    -
    -
    - +
    +

    Invitations

    +
    +
    + { - setSearch(newValue || ""); - }} - iconProps={{ - iconName: "Search", - children: - }} - /> -
    - {loading ? ( - - ) : ( -
    - - - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - - - + + ); + })} + +
    EmailRoleStatus
    {user.invited_user_email} -
    -
    - {user.role} -
    + } + }} + onChange={(_ev, newValue) => { + setSearch(newValue || ""); + }} + iconProps={{ + iconName: "Search", + children: + }} + /> +
    + {loading ? ( + + ) : ( +
    + + + + + + + + + + {filteredUsers.map((user: any, index) => { + return ( + + + - + - - ); - })} - -
    EmailRoleStatus
    {user.invited_user_email} +
    +
    + {user.role}
    -
    -
    -
    - {user.active ? "Active" : "Inactive"} -
    +
    +
    +
    +
    + {user.active ? "Active" : "Inactive"}
    -
    -
    - )} - - )} + +
    +
    + )} +
    ); }; diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx index f2b69b29..6383be45 100644 --- a/frontend/src/pages/layout/Layout.tsx +++ b/frontend/src/pages/layout/Layout.tsx @@ -1,4 +1,6 @@ import React, { useContext, useEffect, useState } from "react"; +import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react"; + import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; import { getChatHistory } from "../../api"; //FUNCION DE LA API import salesLogo from "../../img/logo.png"; @@ -8,15 +10,14 @@ import github from "../../assets/github.svg"; import styles from "./Layout.module.css"; import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; import { FeedbackRatingButton } from "../../components/FeedbackRating/FeedbackRatingButton"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import { SettingsButton } from "../../components/SettingsButton"; import { ButtonPaymentGateway } from "../../components/PaymentGateway/ButtonPaymentGateway"; import { SideMenu } from "../../components/SideMenu/SideMenu"; import { ProfileButton } from "../../components/Profile"; const Layout = () => { - const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = - useContext(AppContext); + const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); const { pathname } = useLocation(); const [isCollapsed, setIsCollapsed] = useState(false); @@ -40,32 +41,34 @@ const Layout = () => { }; return ( -
    - -
    -
    -
    - -
    - {pathname === "/" && ( - <> - - - - - )} -
    - + <> +
    + +
    +
    +
    + +
    + {pathname === "/" && ( + <> + + + + + )} +
    + +
    -
    -
    + - -
    -
    + + +
    + ); }; diff --git a/frontend/src/pages/onboarding/Onboarding.tsx b/frontend/src/pages/onboarding/Onboarding.tsx index e2613f2d..21a3042e 100644 --- a/frontend/src/pages/onboarding/Onboarding.tsx +++ b/frontend/src/pages/onboarding/Onboarding.tsx @@ -1,38 +1,47 @@ -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; +import { Navigate } from "react-router-dom"; import salesLogo from "../../img/logo.png"; import styles from "./Onboarding.module.css"; import { ChevronRightRegular, ChevronLeftRegular, ContactCardRibbon48Regular, MoneySettingsRegular } from "@fluentui/react-icons"; import { Spinner } from "@fluentui/react"; import { createOrganization, getOrganizationSubscription } from "../../api"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; const Onboarding: React.FC = () => { - const { user, setUser, organization, setOrganization } = useContext(AppContext); - + const { user, setUser, organization, setOrganization } = useAppContext(); const [organizationName, setOrganizationName] = useState(""); const [step, setStep] = useState(0); const [isLoadingStep, setIsLoadingStep] = useState(false); const maxSteps = 2; - const handleOrganizationChange = (event: React.ChangeEvent) => { setOrganizationName(event.target.value); }; const handleCreateOrganization = async () => { + if (!user) { + return null; + } const newOrganization = await createOrganization({ userId: user.id, organizationName: organizationName }); if (newOrganization.id) { setOrganization(newOrganization); setUser({ ...user, organizationId: newOrganization.id }); + return newOrganization; } }; const handleNextClick = async () => { + if (user?.organizationId) { + setStep(2); + return; + } if (step < maxSteps) { setIsLoadingStep(true); + let organization = null; if (step === 1) { - await handleCreateOrganization(); + organization = await handleCreateOrganization(); } + setStep(prevStep => prevStep + 1); setIsLoadingStep(false); } @@ -48,6 +57,9 @@ const Onboarding: React.FC = () => { window.location.href = "#/payment"; }; + if (user?.organizationId && organization?.subscriptionId) { + return ; + } return (
    diff --git a/frontend/src/pages/organization/Organization.tsx b/frontend/src/pages/organization/Organization.tsx index 99307060..03821623 100644 --- a/frontend/src/pages/organization/Organization.tsx +++ b/frontend/src/pages/organization/Organization.tsx @@ -1,61 +1,63 @@ import React, { useContext } from "react"; import { Label } from "@fluentui/react"; import { Globe32Regular } from "@fluentui/react-icons"; -import { AppContext } from "../../providers/AppProviders"; +import { useAppContext } from "../../providers/AppProviders"; import styles from "./Organization.module.css"; const Organization = () => { - const { organization } = useContext(AppContext); - const { user } = useContext(AppContext); + const { organization } = useAppContext(); + + if (!organization) { + return ( +
    +

    No Organization

    +
    + ); + } return (
    - {user.role !== "admin" &&

    Access denied

    } - {user.role === "admin" && ( - <> -
    -

    Organization

    +
    +

    Organization

    +
    +
    +
    + +
    +
    + + {organization?.id} +
    +
    +
    +
    + + {organization?.name} +
    +
    + + {organization?.owner} +
    +
    +
    +
    + + {organization?.subscriptionId} +
    -
    -
    - -
    -
    - - {organization.id} -
    -
    -
    -
    - - {organization.name} -
    -
    - - {organization.owner} -
    -
    -
    -
    - - {organization.subscriptionId} -
    -
    -
    -
    - - {organization.subscriptionStatus} -
    -
    - - {organization.subscriptionExpirationDate} -
    -
    +
    +
    + + {organization?.subscriptionStatus} +
    +
    + + {organization?.subscriptionExpirationDate}
    - - )} +
    +
    ); }; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 1a1d5237..238bbe61 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -1,12 +1,13 @@ -import React, { createContext, useContext, useState, ReactNode, Dispatch, SetStateAction, useEffect } from "react"; -import { ConversationHistoryItem, ConversationChatItem, ChatTurn } from "../api"; +import React, { createContext, useContext, useState, useEffect, useCallback, useMemo, ReactNode, Dispatch, SetStateAction } from "react"; import { Spinner } from "@fluentui/react"; -import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, getUserInfo, checkUser, getOrganizationSubscription } from "../api"; +import { checkUser, getOrganizationSubscription } from "../api"; + +// Define interfaces for UserInfo and OrganizationInfo interface UserInfo { id: string; name: string; email: string | null; - role: string | undefined; + role?: string; organizationId?: string; } @@ -14,28 +15,46 @@ interface OrganizationInfo { id: string; name: string; owner: string; - subscriptionId: string | undefined; - subscriptionStatus: string | undefined; - subscriptionExpirationDate: number | undefined; + subscriptionId?: string; + subscriptionStatus?: string; + subscriptionExpirationDate?: number; +} + +// In types.ts +export interface ConversationHistoryItem { + id: string; + start_date: string; + content: string; + // Add other properties as needed } +export interface ChatTurn { + user: string; + bot?: { + message: string; + thoughts: any; + } | null; + // Add other properties as needed +} + +// Define the shape of the context interface AppContextType { showHistoryPanel: boolean; setShowHistoryPanel: Dispatch>; - newChatDeleted: boolean; - setNewChatDeleted: Dispatch>; showFeedbackRatingPanel: boolean; setShowFeedbackRatingPanel: Dispatch>; - refreshFetchHistorial: boolean; - setRefreshFetchHistorial: Dispatch>; + settingsPanel: boolean; + setSettingsPanel: Dispatch>; + refreshFetchHistory: boolean; + setRefreshFetchHistory: Dispatch>; chatIsCleaned: boolean; setChatIsCleaned: Dispatch>; dataHistory: ConversationHistoryItem[]; setDataHistory: Dispatch>; - user: UserInfo; - setUser: Dispatch>; - organization: OrganizationInfo; - setOrganization: Dispatch>; + user: UserInfo | null; + setUser: Dispatch>; + organization: OrganizationInfo | null; + setOrganization: Dispatch>; chatSelected: string; setChatSelected: Dispatch>; chatId: string; @@ -44,187 +63,214 @@ interface AppContextType { setDataConversation: Dispatch>; conversationIsLoading: boolean; setConversationIsLoading: Dispatch>; - settingsPanel: boolean; - setSettingsPanel: Dispatch>; + newChatDeleted: boolean; + setNewChatDeleted: Dispatch>; + isAuthenticated: boolean; + isLoading: boolean; } -export const AppContext = createContext({ - showHistoryPanel: true, - setShowHistoryPanel: () => {}, - showFeedbackRatingPanel: false, - setShowFeedbackRatingPanel: () => {}, - dataHistory: [], - setDataHistory: () => {}, - user: { - id: "00000000-0000-0000-0000-000000000000", - name: "anonymous", - email: "anonymous@gmail.com", - role: undefined, - organizationId: undefined - }, - setUser: () => {}, - organization: { - id: "00000000-0000-0000-0000-000000000000", - name: "no-organization", - owner: "no-owner", - subscriptionId: undefined, - subscriptionStatus: undefined, - subscriptionExpirationDate: undefined - }, - setOrganization: () => {}, - dataConversation: [], - setDataConversation: () => {}, - chatId: "", - setChatId: () => {}, - conversationIsLoading: false, - setConversationIsLoading: () => {}, - refreshFetchHistorial: false, - setRefreshFetchHistorial: () => {}, - chatSelected: "", - setChatSelected: () => {}, - chatIsCleaned: false, - setChatIsCleaned: () => {}, - settingsPanel: false, - setSettingsPanel: () => {}, - newChatDeleted: false, - setNewChatDeleted: () => {} -}); +// Create the context with a default value +export const AppContext = createContext(undefined); +// Create the provider component export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + // State variables const [showHistoryPanel, setShowHistoryPanel] = useState(true); const [showFeedbackRatingPanel, setShowFeedbackRatingPanel] = useState(false); - const [refreshFetchHistorial, setRefreshFetchHistorial] = useState(false); + const [settingsPanel, setSettingsPanel] = useState(false); + const [refreshFetchHistory, setRefreshFetchHistory] = useState(false); + const [chatIsCleaned, setChatIsCleaned] = useState(false); const [dataHistory, setDataHistory] = useState([]); const [dataConversation, setDataConversation] = useState([]); - const [user, setUser] = useState({ - id: "00000000-0000-0000-0000-000000000000", - name: "anonymous", - email: "anonymous@gmail.com", - role: undefined, - organizationId: undefined - }); - const [organization, setOrganization] = useState({ - id: "00000000-0000-0000-0000-000000000000", - name: "no-organization", - owner: "no-owner", - subscriptionId: undefined, - subscriptionStatus: undefined, - subscriptionExpirationDate: undefined - }); + const [chatSelected, setChatSelected] = useState(""); const [chatId, setChatId] = useState(""); const [conversationIsLoading, setConversationIsLoading] = useState(false); - const [chatIsCleaned, setChatIsCleaned] = useState(false); - const [chatSelected, setChatSelected] = useState(""); - const [settingsPanel, setSettingsPanel] = useState(false); - const [newChatDeleted, setNewChatDeleted] = useState(false); + const [newChatDeleted, setNewChatDeleted] = useState(false); + const [user, setUser] = useState(null); + const [organization, setOrganization] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isAuthenticated, setIsAuthenticated] = useState(false); + // MSAL instance and active account - const handleKeyDown = (event: KeyboardEvent) => { - const isCtrlOrCmd = event.ctrlKey || event.metaKey; - const isAlt = event.altKey; + // Handle keyboard shortcuts + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + const isCtrlOrCmd = event.ctrlKey || event.metaKey; + const isAlt = event.altKey; - if (event.code === "Digit8" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - setShowFeedbackRatingPanel(!showFeedbackRatingPanel); - setSettingsPanel(false); - setShowHistoryPanel(false); - } else if (event.code === "Period" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - setShowHistoryPanel(!showHistoryPanel); - setShowFeedbackRatingPanel(false); - setSettingsPanel(false); - } else if (event.code === "Comma" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - setSettingsPanel(!settingsPanel); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - } else if (event.code === "Digit0" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; - } else if (event.code === "Digit9" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - window.location.href = "#/admin"; - } else if (event.code === "Digit7" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - window.location.href = "#/payment"; - } - }; - - window.addEventListener("keydown", handleKeyDown); - //If I add this on a useEffect it doesn't work, I don't know why - //maybe because it's a global event listener and is called multiple times + if (isCtrlOrCmd && isAlt) { + switch (event.code) { + case "Digit8": + event.preventDefault(); + setShowFeedbackRatingPanel(prev => !prev); + setSettingsPanel(false); + setShowHistoryPanel(false); + break; + case "Period": + event.preventDefault(); + setShowHistoryPanel(prev => !prev); + setShowFeedbackRatingPanel(false); + setSettingsPanel(false); + break; + case "Comma": + event.preventDefault(); + setSettingsPanel(prev => !prev); + setShowHistoryPanel(false); + setShowFeedbackRatingPanel(false); + break; + case "Digit0": + event.preventDefault(); + window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; + break; + case "Digit9": + event.preventDefault(); + window.location.href = "#/admin"; + break; + case "Digit7": + event.preventDefault(); + window.location.href = "#/payment"; + break; + default: + break; + } + } + }, + [setShowFeedbackRatingPanel, setSettingsPanel, setShowHistoryPanel] + ); - const [loading, setLoading] = useState(true); + // Add event listener for keyboard shortcuts + useEffect(() => { + window.addEventListener("keydown", handleKeyDown); + return () => { + window.removeEventListener("keydown", handleKeyDown); + }; + }, [handleKeyDown]); + // Fetch user and organization info useEffect(() => { - if (!user.organizationId) { - const getUserInfoList = async () => { - if (window.location.hostname !== "127.0.0.1" && window.location.hostname !== "localhost") { - const userInfoList = await getUserInfo(); - if (userInfoList.length === 0) { - // setShowAuthMessage(true); - console.log("No user info found. Using anonymous user.", userInfoList); - } else { - // setShowAuthMessage(false); - console.log("User info found.", userInfoList); + const fetchUserAuth = async (invitationToken?: string | null) => { + let url = "/api/auth/user"; + if (invitationToken) { + const params = new URLSearchParams({ invitation: invitationToken }); + url += `?${params.toString()}`; + } - const keyId = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"; - const keyName = "name"; - const keyEmail = "emails"; + const response = await fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json" + } + }); - const _user = userInfoList.find(obj => { - const _id = obj?.user_claims?.some(claim => claim.typ === keyId); - const _name = obj?.user_claims?.some(claim => claim.typ === keyName); - return _id && _name; - }); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } - if (_user) { - const id = _user?.user_claims?.find(claim => claim.typ === keyId)?.val || ""; - const name = _user?.user_claims?.find(claim => claim.typ === keyName)?.val || ""; - const email = _user?.user_claims?.find(claim => claim.typ === keyEmail)?.val || null; + return response.json(); + }; - if (id && name) { - setUser({ id, name, email, role: undefined, organizationId: undefined }); - } + const fetchOrganizationDetails = async (userId: string, organizationId: string) => { + try { + const organization = await getOrganizationSubscription({ + userId, + organizationId + }); - // register user if doesn't exist + if (organization) { + setOrganization({ + id: organization.id, + name: organization.name, + owner: organization.owner, + subscriptionId: organization.subscriptionId, + subscriptionStatus: organization.subscriptionStatus, + subscriptionExpirationDate: organization.subscriptionExpirationDate + }); + } + } catch (error) { + console.error("Failed to fetch organization details:", error); + throw error; + } + }; - // const response = await getUsers({ user: { id, name, email } }); // to get all users + const initialize = async () => { + try { + const urlParams = new URLSearchParams(window.location.search); + const invitationToken = urlParams.get("invitation"); - // verifies if user exists and assigns the role - const result = await checkUser({ user: { id, name, email } }); - const role = result["role"] || undefined; - const organizationId = result["organizationId"] || undefined; + const authData = await fetchUserAuth(invitationToken); - const organization = await getOrganizationSubscription({ userId: id, organizationId: organizationId }); + if (authData.authenticated) { + setUser(authData.user); + setIsAuthenticated(true); - if (result && role) { - setUser({ id, name, email, role, organizationId }); - } - if (organization) { - setOrganization({ - id: organization.id, - name: organization.name, - owner: organization.owner, - subscriptionId: organization.subscriptionId, - subscriptionStatus: organization.subscriptionStatus, - subscriptionExpirationDate: organization.subscriptionExpirationDate - }); - } - setLoading(false); - } + if (authData.user.id && authData.user.organizationId) { + await fetchOrganizationDetails(authData.user.id, authData.user.organizationId); } - } else { - console.log("Local"); - setUser({ ...user, organizationId: "test-organization" }); - setOrganization({ ...organization, subscriptionId: "test-subscriptionId" }); - setLoading(false); } - }; - getUserInfoList(); - } + } catch (error) { + console.error("Initialization failed:", error); + } finally { + setIsLoading(false); + } + }; + + initialize(); }, []); - if (loading) + // Memoize context value to prevent unnecessary re-renders + const contextValue = useMemo( + () => ({ + showHistoryPanel, + setShowHistoryPanel, + showFeedbackRatingPanel, + setShowFeedbackRatingPanel, + settingsPanel, + setSettingsPanel, + refreshFetchHistory, + setRefreshFetchHistory, + chatIsCleaned, + setChatIsCleaned, + dataHistory, + setDataHistory, + user, + setUser, + organization, + setOrganization, + chatSelected, + setChatSelected, + chatId, + setChatId, + dataConversation, + setDataConversation, + conversationIsLoading, + setConversationIsLoading, + newChatDeleted, + setNewChatDeleted, + isAuthenticated, + isLoading + }), + [ + showHistoryPanel, + showFeedbackRatingPanel, + settingsPanel, + refreshFetchHistory, + chatIsCleaned, + dataHistory, + user, + organization, + chatSelected, + chatId, + dataConversation, + conversationIsLoading, + newChatDeleted, + isAuthenticated, + isLoading + ] + ); + + // Render loading spinner if data is still loading + if (isLoading) { return (
    = ({ children }) =>
    ); + } - return ( - - {children} - - ); + // Provide the context to child components + return {children}; +}; + +// Custom hook for consuming the context +export const useAppContext = () => { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error("useAppContext must be used within an AppProvider"); + } + return context; }; diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index e42a9011..e853353a 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -1,18 +1,50 @@ -import React, { useContext, ReactNode } from 'react'; -import { AppContext } from "../providers/AppProviders"; -import { Navigate } from 'react-router-dom'; +// src/ProtectedRoute.jsx +import React from "react"; +import { Outlet, Navigate } from "react-router-dom"; +import { useAppContext } from "../providers/AppProviders"; +import LoadingSpinner from "../components/LoadingSpinner/LoadingSpinner"; // Optional: Create a loading spinner component +/** + * ProtectedRoute component ensures that only authenticated users with allowed roles can access certain routes. + * It uses MsalAuthenticationTemplate to handle authentication automatically. + * + * @param {Array} allowedRoles - Array of roles that are permitted to access the route. + */ interface ProtectedRouteProps { - children: ReactNode; - allowedRoles: string[]; + allowedRoles: string[]; } -const ProtectedRoute = ({ children, allowedRoles }: ProtectedRouteProps) => { - const { user } = useContext(AppContext); - if (!user.role || !allowedRoles.includes(user.role)) { - return ; - } - return (<>{children}); +const ProtectedRoute: React.FC = ({ allowedRoles }) => { + const { user, isAuthenticated, organization } = useAppContext(); + + console.log(isAuthenticated); + // Function to check if the user has at least one of the allowed roles + const hasRequiredRole = (): boolean => { + console.log(user); + const roles = [user?.role]; + //const roles = activeAccount.idTokenClaims?.roles as string[] | undefined; + if (!roles) return false; + return allowedRoles.some(role => roles.includes(role)); + }; + + const isValidSubscriptionForOrganization = (): boolean => { + if (!user?.organizationId || !organization?.subscriptionId) return false; + return true; + }; + + return ( + <> + {isValidSubscriptionForOrganization() && hasRequiredRole() ? ( + + ) : hasRequiredRole() === false ? ( + + ) : isValidSubscriptionForOrganization() === false ? ( + + ) : ( + + )} + + ); }; export default ProtectedRoute; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d711f0f9..db3d8e00 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,12 +11,32 @@ export default defineConfig({ }, server: { proxy: { - '/chatgpt': "http://localhost:8000", - '/api/get-speech-token': "http://localhost:8000", - '/api/get-storage-account': "http://localhost:8000", - '/api/get-blob': "http://localhost:8000", - '/api/settings': "http://localhost:8000", - '/api/conversations': "http://localhost:8000", + "/chatgpt": "http://localhost:8000", + "/api/stripe": "http://localhost:8000", + "/api/chat-history": "http://localhost:8000", + "/api/chat-conversation/": "http://localhost:8000", + "/api/chat-conversations/": "http://localhost:8000", + "/api/create-organization": "http://localhost:8000", + "/api/get-speech-token": "http://localhost:8000", + "/api/get-storage-account": "http://localhost:8000", + "/create-checkout-session": "http://localhost:8000", + "/webhook": "http://localhost:8000", + "/api/upload-blob": "http://localhost:8000", + "/api/get-blob": "http://localhost:8000", + "/api/settings": "http://localhost:8000", + "/api/feedback": "http://localhost:8000", + "/api/getusers": "http://localhost:8000", + "/api/deteleuser": "http://localhost:8000", + "/api/inviteUser": "http://localhost:8000", + "/api/getInvitations": "http://localhost:8000", + "/api/createInvitation": "http://localhost:8000", + "/api/checkuser": "http://localhost:8000", + "/api/get-organization-subscription": "http://localhost:8000", + "/api/getUser": "http://localhost:8000", + "/api/conversations": "http://localhost:8000", + "/api/chat": "http://localhost:8000", + "/api/auth/user": "http://localhost:8000", + "/api/prices": "http://localhost:8000" }, host: true } From 5572cdc7a4f069d3695cff4564530a3d74db092a Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Sat, 2 Nov 2024 08:13:21 -0400 Subject: [PATCH 163/820] FA-149 Fix logout (#138) --- backend/app.py | 13 +++++++++++++ frontend/src/components/Profile/index.tsx | 2 +- frontend/src/providers/AppProviders.tsx | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index 7c8a4c48..af22ca76 100644 --- a/backend/app.py +++ b/backend/app.py @@ -12,6 +12,7 @@ send_from_directory, redirect, url_for, + session, ) from flask_cors import CORS from dotenv import load_dotenv @@ -1073,6 +1074,18 @@ def deleteUser(): logging.exception("[webbackend] exception in /api/checkUser") return jsonify({"error": str(e)}), 500 +@app.route('/logout') +def logout(): + # Clear the user's session + session.clear() + # Build the Azure AD B2C logout URL + logout_url = ( + f"https://{os.getenv('AAD_TENANT_NAME')}.b2clogin.com/{os.getenv('AAD_TENANT_NAME')}.onmicrosoft.com/" + f"{os.getenv('AAD_POLICY_NAME')}/oauth2/v2.0/logout" + f"?p={os.getenv('AAD_POLICY_NAME')}" + f"&post_logout_redirect_uri={os.getenv('AAD_REDIRECT_URI')}" + ) + return redirect(logout_url) @app.route("/api/inviteUser", methods=["POST"]) def sendEmail(): diff --git a/frontend/src/components/Profile/index.tsx b/frontend/src/components/Profile/index.tsx index 78495a89..e7500dc9 100644 --- a/frontend/src/components/Profile/index.tsx +++ b/frontend/src/components/Profile/index.tsx @@ -81,7 +81,7 @@ export const ProfileButton: React.FunctionComponent = () => { setSelectedOption(selOption); if (selOption === "Logout") { - window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; + window.location.href = "/logout"; } }; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 238bbe61..2b7a9b16 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -120,7 +120,7 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => break; case "Digit0": event.preventDefault(); - window.location.href = "/.auth/logout?post_logout_redirect_uri=/"; + window.location.href = "/logout"; break; case "Digit9": event.preventDefault(); From 0cc1d5bfe78d9c44677af27a2cbcfcaacd2e7206 Mon Sep 17 00:00:00 2001 From: WanjiruMambo Date: Tue, 5 Nov 2024 12:13:14 -0500 Subject: [PATCH 164/820] created a python file stripeTestLiveApiValidation to ensure that financial agent product in stripe is set up well for test and live mode --- .DS_Store | Bin 0 -> 8196 bytes backend/.DS_Store | Bin 0 -> 6148 bytes backend/stripeTestLiveApiValidation.py | 62 +++++++ frontend/.DS_Store | Bin 0 -> 6148 bytes myenv/bin/Activate.ps1 | 247 +++++++++++++++++++++++++ myenv/bin/activate | 69 +++++++ myenv/bin/activate.csh | 26 +++ myenv/bin/activate.fish | 69 +++++++ myenv/bin/dotenv | 8 + myenv/bin/flask | 8 + myenv/bin/normalizer | 8 + myenv/bin/pip | 8 + myenv/bin/pip3 | 8 + myenv/bin/pip3.10 | 8 + myenv/bin/python | 1 + myenv/bin/python3 | 1 + myenv/bin/python3.10 | 1 + myenv/pyvenv.cfg | 3 + 18 files changed, 527 insertions(+) create mode 100644 .DS_Store create mode 100644 backend/.DS_Store create mode 100644 backend/stripeTestLiveApiValidation.py create mode 100644 frontend/.DS_Store create mode 100644 myenv/bin/Activate.ps1 create mode 100644 myenv/bin/activate create mode 100644 myenv/bin/activate.csh create mode 100644 myenv/bin/activate.fish create mode 100755 myenv/bin/dotenv create mode 100755 myenv/bin/flask create mode 100755 myenv/bin/normalizer create mode 100755 myenv/bin/pip create mode 100755 myenv/bin/pip3 create mode 100755 myenv/bin/pip3.10 create mode 120000 myenv/bin/python create mode 120000 myenv/bin/python3 create mode 120000 myenv/bin/python3.10 create mode 100644 myenv/pyvenv.cfg diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e9799dce389487168d9a069e15a00cf95f7da74a GIT binary patch literal 8196 zcmeHMze^lJ6n>LCa|td@dFQr{^ji^vhv`wp|Cu`*6L#d^r(b2yP|^sVi7l(GS#abspXFLU9(=p;gw=Cb$`PF3`!Yfu_>n+Gfq(AU z^7pfx|JNboXNZ9d<%m4L{r)xD!PBq}{x0rJ;i;%RhlT=z>sUML>j+Q-EyGcHJsM=@ z_ki7n(b-)XQTBghj5KlJ4mnV9TF1@#e-3M%ya%{LH&3LB1AoZ@Rjn`9XEB-G)k`Kd vyS&ELz-5}jWiHwhbh#e__`ClPLwweNnwr?h%tef#$%_DMgLLA+KXu?Y!1D(5 literal 0 HcmV?d00001 diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dbea4c5608b92853851d26707adbaeaccbbbfc8d GIT binary patch literal 6148 zcmeHKOG*Pl5UtXH1KDIHF8d0(!5GF9ycUDcmAOjoyv=w#iDMMff0pb(WxkI>xPb&#D0K(#S0vlJ1Q{I z04y_hj<7(~LxCQu`H7((4tvUeWya3Y!$oaojFV^HJzg~5kTsdRXz3VrFc1vvGjQn5 z5%d2Uex=?={(eZbf`MS*pE00=YFbUOshnFsY>&A%q0CSy^y{cVpj#>iS}I0^=g5h5 cdVivh{xW0d$Wdswa9~^njF6~;fnQ+Y6~5v%<^TWy literal 0 HcmV?d00001 diff --git a/backend/stripeTestLiveApiValidation.py b/backend/stripeTestLiveApiValidation.py new file mode 100644 index 00000000..4b3ebd20 --- /dev/null +++ b/backend/stripeTestLiveApiValidation.py @@ -0,0 +1,62 @@ +import os +import stripe +from dotenv import load_dotenv + +# load the environment variables from the .env file +load_dotenv() + +# set stripe api keys for test and live +STRIPE_API_KEY = os.getenv('STRIPE_API_KEY') +STRIPE_LIVE_API_KEY = os.getenv('STRIPE_LIVE_API_KEY') + +"""initialize stripe to ensure successful authentication""" +# initialize with the test api key, switching to the live key can be done later +stripe.api_key = STRIPE_API_KEY + +# product ids from both test and live environments +test_product_id = os.getenv('STRIPE_FINANCIAL_AGENT_TEST_ID') +live_product_id = os.getenv('STRIPE_FINANCIAL_AGENT_LIVE_ID') + +# function to validate that the test and live products are accessible and match the requirements in each model +def validate_products(): + try: + """retrieve product information for the test environment""" + # since stripe was initialized with the test api key, no switching is done here + test_product = stripe.Product.retrieve(test_product_id) + + """retrieve product information for the live environment""" + # switch api key to live mode + stripe.api_key = STRIPE_LIVE_API_KEY + live_product = stripe.Product.retrieve(live_product_id) + + """retrieve associated prices for both test and live products""" + # get the prices in live mode - no switching needed because api key is currently in live mode (see line 33) + live_prices = stripe.Price.list(product = live_product_id) + + # switch api key to test mode and retrieve the price for test + stripe.api_key = STRIPE_API_KEY + test_prices = stripe.Price.list(product = test_product_id) + + # get the first price object, assuming that there's only one price per product + live_price = live_prices['data'][0] + test_price = test_prices['data'][0] + + # validate if both products have the same name, price, and billing model + are_products_matching = ( + test_product['name'] == live_product['name'] and + test_price['unit_amount'] == live_price['unit_amount'] and + test_price['recurring']['interval'] == live_price['recurring']['interval'] + ) + + # display a message to show success or failure in the validation process + if are_products_matching: + print("Validation successful: Test and Live products match the requirements.") + else: + print("Validation failed: Test and Live products are not identical.") + + # exception handling + except Exception as e: + print(f"Error during product validation: {e}") + +# run the validation function +validate_products() diff --git a/frontend/.DS_Store b/frontend/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f5c32f4651cce67bf98f6cc1ff4e8d7cc8b7dbbd GIT binary patch literal 6148 zcmeHKJ5Iwu5Ph2j6hw-Y6cp}}5a}sQWLlauNGXW&Q9z1uAf!Mv7Bw|B*Wd!20Js4c z-~_zcT@pJckq{C>XhxcS_Uz2g_^svf0x->KzX8+%RH=fc4Gv$3%!?MJMx4xRTIQ3j z0|=##GA6iOsGxN9HlNAne!Y14%kd@P47k z)S(N9v6;D!&ush&#n|k$2i6@Xbf}{;pbX?0D7)Lb^#8-<=l^_=-YEmhz`tU^RH9bY z Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/myenv/bin/activate b/myenv/bin/activate new file mode 100644 index 00000000..2d78aa3e --- /dev/null +++ b/myenv/bin/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(myenv) ${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT="(myenv) " + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/myenv/bin/activate.csh b/myenv/bin/activate.csh new file mode 100644 index 00000000..bcd59dc4 --- /dev/null +++ b/myenv/bin/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "(myenv) $prompt" + setenv VIRTUAL_ENV_PROMPT "(myenv) " +endif + +alias pydoc python -m pydoc + +rehash diff --git a/myenv/bin/activate.fish b/myenv/bin/activate.fish new file mode 100644 index 00000000..76b8ad2b --- /dev/null +++ b/myenv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "(myenv) " (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT "(myenv) " +end diff --git a/myenv/bin/dotenv b/myenv/bin/dotenv new file mode 100755 index 00000000..cc6ca733 --- /dev/null +++ b/myenv/bin/dotenv @@ -0,0 +1,8 @@ +#!/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from dotenv.__main__ import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/myenv/bin/flask b/myenv/bin/flask new file mode 100755 index 00000000..31503501 --- /dev/null +++ b/myenv/bin/flask @@ -0,0 +1,8 @@ +#!/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from flask.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/myenv/bin/normalizer b/myenv/bin/normalizer new file mode 100755 index 00000000..43e39a1a --- /dev/null +++ b/myenv/bin/normalizer @@ -0,0 +1,8 @@ +#!/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer.cli import cli_detect +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_detect()) diff --git a/myenv/bin/pip b/myenv/bin/pip new file mode 100755 index 00000000..5618deeb --- /dev/null +++ b/myenv/bin/pip @@ -0,0 +1,8 @@ +#!/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/myenv/bin/pip3 b/myenv/bin/pip3 new file mode 100755 index 00000000..5618deeb --- /dev/null +++ b/myenv/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/myenv/bin/pip3.10 b/myenv/bin/pip3.10 new file mode 100755 index 00000000..5618deeb --- /dev/null +++ b/myenv/bin/pip3.10 @@ -0,0 +1,8 @@ +#!/Users/wanjirumambo/SalesFactory/gpt-rag-frontend/myenv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/myenv/bin/python b/myenv/bin/python new file mode 120000 index 00000000..b8a0adbb --- /dev/null +++ b/myenv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/myenv/bin/python3 b/myenv/bin/python3 new file mode 120000 index 00000000..cf0626ea --- /dev/null +++ b/myenv/bin/python3 @@ -0,0 +1 @@ +/Users/wanjirumambo/.pyenv/versions/3.10.11/bin/python3 \ No newline at end of file diff --git a/myenv/bin/python3.10 b/myenv/bin/python3.10 new file mode 120000 index 00000000..b8a0adbb --- /dev/null +++ b/myenv/bin/python3.10 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/myenv/pyvenv.cfg b/myenv/pyvenv.cfg new file mode 100644 index 00000000..29964456 --- /dev/null +++ b/myenv/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /Users/wanjirumambo/.pyenv/versions/3.10.11/bin +include-system-site-packages = false +version = 3.10.11 From 81d897dba47130dd2b5b76d5fb242d95f0ee4e66 Mon Sep 17 00:00:00 2001 From: Victor Maldonado Date: Tue, 5 Nov 2024 14:07:30 -0400 Subject: [PATCH 165/820] Fa 97 financial assistant endpoint (#139) * FA-97 Create Financial Assistant endpoint * change http error code from 400 to 500 * Update endpoint & Http response * Add docstring to clarify endpoint function behavior * add validation to subscriptionID * Add verification for financial Assistant Env variable * add specific metadata to the update subscription request * correct HTTP response codes for API endpoints * FA-97 abtratc response structures into function * abstract common utilities into an api_utils.py file * fix commit conflicts * change import from api_utils to utils.py --------- Co-authored-by: Manuel Castro --- backend/app.py | 102 +++++++++++++++++++++++++++++++++++++++++++++++ backend/utils.py | 63 +++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 backend/utils.py diff --git a/backend/app.py b/backend/app.py index af22ca76..48084667 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,3 +1,4 @@ +from functools import wraps import os import re import logging @@ -14,6 +15,7 @@ url_for, session, ) + from flask_cors import CORS from dotenv import load_dotenv from azure.keyvault.secrets import SecretClient @@ -21,6 +23,7 @@ from azure.storage.blob import BlobServiceClient from urllib.parse import unquote import uuid + from identity.flask import Auth from datetime import timedelta, datetime @@ -32,6 +35,12 @@ from functools import wraps from typing import Dict, Any, Tuple, Optional from tenacity import retry, wait_fixed, stop_after_attempt +from http import HTTPStatus # Best Practice: Use standard HTTP status codes +import smtplib +from werkzeug.exceptions import BadRequest, Unauthorized, NotFound +from utils import create_error_response, create_success_response, SubscriptionError, InvalidSubscriptionError, InvalidFinancialPriceError, require_client_principal +import stripe.error + load_dotenv() @@ -56,6 +65,7 @@ # stripe stripe.api_key = os.getenv("STRIPE_API_KEY") +FINANCIAL_ASSISTANT_PRICE_ID = os.getenv("STRIPE_FA_PRICE_ID") INVITATION_LINK = os.getenv("INVITATION_LINK") @@ -268,6 +278,7 @@ def index(*, context): # route for other static files + @app.route("/") def static_files(path): # Don't require authentication for static assets @@ -1507,5 +1518,96 @@ def get_product_prices_endpoint(): return jsonify({"error": str(e)}), 500 + +@app.route("/subscription//financialAssistant", methods=["PUT"]) +@require_client_principal # Security: Enforce authentication +def financial_assistant(subscriptionId): + """ + Add Financial Assistant to an existing subscription. + + Args: + subscription_id (str): Unique Stripe Subscription ID + Returns: + JsonResponse: Response containing a new updated subscription with the new new Item + Success format: { + "data": { + "message": "Financial Assistant added to subscription successfully.", + "subscription": { + "application": null, ... + }, + status: 200 + } + } + + Raises: + BadRequest: If the request is invalid. HttpCode: 400 + NotFound: If the subscription is not found. HttpCode: 404 + Unauthorized: If client principal ID is missing. HttpCode: 401 + """ + if not subscriptionId or not isinstance(subscriptionId, str): + raise BadRequest("Invalid subscription ID") + + # Logging: Info level for normal operations + logging.info(f"Modifying subscription {subscriptionId} to add Financial Assistant") + if not FINANCIAL_ASSISTANT_PRICE_ID: + raise InvalidFinancialPriceError("Financial Assistant price ID not configured") + + try: + updated_subscription = stripe.Subscription.modify( + subscriptionId, + items=[{"price": FINANCIAL_ASSISTANT_PRICE_ID}], + metadata={ + "modified_by": request.headers.get("X-MS-CLIENT-PRINCIPAL-ID"), + "modification_type": "add_financial_assistant", + "modification_time": datetime.datetime.now().isoformat(), + }, + ) + # Logging: Success confirmation + logging.info(f"Successfully modified subscription {subscriptionId}") + + # Response Formatting: Clean, structured success response + return create_success_response( + { + "message": "Financial Assistant added to subscription successfully.", + "subscription": { + "id": updated_subscription.id, + "status": updated_subscription.status, + "current_period_end": updated_subscription.current_period_end, + }, + } + ) + + # Error Handling: Specific error types with proper status codes + except InvalidFinancialPriceError as e: + # Logging: Error level for operation failures + logging.error(f"Stripe invalid request error: {str(e)}") + return create_error_response( + f"An error occurred while processing your request", HTTPStatus.NOT_FOUND + ) + except stripe.error.InvalidRequestError as e: + logging.error(f"Stripe API error: {str(e)}") + return create_error_response( + "Invalid Subscription ID", HTTPStatus.NOT_FOUND + ) + except stripe.error.StripeError as e: + # Logging: Error level for API failures + logging.error(f"Stripe API error: {str(e)}") + return create_error_response( + "An error occurred while processing your request", HTTPStatus.BAD_REQUEST + ) + + except BadRequest as e: + # Logging: Warning level for invalid requests + logging.warning(f"Bad request: {str(e)}") + return create_error_response(str(e), HTTPStatus.BAD_REQUEST) + + except Exception as e: + # Logging: Exception level for unexpected errors + logging.exception(f"Unexpected error: {str(e)}") + return create_error_response( + "An unexpected error occurred", HTTPStatus.INTERNAL_SERVER_ERROR + ) + + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 00000000..50e4d547 --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,63 @@ +from functools import wraps +import logging +from flask import request, jsonify +from http import HTTPStatus +from typing import Tuple, Dict, Any + + +# Response Formatting: Type hint for JSON responses +JsonResponse = Tuple[Dict[str, Any], int] + + +# Response Formatting: Standardized error response creation +def create_error_response(message: str, status_code: int) -> JsonResponse: + """ + Create a standardized error response. + Response Formatting: Ensures consistent error response structure. + """ + return jsonify({"error": {"message": message, "status": status_code}}), status_code + + +# Response Formatting: Standardized success response creation +def create_success_response(data: Dict[str, Any]) -> JsonResponse: + """ + Create a standardized success response. + Response Formatting: Ensures consistent success response structure. + """ + return jsonify({"data": data, "status": HTTPStatus.OK}), HTTPStatus.OK + + +# Error Handling: Custom exception hierarchy for subscription-specific errors +class SubscriptionError(Exception): + """Base exception for subscription-related errors""" + + pass + +class InvalidFinancialPriceError(SubscriptionError): + """Raised when subscription modification fails""" + + pass + +class InvalidSubscriptionError(SubscriptionError): + """Raised when subscription modification fails""" + + pass + + + +# Security: Decorator to ensure client principal ID is present +def require_client_principal(f): + """ + Decorator that validates the presence of client principal ID in request headers. + Security: Ensures proper authentication before processing requests. + """ + + @wraps(f) + def decorated_function(*args, **kwargs): + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + if not client_principal_id: + # Logging: Warning for security-related events + logging.warning("Attempted access without client principal ID") + return create_error_response("Missing required client principal ID", HTTPStatus.UNAUTHORIZED) + return f(*args, **kwargs) + return decorated_function From 320030058eb47bd81dc3e78259dd11cb619c0fe6 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Thu, 7 Nov 2024 12:58:05 -0400 Subject: [PATCH 166/820] Upgrade Subscription feature added (#140) * Upgrade Subscription feature added * Update app.py Fix in datetime declaration --- backend/app.py | 4 +-- frontend/src/api/api.ts | 50 ++++++++++++++++++++++++++++++ frontend/src/pages/admin/Admin.tsx | 2 +- frontend/vite.config.ts | 3 +- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/backend/app.py b/backend/app.py index 48084667..fd9face0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1519,7 +1519,7 @@ def get_product_prices_endpoint(): -@app.route("/subscription//financialAssistant", methods=["PUT"]) +@app.route("/api/subscription//financialAssistant", methods=["PUT"]) @require_client_principal # Security: Enforce authentication def financial_assistant(subscriptionId): """ @@ -1559,7 +1559,7 @@ def financial_assistant(subscriptionId): metadata={ "modified_by": request.headers.get("X-MS-CLIENT-PRINCIPAL-ID"), "modification_type": "add_financial_assistant", - "modification_time": datetime.datetime.now().isoformat(), + "modification_time": datetime.now().isoformat(), }, ) # Logging: Success confirmation diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 6584a05c..5408381b 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -323,6 +323,56 @@ export async function inviteUser({ username, email, organizationId }: any): Prom return { error: error }; } } + +interface User { + id: string; + name: string; + organizationId: string; + } + + interface SubscriptionResponse { + data: { + message: string; + subscription: { + id: string; + status: string; + current_period_end: number; + }; + }; + status: number; + } +export async function upgradeSubscription({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { + const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; + const userOrganizationId = user?.organizationId ?? "00000000-0000-0000-0000-000000000000"; + + try { + const response = await fetch(`/subscription/${subscriptionId}/financialAssistant`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId, + }, + body: JSON.stringify({ + organizationId: userOrganizationId, + activateFinancialAssistant: true, + }), + }); + + if (!response.ok) { + throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); + } + + const parsedResponse: SubscriptionResponse = await response.json(); + const { message, subscription } = parsedResponse.data; + + console.log("Subscription upgraded successfully:", message); + return subscription; + } catch (error) { + console.error("Error upgrading subscription:", error instanceof Error ? error.message : error); + throw error; + } +} + export async function createInvitation({ organizationId, invitedUserEmail, userId, role }: any): Promise { try { const response = await fetch("/api/createInvitation", { diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 0de671c1..bb86829a 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -7,7 +7,7 @@ import { AddFilled, DeleteRegular, EditRegular, SearchRegular } from "@fluentui/ import { useAppContext } from "../../providers/AppProviders"; import DOMPurify from "dompurify"; -import { checkUser, getUsers, inviteUser, createInvitation, deleteUser } from "../../api"; +import { getUsers, inviteUser, createInvitation, deleteUser } from "../../api"; import styles from "./Admin.module.css"; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index db3d8e00..a71fa49e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -36,7 +36,8 @@ export default defineConfig({ "/api/conversations": "http://localhost:8000", "/api/chat": "http://localhost:8000", "/api/auth/user": "http://localhost:8000", - "/api/prices": "http://localhost:8000" + "/api/prices": "http://localhost:8000", + "/api/subscription//financialAssistant": "http://localhost:8000" }, host: true } From 597536de9354b0699082738c124195ca12224db1 Mon Sep 17 00:00:00 2001 From: wanjiru_mambo <99056428+wmambo@users.noreply.github.com> Date: Fri, 8 Nov 2024 06:34:35 -0500 Subject: [PATCH 167/820] Fa 95 create financial agent product on stripe for test and live environments (#142) * refined code * refined code * update comment in line 33 --- backend/stripeTestLiveApiValidation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/stripeTestLiveApiValidation.py b/backend/stripeTestLiveApiValidation.py index 4b3ebd20..c4432bb8 100644 --- a/backend/stripeTestLiveApiValidation.py +++ b/backend/stripeTestLiveApiValidation.py @@ -5,19 +5,19 @@ # load the environment variables from the .env file load_dotenv() -# set stripe api keys for test and live +# set stripe api keys for the test and live environments STRIPE_API_KEY = os.getenv('STRIPE_API_KEY') STRIPE_LIVE_API_KEY = os.getenv('STRIPE_LIVE_API_KEY') """initialize stripe to ensure successful authentication""" -# initialize with the test api key, switching to the live key can be done later +# initialize with the test api key, switching to the live key will be done dynamically later stripe.api_key = STRIPE_API_KEY # product ids from both test and live environments test_product_id = os.getenv('STRIPE_FINANCIAL_AGENT_TEST_ID') live_product_id = os.getenv('STRIPE_FINANCIAL_AGENT_LIVE_ID') -# function to validate that the test and live products are accessible and match the requirements in each model +# function to validate that the test and live products are accessible and fulfill the requirements def validate_products(): try: """retrieve product information for the test environment""" @@ -30,7 +30,7 @@ def validate_products(): live_product = stripe.Product.retrieve(live_product_id) """retrieve associated prices for both test and live products""" - # get the prices in live mode - no switching needed because api key is currently in live mode (see line 33) + # get the prices in live mode - no switching needed because api key is currently in live mode (see line 30) live_prices = stripe.Price.list(product = live_product_id) # switch api key to test mode and retrieve the price for test @@ -48,7 +48,7 @@ def validate_products(): test_price['recurring']['interval'] == live_price['recurring']['interval'] ) - # display a message to show success or failure in the validation process + # display a message to notify success or failure in the product validation process if are_products_matching: print("Validation successful: Test and Live products match the requirements.") else: From a9387bb68abcb83a788c53d8c04bcb2cad6b3eb5 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Tue, 12 Nov 2024 10:38:25 -0400 Subject: [PATCH 168/820] FA send user data in chat request (#143) --- frontend/src/api/api.ts | 93 +++++++++++++++----------------- frontend/src/pages/chat/Chat.tsx | 5 +- 2 files changed, 44 insertions(+), 54 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 5408381b..f45066bc 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,17 +1,4 @@ -import { List } from "@fluentui/react"; -import { - AskRequest, - AskResponse, - AskResponseGpt, - ChatRequest, - ChatRequestGpt, - GetSettingsProps, - PostSettingsProps, - ConversationHistoryItem, - ConversationChatItem, - ChatTurn, - UserInfo -} from "./models"; +import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo } from "./models"; export async function getUsers({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; @@ -132,11 +119,15 @@ export async function postSettings({ user, temperature }: PostSettingsProps): Pr } } -export async function chatApiGpt(options: ChatRequestGpt): Promise { +export async function chatApiGpt(options: ChatRequestGpt, user: any): Promise { + const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; + const user_name = user ? user.name : "anonymous"; const response = await fetch("/chatgpt", { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user_id, + "X-MS-CLIENT-PRINCIPAL-NAME": user_name }, body: JSON.stringify({ history: options.history, @@ -328,48 +319,48 @@ interface User { id: string; name: string; organizationId: string; - } - - interface SubscriptionResponse { +} + +interface SubscriptionResponse { data: { - message: string; - subscription: { - id: string; - status: string; - current_period_end: number; - }; + message: string; + subscription: { + id: string; + status: string; + current_period_end: number; + }; }; status: number; - } +} export async function upgradeSubscription({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; const userOrganizationId = user?.organizationId ?? "00000000-0000-0000-0000-000000000000"; try { - const response = await fetch(`/subscription/${subscriptionId}/financialAssistant`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId, - }, - body: JSON.stringify({ - organizationId: userOrganizationId, - activateFinancialAssistant: true, - }), - }); - - if (!response.ok) { - throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); - } - - const parsedResponse: SubscriptionResponse = await response.json(); - const { message, subscription } = parsedResponse.data; - - console.log("Subscription upgraded successfully:", message); - return subscription; + const response = await fetch(`/subscription/${subscriptionId}/financialAssistant`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId + }, + body: JSON.stringify({ + organizationId: userOrganizationId, + activateFinancialAssistant: true + }) + }); + + if (!response.ok) { + throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); + } + + const parsedResponse: SubscriptionResponse = await response.json(); + const { message, subscription } = parsedResponse.data; + + console.log("Subscription upgraded successfully:", message); + return subscription; } catch (error) { - console.error("Error upgrading subscription:", error instanceof Error ? error.message : error); - throw error; + console.error("Error upgrading subscription:", error instanceof Error ? error.message : error); + throw error; } } @@ -453,7 +444,7 @@ export async function createCheckoutSession({ userId, priceId, successUrl, cance return session; } -export async function getProductPrices({ user}: { user: any}): Promise { +export async function getProductPrices({ user }: { user: any }): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; try { @@ -473,7 +464,7 @@ export async function getProductPrices({ user}: { user: any}): Promise { } } -export async function getOrganizationSubscription({userId, organizationId} : any) { +export async function getOrganizationSubscription({ userId, organizationId }: any) { const response = await fetch("/api/get-organization-subscription?organizationId=" + organizationId, { method: "GET", headers: { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 01b14753..44eb7c6e 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -60,8 +60,7 @@ const Chat = () => { setChatIsCleaned, chatIsCleaned, settingsPanel, - setUser, - setOrganization + user } = useAppContext(); const lastQuestionRef = useRef(""); @@ -113,7 +112,7 @@ const Chat = () => { suggestFollowupQuestions: useSuggestFollowupQuestions } }; - const result = await chatApiGpt(request); + const result = await chatApiGpt(request, user); const conditionOne = answers.map(a => ({ user: a[0] })); if (conditionOne.length <= 0) { setRefreshFetchHistory(true); From f73be0ef1ad66ec0ec7a6d8c21355af8b6e97eb1 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Tue, 12 Nov 2024 10:39:17 -0400 Subject: [PATCH 169/820] FA-98 Create an endpoint to delete the financial asistant subscription (#144) * The endpoint was created and the function was added to api.ts * Update app.py --- backend/app.py | 164 +++++++++++++++++++++++++++++++++++++++- frontend/src/api/api.ts | 28 +++++++ 2 files changed, 191 insertions(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index fd9face0..21193fbe 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1518,7 +1518,7 @@ def get_product_prices_endpoint(): return jsonify({"error": str(e)}), 500 - +#ADD FINANCIAL ASSITANT A SUBSCRIPTION @app.route("/api/subscription//financialAssistant", methods=["PUT"]) @require_client_principal # Security: Enforce authentication def financial_assistant(subscriptionId): @@ -1608,6 +1608,168 @@ def financial_assistant(subscriptionId): "An unexpected error occurred", HTTPStatus.INTERNAL_SERVER_ERROR ) +#DELETE FINANCIAL ASSITANT A SUBSCRIPTION +@app.route("/api/subscription//financialAssistant", methods=["DELETE"]) +@require_client_principal # Security: Enforce authentication +def remove_financial_assistant(subscriptionId): + """ + Remove Financial Assistant from an existing subscription. + + Args: + subscription_id (str): Unique Stripe Subscription ID + Returns: + JsonResponse: Response confirming the removal of the Financial Assistant + Success format: { + "data": { + "message": "Financial Assistant removed from subscription successfully.", + "subscription": { + "id": "", + "status": "", + "current_period_end": "" + }, + status: 200 + } + } + + Raises: + BadRequest: If the request is invalid. HttpCode: 400 + NotFound: If the subscription is not found. HttpCode: 404 + Unauthorized: If client principal ID is missing. HttpCode: 401 + """ + if not subscriptionId or not isinstance(subscriptionId, str): + raise BadRequest("Invalid subscription ID") + + logging.info(f"Modifying subscription {subscriptionId} to remove Financial Assistant") + + try: + # Get the subscription to find the Financial Assistant item + subscription = stripe.Subscription.retrieve(subscriptionId) + + # Find the Financial Assistant item + assistant_item_id = None + for item in subscription['items']['data']: + if item['price']['id'] == FINANCIAL_ASSISTANT_PRICE_ID: + assistant_item_id = item['id'] + break + + if not assistant_item_id: + raise NotFound("Financial Assistant item not found in subscription") + + # Modify the subscription to remove the Financial Assistant item + updated_subscription = stripe.Subscription.modify( + subscriptionId, + items=[{"id": assistant_item_id, "deleted": True}], + metadata={ + "modified_by": request.headers.get("X-MS-CLIENT-PRINCIPAL-ID"), + "modification_type": "remove_financial_assistant", + "modification_time": datetime.now().isoformat(), + }, + ) + + logging.info(f"Successfully removed Financial Assistant from subscription {subscriptionId}") + + return create_success_response( + { + "message": "Financial Assistant removed from subscription successfully.", + "subscription": { + "id": updated_subscription.id, + "status": updated_subscription.status, + "current_period_end": updated_subscription.current_period_end, + }, + } + ) + + except stripe.error.InvalidRequestError as e: + logging.error(f"Stripe API error: {str(e)}") + return create_error_response( + "Invalid Subscription ID", HTTPStatus.NOT_FOUND + ) + except stripe.error.StripeError as e: + logging.error(f"Stripe API error: {str(e)}") + return create_error_response( + "An error occurred while processing your request", HTTPStatus.BAD_REQUEST + ) + except NotFound as e: + logging.warning(f"Not found: {str(e)}") + return create_error_response(str(e), HTTPStatus.NOT_FOUND) + except Exception as e: + logging.exception(f"Unexpected error: {str(e)}") + return create_error_response( + "An unexpected error occurred", HTTPStatus.INTERNAL_SERVER_ERROR + ) + +#CHECK STATUS SUBSCRIPTION FA (FINANCIAL ASSITANT) +@app.route("/api/subscription//financialAssistant/status", methods=["GET"]) +@require_client_principal # Security: Enforce authentication +def get_financial_assistant_status(subscriptionId): + """ + Check if Financial Assistant is added to a subscription. + + Args: + subscriptionId (str): Unique Stripe Subscription ID + + Returns: + JsonResponse: Response indicating if Financial Assistant is active in the subscription. + Success format: + { + "data": { + "financial_assistant_active": true, + "subscription": { + "id": "", + "status": "active" + } + } + } + + Raises: + NotFound: If the subscription is not found. HttpCode: 404 + Unauthorized: If client principal ID is missing. HttpCode: 401 + """ + try: + + subscription = stripe.Subscription.retrieve(subscriptionId) + + financial_assistant_active = any( + item.price.id == FINANCIAL_ASSISTANT_PRICE_ID for item in subscription["items"]["data"] + ) + + return jsonify({ + "data": { + "financial_assistant_active": financial_assistant_active, + "subscription": { + "id": subscription.id, + "status": subscription.status + } + } + }), HTTPStatus.OK + + except stripe.error.InvalidRequestError: + logging.error(f"Invalid Subscription ID: {subscriptionId}") + return jsonify({ + "error": { + "message": "Invalid Subscription ID", + "status": 404 + } + }), HTTPStatus.NOT_FOUND + + except stripe.error.StripeError as e: + logging.error(f"Stripe API error: {str(e)}") + return jsonify({ + "error": { + "message": "An error occurred while processing your request.", + "status": 400 + } + }), HTTPStatus.BAD_REQUEST + + except Exception as e: + logging.exception(f"Unexpected error: {str(e)}") + return jsonify({ + "error": { + "message": "An unexpected error occurred", + "status": 500 + } + }), HTTPStatus.INTERNAL_SERVER_ERROR + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index f45066bc..c77632e9 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -364,6 +364,34 @@ export async function upgradeSubscription({ user, subscriptionId }: { user?: Use } } +export async function removeFinancialAssistant({ user, subscriptionId }: { user?: User; subscriptionId: string; }): Promise { + const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; + + try { + const response = await fetch(`/api/subscription/${subscriptionId}/financialAssistant`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId, + }, + }); + + if (!response.ok) { + throw new Error(`Subscription removal failed: ${response.status} ${response.statusText}`); + } + + const parsedResponse: SubscriptionResponse = await response.json(); + const { message, subscription } = parsedResponse.data; + + console.log("Financial Assistant removed successfully:", message); + return subscription; + } catch (error) { + console.error("Error removing Financial Assistant:", error instanceof Error ? error.message : error); + throw error; + } +} + + export async function createInvitation({ organizationId, invitedUserEmail, userId, role }: any): Promise { try { const response = await fetch("/api/createInvitation", { From 2502ad235cada7642fbf33e1a33acce1c1494655 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 14 Nov 2024 14:23:24 -0400 Subject: [PATCH 170/820] Fa -173 build UI for financial assistant (#147) * FA-layout * FA 39 Fix hambuerguer button horizontal aligment * FA-39 toogle sidebar at click * FA-39 adjust heigth on chat UI * FA-39 refactor input chat question make it visible on mobile devices --- frontend/package-lock.json | 58 ++++ frontend/package.json | 2 + frontend/src/App.tsx | 10 +- frontend/src/components/Navbar/NavBar.tsx | 112 ++++++++ .../src/components/Navbar/Navbar.module.css | 63 +++++ .../QuestionInput/QuestionInput.tsx | 88 +++--- .../components/SideMenu/SideMenu.module.css | 18 +- .../src/components/Sidebar/Sidebar.module.css | 257 ++++++++++++++++++ frontend/src/components/Sidebar/Sidebar.tsx | 118 ++++++++ .../src/components/Sidebar/SidebarItem.tsx | 58 ++++ frontend/src/index.css | 22 +- frontend/src/index.tsx | 1 + frontend/src/pages/chat/Chat.module.css | 4 +- frontend/src/pages/chat/Chat.tsx | 4 +- frontend/src/pages/layout/LayoutNew.tsx | 37 +++ frontend/src/pages/layout/_Layout.module.css | 61 +++++ 16 files changed, 851 insertions(+), 62 deletions(-) create mode 100644 frontend/src/components/Navbar/NavBar.tsx create mode 100644 frontend/src/components/Navbar/Navbar.module.css create mode 100644 frontend/src/components/Sidebar/Sidebar.module.css create mode 100644 frontend/src/components/Sidebar/Sidebar.tsx create mode 100644 frontend/src/components/Sidebar/SidebarItem.tsx create mode 100644 frontend/src/pages/layout/LayoutNew.tsx create mode 100644 frontend/src/pages/layout/_Layout.module.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 68a1c0fc..a41c9907 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,8 @@ "@react-spring/web": "^9.7.1", "@stripe/react-stripe-js": "^2.7.3", "@stripe/stripe-js": "^4.1.0", + "@tabler/icons-react": "^3.21.0", + "bootstrap": "^5.3.3", "dompurify": "^3.0.1", "mammoth": "^1.7.0", "microsoft-cognitiveservices-speech-sdk": "^1.27.0", @@ -1219,6 +1221,17 @@ "resolved": "https://registry.npmjs.org/@pdftron/webviewer/-/webviewer-10.7.3.tgz", "integrity": "sha512-dZbdxnPsaGDN9xOv6m3eAc2mhr3HOkeCo4VbS6oHof1cjgEUuVJGUvoZ8kgD4j2uHlKPVK0K8/ClJuCPDKwe6w==" }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@react-pdf-viewer/attachment": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@react-pdf-viewer/attachment/-/attachment-3.12.0.tgz", @@ -1754,6 +1767,32 @@ "tslib": "^2.4.0" } }, + "node_modules/@tabler/icons": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.21.0.tgz", + "integrity": "sha512-5+GkkmWCr1wgMor5cOF1/YYflTQdc15y10FUikJ3HW8hDiFjfbuoAHJi17FT1vwsr1sA78rkJMn+fDoOOjnnPA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.21.0.tgz", + "integrity": "sha512-Qq0GnZzzccbv/zuMyXAUUPlogNAqx9KsF8cr/ev3bxs+GMObqNEjXv1eZl9GFzxyQTS435siJNU8A1BaIYhX8g==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.21.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@types/cookie": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", @@ -2182,6 +2221,25 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index d074fd40..5775c264 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,8 @@ "@react-spring/web": "^9.7.1", "@stripe/react-stripe-js": "^2.7.3", "@stripe/stripe-js": "^4.1.0", + "@tabler/icons-react": "^3.21.0", + "bootstrap": "^5.3.3", "dompurify": "^3.0.1", "mammoth": "^1.7.0", "microsoft-cognitiveservices-speech-sdk": "^1.27.0", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dec85a4c..a53f066e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,8 @@ -import React, { useState, useEffect } from "react"; -import { Routes, Route, useNavigate } from "react-router-dom"; +import { Routes, Route } from "react-router-dom"; import ProtectedRoute from "./router/ProtectedRoute"; import Layout from "./pages/layout/Layout"; +import LayoutNew from "./pages/layout/LayoutNew"; + import NoPage from "./pages/NoPage"; import AccessDenied from "./pages/AccesDenied"; import Chat from "./pages/chat/Chat"; @@ -19,7 +20,9 @@ export default function App() { {/* Public Routes */} } /> } /> - + }> + } /> + {/* Access Denied Route */} } /> @@ -28,6 +31,7 @@ export default function App() { }> {/* Regular User and Admin Routes */} } /> + } /> diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx new file mode 100644 index 00000000..baa961bb --- /dev/null +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import styles from "./Navbar.module.css"; +import { IconMenu2, IconMessageCircle, IconHistory, IconSettings, IconBell, IconUser, IconMail, IconListCheck } from "@tabler/icons-react"; +import { useAppContext } from "../../providers/AppProviders"; + +interface NavbarProps { + setIsCollapsed: React.Dispatch>; +} + +const Navbar: React.FC = ({ setIsCollapsed }) => { + const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); + + const handleShowHistoryPanel = () => { + setShowHistoryPanel(!showHistoryPanel); + setShowFeedbackRatingPanel(false); + setSettingsPanel(false); + }; + + const handleShowFeedbackRatingPanel = () => { + setShowFeedbackRatingPanel(!showFeedbackRatingPanel); + setSettingsPanel(false); + setShowHistoryPanel(false); + }; + + const handleShowSettings = () => { + setSettingsPanel(!settingsPanel); + setShowHistoryPanel(false); + setShowFeedbackRatingPanel(false); + }; + + const handleOnClickShowSidebar = () => { + setIsCollapsed(false); + }; + + return ( +
    + ); +}; + +export default Navbar; diff --git a/frontend/src/components/Navbar/Navbar.module.css b/frontend/src/components/Navbar/Navbar.module.css new file mode 100644 index 00000000..3e23d655 --- /dev/null +++ b/frontend/src/components/Navbar/Navbar.module.css @@ -0,0 +1,63 @@ +/* Icon Styles */ +.iconLarge { + font-size: 1.9em; + color: #a7c444; +} + +/* Profile Card Styles */ +.profileCard { + display: flex; + align-items: center; + padding: 8px 12px; + background-color: #dbe3ec8a; + border-radius: 8px; +} + +.userDetails { + line-height: 1.2; + margin-right: 5px; +} + +.userName { + font-size: 1em; + font-weight: 600; + color: #333; + margin: 0; +} + +.userEmail { + font-size: 0.9em; + color: #6c757d; + margin: 0; +} + +/* Responsive User Details */ +@media (max-width: 576px) { + .userDetails { + display: none; + } +} + +@media (min-width: 577px) { + .userDetails { + display: flex; + flex-direction: column; + } +} + +/* Sidebartoggler */ +.sidebartoggler { + /* Define styles if needed */ +} + +/* Message Body */ +.messageBody { + /* Define styles if needed */ +} + +@media (max-width: 991.98px) { + .headerNavbar { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + } +} diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index b1814768..5f24b6f8 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -2,7 +2,7 @@ import { useState, useContext } from "react"; import { useAppContext } from "../../providers/AppProviders"; import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; import { getTokenOrRefresh } from "./token_util"; -import { Send24Filled, Mic24Regular, AttachRegular } from "@fluentui/react-icons"; +import { Send24Filled, Mic24Regular, AttachRegular, AddRegular, BroomRegular } from "@fluentui/react-icons"; import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from "microsoft-cognitiveservices-speech-sdk"; import { uploadFile } from "../../api"; @@ -249,48 +249,54 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Pr !organization.subscriptionId; return ( - - -
    - -
    { - if (ev.key === "Enter") { - ev.preventDefault(); - sttFromMic(); - } - }} - tabIndex={0} - > - +
    +
    + {/* Left Buttons */} +
    + +
    -
    { - if (ev.key === "Enter") { - ev.preventDefault(); - sendQuestion(); - } - }} - tabIndex={0} - > - + + {/* Text Field */} +
    + +
    + + {/* Right Buttons */} +
    + +
    - +
    ); }; diff --git a/frontend/src/components/SideMenu/SideMenu.module.css b/frontend/src/components/SideMenu/SideMenu.module.css index ba82dcb2..6b4a414e 100644 --- a/frontend/src/components/SideMenu/SideMenu.module.css +++ b/frontend/src/components/SideMenu/SideMenu.module.css @@ -33,22 +33,22 @@ padding: 24px 0px; } -nav { +aside nav { width: 100%; } -nav ul { +aside nav ul { list-style-type: none; padding: 0; margin: 40px 0; width: 100%; } -nav ul li { +aside nav ul li { width: 100%; } -nav ul li a { +aside nav ul li a { color: #12100b; font-size: 15px; text-decoration: none; @@ -60,29 +60,29 @@ nav ul li a { transition: padding 0.3s ease; } -.sidebar.collapsed nav ul li a { +.sidebar.collapsed aside nav ul li a { justify-content: center; padding: 10px 0; width: 85%; } -nav ul li a:hover { +aside nav ul li a:hover { background-color: #85a717; } -.sidebar.collapsed nav ul li a:hover { +.sidebar.collapsed aside nav ul li a:hover { border-top-right-radius: 6px; border-bottom-right-radius: 6px; } -nav ul li a.active { +aside nav ul li a.active { background-color: #f5f5f5; border-left: 6px solid #0a0b0a; font-weight: 700; padding: 10px 25px; } -.sidebar.collapsed nav ul li a.active { +.sidebar.collapsed aside nav ul li a.active { border-left: 6px solid #0a0b0a; padding: 10px 0; border-top-right-radius: 6px; diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css new file mode 100644 index 00000000..a598c971 --- /dev/null +++ b/frontend/src/components/Sidebar/Sidebar.module.css @@ -0,0 +1,257 @@ +/*==================================== += Sidebar Styles = +====================================*/ +.submenuLink { + color: rgb(41 52 61); + text-decoration: none; +} +/* Nav Styles */ +.nav { + width: 100%; + } + + .navUl { + list-style-type: none; + padding-left: 0; + } + + .navLi { + width: 100%; + } + + .navLink { + color: #12100b; + font-size: 15px; + text-decoration: none; + padding: 10px 30px; + display: flex; + align-items: center; + width: 100%; + text-align: left; + transition: padding 0.3s ease; + } + + .navLinkCollapsed { + justify-content: center; + padding: 10px 0; + width: 85%; + } + + :global(.navLink:hover) { + background-color: #85a717; + } + + .navLinkActive { + background-color: #f5f5f5; + border-left: 6px solid #0a0b0a; + font-weight: 700; + padding: 10px 25px; + } + + .navLinkActiveCollapsed { + border-left: 6px solid #0a0b0a; + padding: 10px 0; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + + /* Icon Styles */ + .icon { + color: rgb(63, 63, 63); + font-size: 25px; + margin-right: 15px; + transition: margin-right 0.3s ease; + } + + .iconCollapsed { + margin-right: 0; + } + + /* Collapse Button */ + .collapseButton { + color: rgb(63, 63, 63); + border-right: 2px solid #0a0b0a; + cursor: pointer; + font-size: 22px; + display: flex; + align-items: center; + position: absolute; + bottom: 20px; + right: 10px; + } + + .collapseButtonCollapsed { + position: absolute; + bottom: 20px; + left: 10px; + border-left: 2px solid #0a0b0a; + border-right: none; + } + + /* Sidebar Link */ + .sidebarLink { + display: flex; + font-size: 14px; + white-space: nowrap; + align-items: center; + line-height: 26px; + position: relative; + padding: 9px 16px; + border-radius: 6px; + gap: 10px; + text-decoration: none; + font-weight: 500; + margin-bottom: 4px; + } + + .sidebarLinkIcon { + font-size: 18px; + color: var(--bs-heading-color); + opacity: 0.8; + } + + :global(.sidebarLink:hover) { + color: #000000; + } + + .sidebarLinkActive { + background-color: #000000; + color: #fff; + box-shadow: 0px 17px 20px -8px rgba(77, 91, 236, 0.23); + } + + .sidebarLinkActiveIcon { + color: #fff; + } + + .sidebarLinkActiveI { + color: #A7C444; + } + + /* Nav Small Cap */ + .navSmallCap { + font-size: 12px; + font-weight: 600; + padding: 7px 0; + line-height: 26px; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--bs-heading-color); + } + + .navSmallCapIcon { + display: none; + } + + /* Submenu Styles */ + .submenu { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; + padding-left: 20px; + border-radius: 0.4em; + background-color: #ffffff30; + } + + .submenuActive { + max-height: 200px; /* Adjust as needed */ + } + + .submenuArrow { + margin-left: auto; + transition: transform 0.3s ease; + } + + .submenuArrowActive { + transform: rotate(180deg); + } + + .submenuItem { + border-bottom: 1px solid #33333329; + padding: 0.9em 0; + } + + /* Sidebar Divider */ + .sidebarDivider { + height: 0.5px; + display: block; + margin: 20px 0; + background: #00000029; + width: 100%; + } + + /* Left Sidebar */ + .leftSidebar { + width: 280px; + background-color: #A7C444; + position: absolute; + transition: 0.2s ease-in; + height: 100%; + z-index: 11; + } + + /* Brand Logo */ + .brandLogo { + min-height: 72px; + padding: 3em 28px 1.5em; + } + + .brandLogoImg { + max-width: 11em; + } + + /* Close Button */ + .closeBtn { + background: none; /* Remove background */ + border: none; /* Replace #000 with your desired border color */ + color: none; /* Text color matching your border color or choose another */ + padding: 0; /* Adjust padding as needed */ + border-radius: 0; /* Optional: for rounded corners */ + cursor: pointer; /* Pointer cursor on hover */ + transition: all 0.3s ease; /* Optional: smooth transition for hover effect */ + } + + /* Sidebar Toggler */ + .sidebarToggler { + /* Define styles if needed */ + } + + /* Scroll Sidebar */ + .scrollSidebar { + overflow-y: auto; + padding: 0 16px 0 28px; + height: calc(100vh - 80px); + border-radius: 12px; + } + + .simplebarTrackHorizontal { + visibility: hidden !important; + } + + /* Cursor Pointer */ + .cursorPointer { + cursor: pointer; + } + + /* LG Class */ + .lg { + /* Define styles if needed */ + } + + /* Responsive Adjustments */ + @media (max-width: 1199px) { + .leftSidebar { + left: -260px; + } + + .showSidebar { + left: 0; + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + } + + .brandLogo, + .scrollSidebar { + padding: 0 16px; + } + } + \ No newline at end of file diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 00000000..634d41ee --- /dev/null +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,118 @@ +// Sidebar.tsx +import React from "react"; +import { Link } from "react-router-dom"; +import { + IconX, + IconMessagePlus, + IconBell, + IconDiscountCheck, + IconUsers, + IconStar, + IconFileInvoice, + IconListCheck, + IconHeadset, + IconDots +} from "@tabler/icons-react"; +import salesLogo from "../../img/logo.png"; +import styles from "./Sidebar.module.css"; +import SidebarItem from "./SidebarItem"; + +interface SidebarProps { + isCollapsed: boolean; + setIsCollapsed: React.Dispatch>; +} + +const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { + const handleOnClickCloseSideBar = () => { + setIsCollapsed(true); + }; + return ( + + ); +}; + +export default Sidebar; diff --git a/frontend/src/components/Sidebar/SidebarItem.tsx b/frontend/src/components/Sidebar/SidebarItem.tsx new file mode 100644 index 00000000..62c5a3b0 --- /dev/null +++ b/frontend/src/components/Sidebar/SidebarItem.tsx @@ -0,0 +1,58 @@ +// SidebarItem.tsx +import React from "react"; +import { Link } from "react-router-dom"; +import { IconChevronDown } from "@tabler/icons-react"; +import styles from "./Sidebar.module.css"; + +interface SidebarItemProps { + title: string; + icon: JSX.Element; + to?: string; + links?: Array<{ title: string; href: string }>; +} + +const SidebarItem: React.FC = ({ title, icon, to = "#", links }) => { + const [isActive, setIsActive] = React.useState(false); + + const toggleSubmenu = (e: React.MouseEvent) => { + if (links) { + e.preventDefault(); + setIsActive(!isActive); + } + }; + + return ( +
  • + + {React.cloneElement(icon, { className: styles.sidebarLinkIcon })} + {title} + {links && ( + + + + )} + + {links && ( +
      + {links.map((linkItem, index) => ( +
    • + + {linkItem.title} + +
    • + ))} +
    + )} +
  • + ); +}; + +export default SidebarItem; diff --git a/frontend/src/index.css b/frontend/src/index.css index 2efb29b4..4f0a1420 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -3,21 +3,31 @@ } html, -body { - height: 100%; - margin: 0; - padding: 0; -} html { background: #f2f2f2; + height: 100%; font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - +body { + height: 100%; + margin: 0; + padding: 0; + font-family: var(--bs-body-font-family); + font-size: var(--bs-body-font-size); + font-weight: var(--bs-body-font-weight); + line-height: var(--bs-body-line-height); + color: var(--bs-body-color); + text-align: var(--bs-body-text-align); + background-color: var(--bs-secondary-bg); + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} #root { height: 100%; } + diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index d825fc6a..f80c0524 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -9,6 +9,7 @@ import App from "./App"; import { initializeIcons } from "@fluentui/react"; import "./index.css"; +import "bootstrap/dist/css/bootstrap.min.css"; initializeIcons(); diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 7824795e..4b6e033f 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -1,5 +1,7 @@ .container { flex: 1; + display: flex; + flex-direction: column; background-color: white; margin: 0px 10px 0px 0px; } @@ -7,7 +9,7 @@ .mainContainer { display: flex; flex-direction: row-reverse; - height: 90%; + flex: 1; overflow: hidden; } diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 44eb7c6e..d5e5d120 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -462,7 +462,7 @@ const Chat = () => {
    )}
    -
    + {/*
    -
    +
    */} { + const { pathname } = useLocation(); + const [isCollapsed, setIsCollapsed] = useState(true); + + return ( + <> + {/* Sidebar Start */} + + {/* Sidebar End */} + + {/* Main Wrapper */} +
    + {/* Header Start */} +
    + +
    + {/* Header End */} + + {/* Main Content */} +
    + {/* Content goes here */} + +
    +
    + + ); +}; + +export default Layout; diff --git a/frontend/src/pages/layout/_Layout.module.css b/frontend/src/pages/layout/_Layout.module.css new file mode 100644 index 00000000..0574ae3c --- /dev/null +++ b/frontend/src/pages/layout/_Layout.module.css @@ -0,0 +1,61 @@ + + + +/* Layout Styles */ + +.bodyWrapper { + position: relative; + height: 80vh; + } + + .bodyWrapperInner { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + margin-top: 80px; + } + + .bodyWrapperContainer { + margin: 0 auto; + padding: 24px; + transition: 0.2s ease-in; + } + + /* Responsive Layout Adjustments */ + + @media (max-width: 767.98px) { + .bodyWrapperContainer { + padding: 30px 20px; + } + } + + @media (min-width: 1200px) { + .bodyWrapperFull { + margin-left: 260px; + } + + + } + + /* Fixed Header Position */ + + .bodyWrapperFixedHeader { + padding-top: calc(72px + 30px); + } + + /* App Header Styles */ + + .appHeader { + z-index: 50; + background-color: var(--bs-tertiary-bg); + padding: 0 25px; + top: 0; + } + + .appHeaderContainer { + max-width: 1200px; + margin: 0 auto; + padding: 0 30px; + } + \ No newline at end of file From 9208a73d4f7a745b4e8a973705ee2ffc10866955 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Thu, 14 Nov 2024 14:26:20 -0400 Subject: [PATCH 171/820] Fa 164 update the user interface to include an option that allows users to subscribe to the financial assistant feature (#145) * The endpoint was created and the function was added to api.ts * Include Financial Assistant Menu in UI * Add css file * Fixed bugs and tested * update app.py --- backend/app.py | 30 +++- frontend/src/App.tsx | 2 + frontend/src/api/api.ts | 65 ++++++-- frontend/src/components/SideMenu/SideMenu.tsx | 12 ++ .../FinancialAssistant.module.css | 77 +++++++++ .../financialassistant/FinancialAssistant.tsx | 156 ++++++++++++++++++ 6 files changed, 321 insertions(+), 21 deletions(-) create mode 100644 frontend/src/pages/financialassistant/FinancialAssistant.module.css create mode 100644 frontend/src/pages/financialassistant/FinancialAssistant.tsx diff --git a/backend/app.py b/backend/app.py index 21193fbe..0c714ef0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1699,7 +1699,7 @@ def remove_financial_assistant(subscriptionId): ) #CHECK STATUS SUBSCRIPTION FA (FINANCIAL ASSITANT) -@app.route("/api/subscription//financialAssistant/status", methods=["GET"]) +@app.route("/api/subscription//financialAssistant", methods=["GET"]) @require_client_principal # Security: Enforce authentication def get_financial_assistant_status(subscriptionId): """ @@ -1726,19 +1726,43 @@ def get_financial_assistant_status(subscriptionId): Unauthorized: If client principal ID is missing. HttpCode: 401 """ try: - subscription = stripe.Subscription.retrieve(subscriptionId) financial_assistant_active = any( item.price.id == FINANCIAL_ASSISTANT_PRICE_ID for item in subscription["items"]["data"] ) + + financial_assistant_item = next( + (item for item in subscription["items"]["data"] + if item.price.id == FINANCIAL_ASSISTANT_PRICE_ID), + None + ) + if financial_assistant_item is False: + logging.info(f"Financial Assistant not actived in subscription: {subscriptionId}") + return jsonify({ + "data": { + "financial_assistant_active": False, + "message": "Financial Assistant is not active in this subscription." + } + }), HTTPStatus.OK + + if financial_assistant_item is None: + logging.info(f"Financial Assistant not found in subscription: {subscriptionId}") + return jsonify({ + "data": { + "financial_assistant_active": False, + "message": "Financial Assistant not founded in this subscription." + } + }), HTTPStatus.OK + return jsonify({ "data": { "financial_assistant_active": financial_assistant_active, "subscription": { "id": subscription.id, - "status": subscription.status + "status": subscription.status, + "price_id": financial_assistant_item.price.id } } }), HTTPStatus.OK diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a53f066e..42dd0707 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import Admin from "./pages/admin/Admin"; import Onboarding from "./pages/onboarding/Onboarding"; import Invitations from "./pages/invitations/Invitations"; import Organization from "./pages/organization/Organization"; +import FinancialAssistant from "./pages/financialassistant/FinancialAssistant" import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; @@ -42,6 +43,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index c77632e9..1f30dff4 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -331,33 +331,62 @@ interface SubscriptionResponse { }; }; status: number; -} -export async function upgradeSubscription({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { - const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; - const userOrganizationId = user?.organizationId ?? "00000000-0000-0000-0000-000000000000"; + } + export async function getFinancialAssistant({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { + const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; + try { - const response = await fetch(`/subscription/${subscriptionId}/financialAssistant`, { - method: "PUT", + const response = await fetch(`/api/subscription/${subscriptionId}/financialAssistant`, { + method: "GET", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId - }, - body: JSON.stringify({ - organizationId: userOrganizationId, - activateFinancialAssistant: true - }) + "X-MS-CLIENT-PRINCIPAL-ID": userId, + } }); - + if (!response.ok) { - throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); + const error = new Error(`Failed to check financial assistant status: ${response.status}`); + (error as any).status = response.status; // Añade el código de estado al error + throw error; } - const parsedResponse: SubscriptionResponse = await response.json(); - const { message, subscription } = parsedResponse.data; + const parsedResponse = await response.json(); + return parsedResponse.data; - console.log("Subscription upgraded successfully:", message); - return subscription; + } catch (error) { + console.error("Error verifying the Financial Assistant: ", error instanceof Error ? error.message : error); + throw error; + } +} + + +export async function upgradeSubscription({ user, subscriptionId }: { user?: User ; subscriptionId: string }): Promise { + const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; + const userOrganizationId = user?.organizationId ?? "00000000-0000-0000-0000-000000000000"; + + try { + const response = await fetch(`/api/subscription/${subscriptionId}/financialAssistant`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId, + }, + body: JSON.stringify({ + organizationId: userOrganizationId, + activateFinancialAssistant: true, + }), + }); + + if (!response.ok) { + throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); + } + + const parsedResponse: SubscriptionResponse = await response.json(); + const { message, subscription } = parsedResponse.data; + + console.log("Subscription upgraded successfully:", message); + return subscription; } catch (error) { console.error("Error upgrading subscription:", error instanceof Error ? error.message : error); throw error; diff --git a/frontend/src/components/SideMenu/SideMenu.tsx b/frontend/src/components/SideMenu/SideMenu.tsx index a15bab9c..bd674142 100644 --- a/frontend/src/components/SideMenu/SideMenu.tsx +++ b/frontend/src/components/SideMenu/SideMenu.tsx @@ -96,6 +96,18 @@ export const SideMenu: React.FC = ({ isCollapsed, setIsCollapsed )} + {user?.role === "admin" && ( +
  • + setActiveOption("Financial Assistant")} + > + + {!isCollapsed && "Financial Assistant"} + +
  • + )}
    setIsCollapsed(!isCollapsed)}> diff --git a/frontend/src/pages/financialassistant/FinancialAssistant.module.css b/frontend/src/pages/financialassistant/FinancialAssistant.module.css new file mode 100644 index 00000000..e003afbb --- /dev/null +++ b/frontend/src/pages/financialassistant/FinancialAssistant.module.css @@ -0,0 +1,77 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 10px; + margin-left: 20px; +} + +.table { + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +.tableContainer { + border-radius: 10px; + overflow: hidden; + border: 1px solid #d9d9d9; + margin-top: 35px; +} + +.table thead { + background-color: #9fc51d; + color: white; +} + +.table th, +.table td { + padding: 10px; +} + +.searchContainer { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; +} + +.searchIcon { + font-size: 18px; + margin-right: 5px; + transform: scaleX(-1); +} + +.pillRole { + width: 100px; + padding: 5px; + border-radius: 15px; +} + +.roleContainer { + width: 100%; + justify-content: flex-start; + justify-items: center; + text-align: center; + display: flex; + text-transform: capitalize; +} + +.textJustify { + text-align: justify; +} diff --git a/frontend/src/pages/financialassistant/FinancialAssistant.tsx b/frontend/src/pages/financialassistant/FinancialAssistant.tsx new file mode 100644 index 00000000..5cf47566 --- /dev/null +++ b/frontend/src/pages/financialassistant/FinancialAssistant.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useState } from "react"; +import { useAppContext } from "../../providers/AppProviders"; +import { getFinancialAssistant, upgradeSubscription, removeFinancialAssistant } from "../../api"; // Asegúrate de importar la función +import { + Spinner, + PrimaryButton, + DefaultButton, + Dialog, + DialogType, + DialogFooter, + MessageBar, + MessageBarType, +} from "@fluentui/react"; +import styles from "./FinancialAssistant.module.css"; + +const FinancialAssistant = () => { + const { user, organization } = useAppContext(); + const [subscriptionStatus, setSubscriptionStatus] = useState(null); + const [loading, setLoading] = useState(true); + const [showSubscribeDialog, setShowSubscribeDialog] = useState(false); + const [showUnsubscribeDialog, setShowUnsubscribeDialog] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchStatus = async () => { + setLoading(true); + try { + if (!user?.organizationId) { + throw new Error("Organization ID is required"); + } + const { financial_assistant_active } = await getFinancialAssistant({ + user: { + ...user, + organizationId: user.organizationId, + }, + subscriptionId: organization?.subscriptionId ?? "default-org-id" + }); + setSubscriptionStatus(financial_assistant_active); + } catch (error: any) { + console.log(error) + if (error.status === false) { + setSubscriptionStatus(false); + setError("Financial Assistant feature is not present in this subscription."); + } else if (error.status === null) { + setError("Bad request: unable to retrieve subscription status."); + } else { + setError("An error occurred while fetching subscription status."); + } + } finally { + setLoading(false); + } + }; + + fetchStatus(); + }, [user, organization]); + + const handleSubscribe = async () => { + try { + setLoading(true); + const userObj = user ? { id: user.id, name: user.name, organizationId: user.organizationId ?? "default-org-id" } : undefined; + await upgradeSubscription({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); + setSubscriptionStatus(true); + setShowSubscribeDialog(false); + } catch { + setError("An error occurred while subscribing to the Financial Assistant feature."); + } finally { + setLoading(false); + } + }; + + const handleUnsubscribe = async () => { + try { + setLoading(true); + const userObj = user ? { id: user.id, name: user.name, organizationId: user.organizationId ?? "default-org-id" } : undefined; + await removeFinancialAssistant({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); + setSubscriptionStatus(false); + setShowUnsubscribeDialog(false); + } catch { + setError("An error occurred while unsubscribing from the Financial Assistant feature."); + } finally { + setLoading(false); + } + }; + + if (!user) { + return
    Please log in to manage your subscription.
    ; + } + + if (loading) { + return ; + } + + return ( +
    +

    Financial Assistant Subscription

    + + {error && {error}} + +
    + {subscriptionStatus ? ( + <> + + You are subscribed to the Financial Assistant feature. + + setShowUnsubscribeDialog(true)} + /> + + ) : ( + <> + + You are not subscribed to the Financial Assistant feature. + + setShowSubscribeDialog(true)} + /> + + )} +
    + + + + +
    + ); +}; + +export default FinancialAssistant; From 25c6832501d208316c88251bf00fd8686dd754a0 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Fri, 15 Nov 2024 07:44:58 -0400 Subject: [PATCH 172/820] FA-139 Implement the new UI menu (#149) --- .../components/SideMenu/SideMenu.module.css | 3 +- .../src/components/Sidebar/Sidebar.module.css | 48 ++++++++++++++----- frontend/src/components/Sidebar/Sidebar.tsx | 12 ++--- .../src/components/Sidebar/SidebarItem.tsx | 6 +-- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/SideMenu/SideMenu.module.css b/frontend/src/components/SideMenu/SideMenu.module.css index 6b4a414e..87b3897e 100644 --- a/frontend/src/components/SideMenu/SideMenu.module.css +++ b/frontend/src/components/SideMenu/SideMenu.module.css @@ -11,6 +11,7 @@ align-items: center; padding-top: 20px; transition: width 0.3s ease; + } .sidebar.collapsed { @@ -67,7 +68,7 @@ aside nav ul li a { } aside nav ul li a:hover { - background-color: #85a717; + /*background-color: #85a717;*/ } .sidebar.collapsed aside nav ul li a:hover { diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css index a598c971..28118441 100644 --- a/frontend/src/components/Sidebar/Sidebar.module.css +++ b/frontend/src/components/Sidebar/Sidebar.module.css @@ -1,12 +1,28 @@ /*==================================== = Sidebar Styles = ====================================*/ +.submenuLink:hover { + + background-color: #ffffff30; + color: #000000; +} + +.sidebarLink:hover, .submenuLink:hover { + color: #000000; +} + .submenuLink { - color: rgb(41 52 61); - text-decoration: none; + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + text-decoration: none; + color: #333; + font-size: 14px; + transition: background-color 0.3s ease; } /* Nav Styles */ .nav { + width: 100%; } @@ -20,7 +36,7 @@ } .navLink { - color: #12100b; + color: #333; font-size: 15px; text-decoration: none; padding: 10px 30px; @@ -41,6 +57,7 @@ background-color: #85a717; } + .navLinkActive { background-color: #f5f5f5; border-left: 6px solid #0a0b0a; @@ -89,6 +106,7 @@ } /* Sidebar Link */ + /* Change size of the letters*/ .sidebarLink { display: flex; font-size: 14px; @@ -113,15 +131,21 @@ :global(.sidebarLink:hover) { color: #000000; } - - .sidebarLinkActive { + + /* CHANGE COLOR PREMIUM TIERS BUTTON*/ + .sidebarLinkActive{ background-color: #000000; color: #fff; box-shadow: 0px 17px 20px -8px rgba(77, 91, 236, 0.23); } - .sidebarLinkActiveIcon { + .sidebarLinkActive:hover{ color: #fff; + box-shadow: 0px 17px 20px -8px rgba(77, 91, 236, 0.23); + } + + .sidebarLinkActiveIcon { + color: #a7c444; } .sidebarLinkActiveI { @@ -135,10 +159,11 @@ padding: 7px 0; line-height: 26px; letter-spacing: 1.5px; + color: #333; + transition: color 0.3s ease; text-transform: uppercase; - color: var(--bs-heading-color); } - + .navSmallCapIcon { display: none; } @@ -147,8 +172,9 @@ .submenu { max-height: 0; overflow: hidden; - transition: max-height 0.3s ease; + transition: max-height 0.3s ease, padding 0.3s ease; padding-left: 20px; + margin: 0; border-radius: 0.4em; background-color: #ffffff30; } @@ -168,7 +194,7 @@ .submenuItem { border-bottom: 1px solid #33333329; - padding: 0.9em 0; + padding: 4px 8px; } /* Sidebar Divider */ @@ -183,7 +209,7 @@ /* Left Sidebar */ .leftSidebar { width: 280px; - background-color: #A7C444; + background-color: #a7c444; position: absolute; transition: 0.2s ease-in; height: 100%; diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 634d41ee..4c3ac31c 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -6,12 +6,12 @@ import { IconMessagePlus, IconBell, IconDiscountCheck, - IconUsers, IconStar, IconFileInvoice, - IconListCheck, + IconChecklist, IconHeadset, - IconDots + IconDots, + IconSubtask } from "@tabler/icons-react"; import salesLogo from "../../img/logo.png"; import styles from "./Sidebar.module.css"; @@ -70,7 +70,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { {/* Subscription Module */} } to="/subscription.html" /> - } to="/manage-email-lists.html" /> + } to="/manage-email-lists.html" /> {/* Premium Features with Submenu */} = ({ isCollapsed, setIsCollapsed }) => { } to="/view-manage-reports.html" /> - } to="/details-settings.html" /> + } to="/details-settings.html" />
  • @@ -105,7 +105,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => {
  • - } to="/help-center.html" /> + } to="/financial-assitant" /> {/* End Sidebar navigation */} diff --git a/frontend/src/components/Sidebar/SidebarItem.tsx b/frontend/src/components/Sidebar/SidebarItem.tsx index 62c5a3b0..bc596696 100644 --- a/frontend/src/components/Sidebar/SidebarItem.tsx +++ b/frontend/src/components/Sidebar/SidebarItem.tsx @@ -24,12 +24,12 @@ const SidebarItem: React.FC = ({ title, icon, to = "#", links return (
  • - {React.cloneElement(icon, { className: styles.sidebarLinkIcon })} + {React.cloneElement(icon, { className: isActive ? styles.sidebarLinkActiveIcon : styles.sidebarLinkIcon })} {title} {links && ( @@ -55,4 +55,4 @@ const SidebarItem: React.FC = ({ title, icon, to = "#", links ); }; -export default SidebarItem; +export default SidebarItem; \ No newline at end of file From 0762787842707d3f553e9323d3bf635c4d6cd9d6 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:03:54 -0400 Subject: [PATCH 173/820] FA-192 Implement expected behavior in the navbar UI (#148) * Added functionality to the first three navbar buttons and added user info * Added the dropdown function for the Profile bar * Minor fixes in the Profile Card --- frontend/src/components/Navbar/NavBar.tsx | 64 +++++++++++-------- .../src/components/SettingsPanel/index.tsx | 4 +- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index baa961bb..ea3f6ba2 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -1,14 +1,21 @@ -import React from "react"; +import React, { useState } from "react"; import styles from "./Navbar.module.css"; import { IconMenu2, IconMessageCircle, IconHistory, IconSettings, IconBell, IconUser, IconMail, IconListCheck } from "@tabler/icons-react"; import { useAppContext } from "../../providers/AppProviders"; +import { Link } from "react-router-dom"; + interface NavbarProps { setIsCollapsed: React.Dispatch>; } const Navbar: React.FC = ({ setIsCollapsed }) => { - const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); + const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel, user } = useAppContext(); + const historyContent = showHistoryPanel ? "Hide chat history" : "Show chat history"; + const feedbackContent = showFeedbackRatingPanel ? "Hide feedback panel" : "Show feedback panel"; + const userName = user?.name || ""; // Default to empty string if user or user.name is null + const email = user?.email || " "; + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const handleShowHistoryPanel = () => { setShowHistoryPanel(!showHistoryPanel); @@ -32,6 +39,13 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { setIsCollapsed(false); }; + const handleOnClickProfileCard = () => { + setIsDropdownOpen(!isDropdownOpen); + setShowHistoryPanel(false); + setShowFeedbackRatingPanel(false); + setSettingsPanel(false); + } + return (
  • {loading ? (
    +

    Loading your settings

    -

    Loading your settings

    ) : (
    From 5cffcfa7f3597fa839db8595ded79fe89dba8619 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 15 Nov 2024 17:13:05 -0400 Subject: [PATCH 174/820] FA-185 create endpoint for getting subscriptions active tiers (#150) --- backend/app.py | 286 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 218 insertions(+), 68 deletions(-) diff --git a/backend/app.py b/backend/app.py index 0c714ef0..5dfd9354 100644 --- a/backend/app.py +++ b/backend/app.py @@ -38,7 +38,14 @@ from http import HTTPStatus # Best Practice: Use standard HTTP status codes import smtplib from werkzeug.exceptions import BadRequest, Unauthorized, NotFound -from utils import create_error_response, create_success_response, SubscriptionError, InvalidSubscriptionError, InvalidFinancialPriceError, require_client_principal +from utils import ( + create_error_response, + create_success_response, + SubscriptionError, + InvalidSubscriptionError, + InvalidFinancialPriceError, + require_client_principal, +) import stripe.error @@ -279,6 +286,7 @@ def index(*, context): # route for other static files + @app.route("/") def static_files(path): # Don't require authentication for static assets @@ -1085,7 +1093,8 @@ def deleteUser(): logging.exception("[webbackend] exception in /api/checkUser") return jsonify({"error": str(e)}), 500 -@app.route('/logout') + +@app.route("/logout") def logout(): # Clear the user's session session.clear() @@ -1098,6 +1107,7 @@ def logout(): ) return redirect(logout_url) + @app.route("/api/inviteUser", methods=["POST"]) def sendEmail(): if ( @@ -1518,7 +1528,7 @@ def get_product_prices_endpoint(): return jsonify({"error": str(e)}), 500 -#ADD FINANCIAL ASSITANT A SUBSCRIPTION +# ADD FINANCIAL ASSITANT A SUBSCRIPTION @app.route("/api/subscription//financialAssistant", methods=["PUT"]) @require_client_principal # Security: Enforce authentication def financial_assistant(subscriptionId): @@ -1585,10 +1595,8 @@ def financial_assistant(subscriptionId): f"An error occurred while processing your request", HTTPStatus.NOT_FOUND ) except stripe.error.InvalidRequestError as e: - logging.error(f"Stripe API error: {str(e)}") - return create_error_response( - "Invalid Subscription ID", HTTPStatus.NOT_FOUND - ) + logging.error(f"Stripe API error: {str(e)}") + return create_error_response("Invalid Subscription ID", HTTPStatus.NOT_FOUND) except stripe.error.StripeError as e: # Logging: Error level for API failures logging.error(f"Stripe API error: {str(e)}") @@ -1608,7 +1616,8 @@ def financial_assistant(subscriptionId): "An unexpected error occurred", HTTPStatus.INTERNAL_SERVER_ERROR ) -#DELETE FINANCIAL ASSITANT A SUBSCRIPTION + +# DELETE FINANCIAL ASSITANT A SUBSCRIPTION @app.route("/api/subscription//financialAssistant", methods=["DELETE"]) @require_client_principal # Security: Enforce authentication def remove_financial_assistant(subscriptionId): @@ -1639,19 +1648,21 @@ def remove_financial_assistant(subscriptionId): if not subscriptionId or not isinstance(subscriptionId, str): raise BadRequest("Invalid subscription ID") - logging.info(f"Modifying subscription {subscriptionId} to remove Financial Assistant") + logging.info( + f"Modifying subscription {subscriptionId} to remove Financial Assistant" + ) try: # Get the subscription to find the Financial Assistant item subscription = stripe.Subscription.retrieve(subscriptionId) - + # Find the Financial Assistant item assistant_item_id = None - for item in subscription['items']['data']: - if item['price']['id'] == FINANCIAL_ASSISTANT_PRICE_ID: - assistant_item_id = item['id'] + for item in subscription["items"]["data"]: + if item["price"]["id"] == FINANCIAL_ASSISTANT_PRICE_ID: + assistant_item_id = item["id"] break - + if not assistant_item_id: raise NotFound("Financial Assistant item not found in subscription") @@ -1666,7 +1677,9 @@ def remove_financial_assistant(subscriptionId): }, ) - logging.info(f"Successfully removed Financial Assistant from subscription {subscriptionId}") + logging.info( + f"Successfully removed Financial Assistant from subscription {subscriptionId}" + ) return create_success_response( { @@ -1681,9 +1694,7 @@ def remove_financial_assistant(subscriptionId): except stripe.error.InvalidRequestError as e: logging.error(f"Stripe API error: {str(e)}") - return create_error_response( - "Invalid Subscription ID", HTTPStatus.NOT_FOUND - ) + return create_error_response("Invalid Subscription ID", HTTPStatus.NOT_FOUND) except stripe.error.StripeError as e: logging.error(f"Stripe API error: {str(e)}") return create_error_response( @@ -1698,7 +1709,8 @@ def remove_financial_assistant(subscriptionId): "An unexpected error occurred", HTTPStatus.INTERNAL_SERVER_ERROR ) -#CHECK STATUS SUBSCRIPTION FA (FINANCIAL ASSITANT) + +# CHECK STATUS SUBSCRIPTION FA (FINANCIAL ASSITANT) @app.route("/api/subscription//financialAssistant", methods=["GET"]) @require_client_principal # Security: Enforce authentication def get_financial_assistant_status(subscriptionId): @@ -1720,79 +1732,217 @@ def get_financial_assistant_status(subscriptionId): } } } - + Raises: NotFound: If the subscription is not found. HttpCode: 404 Unauthorized: If client principal ID is missing. HttpCode: 401 """ try: subscription = stripe.Subscription.retrieve(subscriptionId) - + financial_assistant_active = any( - item.price.id == FINANCIAL_ASSISTANT_PRICE_ID for item in subscription["items"]["data"] + item.price.id == FINANCIAL_ASSISTANT_PRICE_ID + for item in subscription["items"]["data"] ) financial_assistant_item = next( - (item for item in subscription["items"]["data"] - if item.price.id == FINANCIAL_ASSISTANT_PRICE_ID), - None + ( + item + for item in subscription["items"]["data"] + if item.price.id == FINANCIAL_ASSISTANT_PRICE_ID + ), + None, ) - + if financial_assistant_item is False: - logging.info(f"Financial Assistant not actived in subscription: {subscriptionId}") - return jsonify({ - "data": { - "financial_assistant_active": False, - "message": "Financial Assistant is not active in this subscription." - } - }), HTTPStatus.OK - + logging.info( + f"Financial Assistant not actived in subscription: {subscriptionId}" + ) + return ( + jsonify( + { + "data": { + "financial_assistant_active": False, + "message": "Financial Assistant is not active in this subscription.", + } + } + ), + HTTPStatus.OK, + ) + if financial_assistant_item is None: - logging.info(f"Financial Assistant not found in subscription: {subscriptionId}") - return jsonify({ - "data": { - "financial_assistant_active": False, - "message": "Financial Assistant not founded in this subscription." - } - }), HTTPStatus.OK + logging.info( + f"Financial Assistant not found in subscription: {subscriptionId}" + ) + return ( + jsonify( + { + "data": { + "financial_assistant_active": False, + "message": "Financial Assistant not founded in this subscription.", + } + } + ), + HTTPStatus.OK, + ) - return jsonify({ - "data": { - "financial_assistant_active": financial_assistant_active, - "subscription": { - "id": subscription.id, - "status": subscription.status, - "price_id": financial_assistant_item.price.id + return ( + jsonify( + { + "data": { + "financial_assistant_active": financial_assistant_active, + "subscription": { + "id": subscription.id, + "status": subscription.status, + "price_id": financial_assistant_item.price.id, + }, + } } - } - }), HTTPStatus.OK + ), + HTTPStatus.OK, + ) except stripe.error.InvalidRequestError: logging.error(f"Invalid Subscription ID: {subscriptionId}") - return jsonify({ - "error": { - "message": "Invalid Subscription ID", - "status": 404 - } - }), HTTPStatus.NOT_FOUND + return ( + jsonify({"error": {"message": "Invalid Subscription ID", "status": 404}}), + HTTPStatus.NOT_FOUND, + ) except stripe.error.StripeError as e: logging.error(f"Stripe API error: {str(e)}") - return jsonify({ - "error": { - "message": "An error occurred while processing your request.", - "status": 400 - } - }), HTTPStatus.BAD_REQUEST + return ( + jsonify( + { + "error": { + "message": "An error occurred while processing your request.", + "status": 400, + } + } + ), + HTTPStatus.BAD_REQUEST, + ) except Exception as e: logging.exception(f"Unexpected error: {str(e)}") - return jsonify({ - "error": { - "message": "An unexpected error occurred", - "status": 500 - } - }), HTTPStatus.INTERNAL_SERVER_ERROR + return ( + jsonify( + {"error": {"message": "An unexpected error occurred", "status": 500}} + ), + HTTPStatus.INTERNAL_SERVER_ERROR, + ) + + +@app.route("/api/subscription//tiers", methods=["GET"]) +@require_client_principal # Security: Enforce authentication +def get_subscription_details(subscription_id): + try: + # Retrieve the subscription from Stripe + subscription = stripe.Subscription.retrieve( + subscription_id, expand=["items.data.price.product"] + ) + + # Log subscription details + logging.info(f"[webbackend] Retrieved subscription: {subscription.id}") + + # Determine the subscription tiers + subscription_tiers = determine_subscription_tiers(subscription) + + # Prepare the response + result = { + "subscriptionId": subscription.id, + "subscriptionTiers": subscription_tiers, + "subscriptionData": { + "status": subscription.status, + "current_period_end": subscription.current_period_end, + "items": [ + { + "product_id": item.price.product.id, + "product_name": item.price.product.name, + "price_id": item.price.id, + "price_nickname": item.price.nickname, + "unit_amount": item.price.unit_amount, + "currency": item.price.currency, + "quantity": item.quantity, + } + for item in subscription["items"]["data"] + ], + }, + } + + return jsonify(result), 200 + except stripe.error.InvalidRequestError as e: + logging.exception("Invalid subscription ID provided") + return jsonify({"error": "Invalid subscription ID provided."}), 400 + except stripe.error.AuthenticationError: + logging.exception("Authentication with Stripe's API failed") + return jsonify({"error": "Authentication with Stripe failed."}), 500 + except stripe.error.APIConnectionError: + logging.exception("Network communication with Stripe failed") + return jsonify({"error": "Network communication with Stripe failed."}), 502 + except stripe.error.StripeError as e: + logging.exception("Stripe error occurred") + return jsonify({"error": "An error occurred with Stripe."}), 500 + except Exception as e: + logging.exception("Exception in /api/subscription//tiers") + return jsonify({"error": str(e)}), 500 + + +def determine_subscription_tiers(subscription): + """ + Determines the subscription tiers based on the products and prices in the Stripe subscription. + Updated to include 'Premium' tiers. + """ + tiers = [] + + # Flags to identify which products and prices are included + has_ai_assistant_basic = False + has_ai_assistant_custom = False + has_ai_assistant_premium = False + has_financial_assistant = False + + # Iterate through subscription items + for item in subscription["items"]["data"]: + product = item["price"]["product"] + product_name = product.get("name", "").lower() + nickname = ( + item["price"]["nickname"] + if item.get("price") + and isinstance(item["price"], dict) + and "nickname" in item["price"] + else None + ) + price_nickname = nickname.lower() if nickname else "" + if "ai assistant" in product_name: + if "basic" in price_nickname: + has_ai_assistant_basic = True + elif "custom" in price_nickname: + has_ai_assistant_custom = True + elif "premium" in price_nickname: + has_ai_assistant_premium = True + elif "financial assistant" in product_name: + has_financial_assistant = True + + # Determine tiers based on flags + if has_ai_assistant_basic: + tiers.append("Basic") + if has_ai_assistant_custom: + tiers.append("Custom") + if has_ai_assistant_premium: + tiers.append("Premium") + if has_financial_assistant: + tiers.append("Financial Assistant") + + # Combine tiers into possible combinations + if has_financial_assistant: + if has_ai_assistant_basic: + tiers.append("Basic + Financial Assistant") + if has_ai_assistant_custom: + tiers.append("Custom + Financial Assistant") + if has_ai_assistant_premium: + tiers.append("Premium + Financial Assistant") + + return tiers if __name__ == "__main__": From 1b2c8c84b107fd6518b79e73b1901fe361364a9d Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:58:29 -0400 Subject: [PATCH 175/820] FA-177 Added the Financial Assistant Toggle (#151) --- frontend/src/components/Navbar/NavBar.tsx | 17 +++++++++++++++ .../src/components/Navbar/Navbar.module.css | 21 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index ea3f6ba2..45dd29e3 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -39,12 +39,20 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { setIsCollapsed(false); }; + + + const handleOnClickProfileCard = () => { setIsDropdownOpen(!isDropdownOpen); setShowHistoryPanel(false); setShowFeedbackRatingPanel(false); setSettingsPanel(false); } + + const handleFinancialAgent = () => { + //Leaving the Handler for the future funcionality + }; + return (
    diff --git a/frontend/src/components/Navbar/Navbar.module.css b/frontend/src/components/Navbar/Navbar.module.css index fa155b90..f2e277c2 100644 --- a/frontend/src/components/Navbar/Navbar.module.css +++ b/frontend/src/components/Navbar/Navbar.module.css @@ -31,7 +31,7 @@ margin: 0; } -.financialToggle{ +.financialToggleText{ font-size: 0.9rem; font-weight: 410; color: #000000; @@ -52,20 +52,27 @@ box-shadow: 0 0 0 0.2rem #aad12c; } + /* Responsive User Details */ -@media (max-width: 576px) { +@media (min-width: 577px) { .userDetails { - display: none; + display: flex; + flex-direction: column; } + } -@media (min-width: 577px) { +@media (max-width: 900px) { .userDetails { - display: flex; - flex-direction: column; + display: none; + } + .financialToggleText{ + display: none; } } + + /* Sidebartoggler */ .sidebartoggler { /* Define styles if needed */ @@ -76,11 +83,28 @@ /* Define styles if needed */ } +.headerNavbar { + -ms-flex-wrap: nowrap !important; + flex-wrap: nowrap !important; + display: flex; + justify-content: center; + align-items: center; + gap: 15px; +} + @media (max-width: 991.98px) { .headerNavbar { -ms-flex-wrap: nowrap !important; flex-wrap: nowrap !important; + display: flex; + justify-content: center; + align-items: center; + gap: 15px; } + .profileCard{ + background-color:rgb(248, 249, 250); + } + } .appHeader{ From 5adf7f00619d4bf5d4265b4c442d03ebbbdc427b Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Wed, 20 Nov 2024 11:42:42 -0400 Subject: [PATCH 186/820] FA 216 add admin features to the new sidebar (#159) * FA-209 implment RBCA and susbcription tiers Access * Add Admin Features to the New Sidebar * Update Sidebar.tsx --------- Co-authored-by: Manuel Castro --- .../src/components/Sidebar/Sidebar.module.css | 7 +-- frontend/src/components/Sidebar/Sidebar.tsx | 45 +++++++++++++++++-- .../src/components/Sidebar/SidebarItem.tsx | 19 ++++---- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css index 7ff32036..a1a72472 100644 --- a/frontend/src/components/Sidebar/Sidebar.module.css +++ b/frontend/src/components/Sidebar/Sidebar.module.css @@ -114,16 +114,17 @@ align-items: center; line-height: 26px; position: relative; - padding: 9px 16px; + padding: 9px 9px; border-radius: 6px; - gap: 10px; + gap: 6px; text-decoration: none; font-weight: 500; margin-bottom: 4px; + width: 100%; } .sidebarLinkIcon { - font-size: 18px; + font-size: 1.5rem; color: var(--bs-heading-color); opacity: 0.8; } diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 537d24bf..629ce3ad 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -1,13 +1,16 @@ // Sidebar.tsx -import React, { useCallback, useMemo } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { IconX, IconMessagePlus, IconBell, - IconDiscountCheck, + IconRosetteDiscountCheck, IconStar, IconFileInvoice, + IconAddressBook, + IconUserCheck, + IconUsers, IconChecklist, IconHeadset, IconDots, @@ -25,6 +28,11 @@ interface SidebarProps { } const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { + const [activeItem, setActiveItem] = useState(null); + const handleItemClick = (itemTitle: string) => { + setActiveItem(itemTitle); + }; + const handleOnClickCloseSideBar = () => { setIsCollapsed(true); }; @@ -77,12 +85,41 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { { divider: true }, + { + section: "Admin Features", + items: [ + { + title: "Roles and Access", + icon: , + to: "/admin", + tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], + roles: ["admin"] + }, + { + title: "Invitations", + icon: , + to: "/invitations", + tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], + roles: ["admin"] + }, + { + title: "Organization Management", + icon: , + to: "/organization", + tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], + roles: ["admin"] + } + ] + }, + { + divider: true + }, { section: "Subscription", items: [ { title: "Subscription Management", - icon: , + icon: , to: "/subscription", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin"] @@ -249,6 +286,8 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon={item.icon} to={item.to} links={item.links} + isActive={activeItem=== item.title} + onClick={()=>handleItemClick(item.title)} /> ); })} diff --git a/frontend/src/components/Sidebar/SidebarItem.tsx b/frontend/src/components/Sidebar/SidebarItem.tsx index c7727145..617c22c3 100644 --- a/frontend/src/components/Sidebar/SidebarItem.tsx +++ b/frontend/src/components/Sidebar/SidebarItem.tsx @@ -1,4 +1,3 @@ -// SidebarItem.tsx import React from "react"; import { Link } from "react-router-dom"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; @@ -9,16 +8,16 @@ interface SidebarItemProps { icon: JSX.Element; to?: string; links?: Array<{ title: string; href: string }>; + isActive: boolean; + onClick: () => void; } -const SidebarItem: React.FC = ({ title, icon, to, links }) => { - const [isActive, setIsActive] = React.useState(false); - +const SidebarItem: React.FC = ({ title, icon, to, links, isActive, onClick }) => { const toggleSubmenu = (e: React.MouseEvent) => { if (links) { e.preventDefault(); - setIsActive(!isActive); } + onClick(); }; return ( @@ -33,7 +32,7 @@ const SidebarItem: React.FC = ({ title, icon, to, links }) => {React.cloneElement(icon, { className: isActive ? styles.sidebarLinkActiveIcon : styles.sidebarLinkIcon })} - {title} + {title} {isActive ? : } @@ -52,11 +51,15 @@ const SidebarItem: React.FC = ({ title, icon, to, links }) => ) : ( - + {React.cloneElement(icon, { className: isActive ? styles.sidebarLinkActiveIcon : styles.sidebarLinkIcon })} - {title} + {title} )} From d78d5e15ae976d9b8752e339b54ba00c6b808f6b Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Wed, 20 Nov 2024 17:53:48 -0400 Subject: [PATCH 187/820] FA-220 Fix the scrollbar and issues in the side bar (#165) --- frontend/src/components/Navbar/NavBar.tsx | 1 - frontend/src/components/Sidebar/Sidebar.module.css | 2 +- frontend/src/components/Sidebar/Sidebar.tsx | 2 +- frontend/src/pages/invitations/Invitations.tsx | 3 --- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index e198a031..ff4053a4 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -67,7 +67,6 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { setShowHistoryPanel(false); setShowFeedbackRatingPanel(false); setSettingsPanel(false); - }; const handleFinancialAgent = () => { diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css index a1a72472..84703c67 100644 --- a/frontend/src/components/Sidebar/Sidebar.module.css +++ b/frontend/src/components/Sidebar/Sidebar.module.css @@ -250,7 +250,7 @@ .scrollSidebar { overflow-y: auto; padding: 0 16px 0 28px; - height: calc(100vh - 80px); + height: calc(100vh - 128px); border-radius: 12px; } diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 629ce3ad..c58e98b4 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -69,7 +69,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { { title: "AI Chat", icon: , - to: "/chat", + to: "/", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin", "user"] }, diff --git a/frontend/src/pages/invitations/Invitations.tsx b/frontend/src/pages/invitations/Invitations.tsx index e1b63d58..41f37f8b 100644 --- a/frontend/src/pages/invitations/Invitations.tsx +++ b/frontend/src/pages/invitations/Invitations.tsx @@ -76,9 +76,6 @@ const Invitations = () => { return
    Please log in to view your invitations.
    ; } - if (loading) { - return
    Loading invitations...
    ; - } return (
    From 2d9e8059b02dcafb752e181bec60274e3ed58c4b Mon Sep 17 00:00:00 2001 From: wanjiru_mambo <99056428+wmambo@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:46:59 -0500 Subject: [PATCH 188/820] FA 211 remove attachment and delete buttons (#157) * commented out code associated with attachment and clear chat buttons * commented out code associated with attachment and clear chat * removed .DS_Store --- .gitignore | 5 ++++- frontend/.DS_Store | Bin 6148 -> 6148 bytes frontend/src/.DS_Store | Bin 0 -> 6148 bytes .../src/components/.DS_Store | Bin 8196 -> 6148 bytes .../ClearChatButton.module.css | 5 +++-- .../ClearChatButton/ClearChatButton.tsx | 5 +++-- .../src/components/ClearChatButton/index.tsx | 3 ++- .../QuestionInput/QuestionInput.module.css | 10 ++++++---- .../QuestionInput/QuestionInput.tsx | 5 +++-- 9 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 frontend/src/.DS_Store rename .DS_Store => frontend/src/components/.DS_Store (68%) diff --git a/.gitignore b/.gitignore index af1eb595..798ecb2c 100644 --- a/.gitignore +++ b/.gitignore @@ -154,4 +154,7 @@ static/ settings.json -flask_session/ \ No newline at end of file +flask_session/ + +# ignore .DS_Store +.DS_Store \ No newline at end of file diff --git a/frontend/.DS_Store b/frontend/.DS_Store index f5c32f4651cce67bf98f6cc1ff4e8d7cc8b7dbbd..5e47f261b75babe4f7214ab9ff193dfa47f501d3 100644 GIT binary patch delta 38 ucmZoMXfc@J&&azmU^gQp?`9q*FGh}(;^ds9{QR8F-pngmHnVg5A!;D$AE3iMA025$j?4Z?1i zdoKYj768`7DG(8u1{D}o%@#w0j(Ewunm7dpT{N2y&6_nl6!qJ2e(`kC8px3fP=UDu zt5~*H|KH$O`u{nJD=I(*9!ddiw!3YICuMD2JkDxufxp8o=NE2LCa|td@dFQr{^ji^vhv`wp|Cu`*6L#d^r(b2yP|^sVi7l(GS#abspXFLU9(=p;gw=Cb$`PF3`!Yfu_>n+Gfq(AU z^7pfx|JNboXNZ9d<%m4L{r)xD!PBq}{x0rJ;i;%RhlT=z>sUML>j+Q-EyGcHJsM=@ z_ki7n(b-)XQTBghj5KlJ4mnV9TF1@#e-3M%ya%{LH&3LB1AoZ@Rjn`9XEB-G)k`Kd vyS&ELz-5}jWiHwhbh#e__`ClPLwweNnwr?h%tef#$%_DMgLLA+KXu?Y!1D(5 diff --git a/frontend/src/components/ClearChatButton/ClearChatButton.module.css b/frontend/src/components/ClearChatButton/ClearChatButton.module.css index 7a2cb263..2123a0bb 100644 --- a/frontend/src/components/ClearChatButton/ClearChatButton.module.css +++ b/frontend/src/components/ClearChatButton/ClearChatButton.module.css @@ -1,4 +1,5 @@ -.container { +/* removing the styling for clear chat +/*.container { display: flex; align-items: center; gap: 6px; @@ -7,4 +8,4 @@ .disabled { opacity: 0.4; -} +}*/ diff --git a/frontend/src/components/ClearChatButton/ClearChatButton.tsx b/frontend/src/components/ClearChatButton/ClearChatButton.tsx index a37cdec0..22ff3f94 100644 --- a/frontend/src/components/ClearChatButton/ClearChatButton.tsx +++ b/frontend/src/components/ClearChatButton/ClearChatButton.tsx @@ -1,4 +1,5 @@ -import { Text } from "@fluentui/react"; +// removing the clear chat button +/*import { Text } from "@fluentui/react"; import { Delete24Regular } from "@fluentui/react-icons"; import styles from "./ClearChatButton.module.css"; @@ -26,4 +27,4 @@ export const ClearChatButton = ({ className, disabled, onClick }: Props) => { {reiniciar_text}
    ); -}; +};*/ diff --git a/frontend/src/components/ClearChatButton/index.tsx b/frontend/src/components/ClearChatButton/index.tsx index c283e71c..3bd2712c 100644 --- a/frontend/src/components/ClearChatButton/index.tsx +++ b/frontend/src/components/ClearChatButton/index.tsx @@ -1 +1,2 @@ -export * from "./ClearChatButton"; +// removing clear chat +/*export * from "./ClearChatButton";*/ diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index af81ae9e..fb1f564f 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -7,12 +7,13 @@ background: white; } -.attachmentContainer { +/* attachment container is not needed if we don't need an attachment button */ +/*.attachmentContainer { height: "100%"; justify-content: "center"; align-items: center; display: flex; -} +}*/ .questionInputTextArea { width: 100%; @@ -36,9 +37,10 @@ opacity: 0.4; } -.attachmentButton { +/* commenting out the css associated with the attachment button instead of permanently deleting it*/ +/*.attachmentButton { transform: rotate(-135deg) scaleX(-1) translate(-5px, 0px); cursor: pointer; font-size: 24px; color: #9f9c9c; -} +}*/ diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 5f24b6f8..44a346e5 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -17,7 +17,8 @@ interface Props { import { useFilePicker } from "use-file-picker"; -export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: string) => void }) => { +// commenting out the attachment button instead of permanently deleting it +/*export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: string) => void }) => { const [files, setFiles] = useState([]); const [loadingFiles, setLoadingFiles] = useState(false); const [error, setError] = useState(""); @@ -166,7 +167,7 @@ export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url:
    ); -}; +};*/ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { const { user, organization } = useAppContext(); From 325f403e29cb615df56182be0ca5554f7593cedc Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:47:59 -0400 Subject: [PATCH 189/820] FA-219 Fix the logout button (#164) * FA-219 Fix the Logout Button * Removed the Use Effect. It wasn't necessary --- frontend/src/App.tsx | 2 ++ frontend/src/pages/logout/Logout.tsx | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 frontend/src/pages/logout/Logout.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e77dffc9..4f15c7c1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import UploadResources from "./pages/resources/UploadResources"; import RequestStudies from "./pages/studies/RequestStudies"; import ReportManagement from "./pages/reports/ReportManagement"; import DistributionLists from "./pages/reports/DistributionLists"; +import Logout from "./pages/logout/Logout"; import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; @@ -48,6 +49,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/pages/logout/Logout.tsx b/frontend/src/pages/logout/Logout.tsx new file mode 100644 index 00000000..df5950f4 --- /dev/null +++ b/frontend/src/pages/logout/Logout.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +const Logout: React.FC = () => { + window.location.href = "/logout"; + + return null; +}; + +export default Logout; From 2601255de359c2eab142ea1659cea062abedd33b Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Fri, 22 Nov 2024 06:22:39 -0400 Subject: [PATCH 190/820] FA-244 Fix the Submenu Dropdown (#167) --- frontend/src/components/Sidebar/Sidebar.tsx | 8 ++++++-- frontend/src/components/Sidebar/SidebarItem.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index c58e98b4..62e92657 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -36,6 +36,9 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { const handleOnClickCloseSideBar = () => { setIsCollapsed(true); }; + const handleSetActiveItem = (title: string) => { + setActiveItem(prev => (prev === title ? null : title)); + }; const { subscriptionTiers: userSubscriptionTiers, @@ -286,8 +289,9 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon={item.icon} to={item.to} links={item.links} - isActive={activeItem=== item.title} - onClick={()=>handleItemClick(item.title)} + isActive={activeItem === item.title} + setIsActive={() => handleSetActiveItem(item.title)} + onClick={() => handleItemClick(item.title)} /> ); })} diff --git a/frontend/src/components/Sidebar/SidebarItem.tsx b/frontend/src/components/Sidebar/SidebarItem.tsx index 617c22c3..b2c78822 100644 --- a/frontend/src/components/Sidebar/SidebarItem.tsx +++ b/frontend/src/components/Sidebar/SidebarItem.tsx @@ -8,16 +8,17 @@ interface SidebarItemProps { icon: JSX.Element; to?: string; links?: Array<{ title: string; href: string }>; - isActive: boolean; - onClick: () => void; + onClick: () => void; + isActive: boolean; + setIsActive: any; } -const SidebarItem: React.FC = ({ title, icon, to, links, isActive, onClick }) => { +const SidebarItem: React.FC = ({ title, icon, to, links, onClick, isActive, setIsActive }) => { const toggleSubmenu = (e: React.MouseEvent) => { if (links) { e.preventDefault(); } - onClick(); + setIsActive(isActive); }; return ( @@ -28,6 +29,7 @@ const SidebarItem: React.FC = ({ title, icon, to, links, isAct className={`${styles.sidebarLink} ${styles.navLink} ${isActive ? styles.sidebarLinkActive : ""}`} aria-expanded={isActive} onClick={toggleSubmenu} + id="submenubutton" > {React.cloneElement(icon, { className: isActive ? styles.sidebarLinkActiveIcon : styles.sidebarLinkIcon From 04c9ca5084135a5b9d5c21395d8de940d8af07b8 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Fri, 22 Nov 2024 06:24:17 -0400 Subject: [PATCH 191/820] FA-215 Fix the Navbar Items and more issues with the panels (#168) --- .../FeedbackRating/FeedbackRating.module.css | 44 +++++- .../FeedbackRating/FeedbackRating.tsx | 8 +- .../HistoryPannel/ChatHistoryPanel.tsx | 6 +- .../ChatHistoryPannel.module.css | 30 +++- .../SettingsPanel/SettingsModal.module.css | 40 +++++- .../src/components/SettingsPanel/index.tsx | 130 ++++++++++-------- frontend/src/pages/chat/Chat.module.css | 2 +- frontend/src/pages/chat/Chat.tsx | 2 +- 8 files changed, 183 insertions(+), 79 deletions(-) diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css index eba0dc2b..e13d99aa 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.module.css +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -10,7 +10,7 @@ .closeButton { border: none; - font-size: 25px; + font-size: 20px; color: #726c6c; background-color: transparent; cursor: pointer; @@ -25,14 +25,36 @@ } .container { - width: 250px; + max-width: 400px; + width: 90%; + background-color: white; + border-radius: 8px; + box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.2); + overflow: hidden; + position: relative; +} + +.cardFeedbackWrapper { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + z-index: 9999; } .cardFeedback { background-color: #f5f5f5; border: 1px solid #e3e3e3; border-radius: 6px; - box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + max-width: 400px; + width: 90%; + padding: 20px; } .header { @@ -62,6 +84,7 @@ flex-direction: column; max-width: 100%; padding: 0px 8px; + gap: 15px; } .listContainer { @@ -147,6 +170,21 @@ } } +@media (max-width: 768px) { + .container { + max-width: 90%; + padding: 15px; + } + + .title { + font-size: 16px; + } + + .closeButton { + font-size: 18px; + } +} + .saveButton { display: flex; padding: 10px 24px 8px 24px; diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index 4a48efb4..b36ca713 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -94,8 +94,12 @@ export const FeedbackRating = () => { setSelectedThumb(thumb); }; + if (!showFeedbackRatingPanel) { + return null; + } + return ( -
    +
    Feedback
    @@ -143,7 +147,7 @@ export const FeedbackRating = () => { {errorMessage !== null &&

    {errorMessage}

    }
    -
    +
    ); }; diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index d0801187..47783271 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -15,8 +15,8 @@ export const ChatHistoryPanel: React.FC = ({ functionDele setShowHistoryPanel(!showHistoryPanel); }; return ( -
    -
    +
    +
    Chat history
    @@ -31,6 +31,6 @@ export const ChatHistoryPanel: React.FC = ({ functionDele
    -
    +
    ); }; diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index aec524fd..435fae36 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -25,16 +25,36 @@ } .container { - width: 250px; + max-width: 400px; + width: 90%; + background-color: white; + border-radius: 8px; + box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.2); + overflow: hidden; + position: relative; } - -.card { +.cardHistory{ background-color: #f5f5f5; border: 1px solid #e3e3e3; border-radius: 6px; - box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + max-width: 400px; + width: 90%; + padding: 20px; +} + +.cardHistoryWrapper { + position: fixed; + top: 90px; + right: 0; + width: 40vh; + height: auto; + display: flex; + justify-content: flex-end; + align-items: flex-start; + z-index: 1000; + padding: 20px; } - .header { display: flex; justify-content: space-between; diff --git a/frontend/src/components/SettingsPanel/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css index 4d23d1a8..37a0e497 100644 --- a/frontend/src/components/SettingsPanel/SettingsModal.module.css +++ b/frontend/src/components/SettingsPanel/SettingsModal.module.css @@ -14,6 +14,25 @@ height: 35px; } +.overlay { + position: fixed; + top: 90px; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + justify-content: flex-end; + align-items: flex-start; + z-index: 1000; + padding: 20px; +} + +.item { + margin: 15px 0; + display: flex; + justify-content: space-between; + align-items: center; +} .button:hover { background-color: rgb(243, 243, 243); } @@ -147,12 +166,25 @@ } .closeButton2 { + background-color: transparent; border: none; - font-size: 25px; + font-size: 20px; color: #726c6c; - background-color: transparent; cursor: pointer; transform: rotate(45deg); - margin-top: -10px; - padding-left: 30px; } + +.closeButton2:hover { + color: #000; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} \ No newline at end of file diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index e25a4757..280ea923 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -187,70 +187,80 @@ export const SettingsPanel = () => { } return ( -
    - { - setIsDialogOpen(false); - }} - onConfirm={() => { - setIsLoadingSettings(true); - handleSubmit(); - }} - /> - - -
    -
    Configuration
    -
    -
    -
    - +
    + + { + setIsDialogOpen(false); + }} + onConfirm={() => { + setIsLoadingSettings(true); + handleSubmit(); + }} + /> + + +
    +
    Configuration
    +
    +
    +
    + +
    -
    - {loading ? ( -
    -

    Loading your settings

    - -
    - ) : ( -
    -
    -
    - Creativity Scale - {onRenderLabel(temperatureDialog, "Temperature")} -
    - handleSetTemperature(e)} - aria-labelledby="temperature-slider" + {loading ? ( +
    +

    + Loading your settings +

    + - setIsDialogOpen(true)} aria-label="Save settings"> - -   Save -
    -
    - )} - - + ) : ( +
    +
    +
    + Creativity Scale +
    + setTemperature(e.toString())} + aria-labelledby="temperature-slider" + /> + setIsDialogOpen(true)} + aria-label="Save settings" + > + +   Save + +
    +
    + )} + +
    ); }; diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 4b6e033f..a538ef4e 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -217,7 +217,7 @@ height: 95%; overflow-y: auto; margin-top: 10px; - margin-right: 10px; + margin-right: 0px; } .loadingData { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index d5e5d120..650fe1ac 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -368,7 +368,7 @@ const Chat = () => {
    {settingsPanel && }
    -
    +
    {!lastQuestionRef.current && dataConversation.length <= 0 ? (
    0 && !conversationIsLoading ? styles.chatMessageStream : styles.chatEmptyState}> From 5c8444875046f6aeec66fd6c72e19dec9efdc159 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 22 Nov 2024 08:37:52 -0400 Subject: [PATCH 192/820] Revert "FA 211 remove attachment and delete buttons (#157)" (#169) This reverts commit 2d9e8059b02dcafb752e181bec60274e3ed58c4b. --- .../src/components/.DS_Store => .DS_Store | Bin 6148 -> 8196 bytes .gitignore | 5 +---- frontend/.DS_Store | Bin 6148 -> 6148 bytes frontend/src/.DS_Store | Bin 6148 -> 0 bytes .../ClearChatButton.module.css | 5 ++--- .../ClearChatButton/ClearChatButton.tsx | 5 ++--- .../src/components/ClearChatButton/index.tsx | 3 +-- .../QuestionInput/QuestionInput.module.css | 10 ++++------ .../QuestionInput/QuestionInput.tsx | 5 ++--- 9 files changed, 12 insertions(+), 21 deletions(-) rename frontend/src/components/.DS_Store => .DS_Store (68%) delete mode 100644 frontend/src/.DS_Store diff --git a/frontend/src/components/.DS_Store b/.DS_Store similarity index 68% rename from frontend/src/components/.DS_Store rename to .DS_Store index e4cc05ed23d54e899245f3c3e4f448d34ead534b..e9799dce389487168d9a069e15a00cf95f7da74a 100644 GIT binary patch literal 8196 zcmeHMze^lJ6n>LCa|td@dFQr{^ji^vhv`wp|Cu`*6L#d^r(b2yP|^sVi7l(GS#abspXFLU9(=p;gw=Cb$`PF3`!Yfu_>n+Gfq(AU z^7pfx|JNboXNZ9d<%m4L{r)xD!PBq}{x0rJ;i;%RhlT=z>sUML>j+Q-EyGcHJsM=@ z_ki7n(b-)XQTBghj5KlJ4mnV9TF1@#e-3M%ya%{LH&3LB1AoZ@Rjn`9XEB-G)k`Kd vyS&ELz-5}jWiHwhbh#e__`ClPLwweNnwr?h%tef#$%_DMgLLA+KXu?Y!1D(5 delta 167 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{MGqg=C6q~50D9Q+A12IsnlA)L(6Nm#C5+@d_ zYk(wp85|k%7!nzBkR^d4sSG(O#mPBI`T04F8w*3&7qfG42r`3=1)9VSBwT^GZDZkg h=E?jrx||@R4gfL3WE&o79wvw@Kn`Qr9M3a@82}hj8|eT5 diff --git a/.gitignore b/.gitignore index 798ecb2c..af1eb595 100644 --- a/.gitignore +++ b/.gitignore @@ -154,7 +154,4 @@ static/ settings.json -flask_session/ - -# ignore .DS_Store -.DS_Store \ No newline at end of file +flask_session/ \ No newline at end of file diff --git a/frontend/.DS_Store b/frontend/.DS_Store index 5e47f261b75babe4f7214ab9ff193dfa47f501d3..f5c32f4651cce67bf98f6cc1ff4e8d7cc8b7dbbd 100644 GIT binary patch delta 32 ocmZoMXfc@J&&abeU^gQp&t@JbFUHN8%=1_#HVAKK=lIJH0HP`h-T(jq delta 38 ucmZoMXfc@J&&azmU^gQp?`9q*FGh}(;^ds9{QR8F-pngmHnVg5A!;D$AE3iMA025$j?4Z?1i zdoKYj768`7DG(8u1{D}o%@#w0j(Ewunm7dpT{N2y&6_nl6!qJ2e(`kC8px3fP=UDu zt5~*H|KH$O`u{nJD=I(*9!ddiw!3YICuMD2JkDxufxp8o=NE2 { {reiniciar_text}
    ); -};*/ +}; diff --git a/frontend/src/components/ClearChatButton/index.tsx b/frontend/src/components/ClearChatButton/index.tsx index 3bd2712c..c283e71c 100644 --- a/frontend/src/components/ClearChatButton/index.tsx +++ b/frontend/src/components/ClearChatButton/index.tsx @@ -1,2 +1 @@ -// removing clear chat -/*export * from "./ClearChatButton";*/ +export * from "./ClearChatButton"; diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index fb1f564f..af81ae9e 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -7,13 +7,12 @@ background: white; } -/* attachment container is not needed if we don't need an attachment button */ -/*.attachmentContainer { +.attachmentContainer { height: "100%"; justify-content: "center"; align-items: center; display: flex; -}*/ +} .questionInputTextArea { width: 100%; @@ -37,10 +36,9 @@ opacity: 0.4; } -/* commenting out the css associated with the attachment button instead of permanently deleting it*/ -/*.attachmentButton { +.attachmentButton { transform: rotate(-135deg) scaleX(-1) translate(-5px, 0px); cursor: pointer; font-size: 24px; color: #9f9c9c; -}*/ +} diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index 44a346e5..5f24b6f8 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -17,8 +17,7 @@ interface Props { import { useFilePicker } from "use-file-picker"; -// commenting out the attachment button instead of permanently deleting it -/*export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: string) => void }) => { +export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: string) => void }) => { const [files, setFiles] = useState([]); const [loadingFiles, setLoadingFiles] = useState(false); const [error, setError] = useState(""); @@ -167,7 +166,7 @@ import { useFilePicker } from "use-file-picker";
    ); -};*/ +}; export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => { const { user, organization } = useAppContext(); From 3f91be862421a5da3d084a210f3d0e02eed7cbe1 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 22 Nov 2024 14:17:42 -0400 Subject: [PATCH 193/820] HOTFIX onboarding check organization before subscription tiers --- frontend/src/router/ProtectedRoute.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index 868ef3f3..0df865a3 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -51,10 +51,10 @@ const ProtectedRoute: React.FC = ({ allowedRoles, allowedTi ) : !hasRequiredRole() ? ( - ) : !hasRequiredTier() ? ( - ) : !isValidSubscriptionForOrganization() ? ( + ) : !hasRequiredTier() ? ( + ) : ( )} From 9af0d797b2f26028360f2b1b1ff76d417c3afe11 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 22 Nov 2024 14:20:14 -0400 Subject: [PATCH 194/820] HOTFIX onboarding check organization before subscription tiers (#171) --- frontend/src/router/ProtectedRoute.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index 868ef3f3..0df865a3 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -51,10 +51,10 @@ const ProtectedRoute: React.FC = ({ allowedRoles, allowedTi ) : !hasRequiredRole() ? ( - ) : !hasRequiredTier() ? ( - ) : !isValidSubscriptionForOrganization() ? ( + ) : !hasRequiredTier() ? ( + ) : ( )} From 81030709617d238fe67848d9f371430ce6ac0f13 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:37:07 -0400 Subject: [PATCH 195/820] FA-221 Only show elements in the navbar in the chat (#166) * Hide Navbar elements when the user is not in the Chat * Change to use conditionals to show the elements and delete a few resources that were unused --- frontend/src/components/Navbar/NavBar.tsx | 31 ++++++++++--------- .../src/components/Navbar/Navbar.module.css | 7 ++++- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index ff4053a4..28e6b11a 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import styles from "./Navbar.module.css"; import { IconMenu2, IconMessageCircle, IconHistory, IconSettings, IconBell, IconUser, IconMail, IconListCheck } from "@tabler/icons-react"; import { useAppContext } from "../../providers/AppProviders"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; interface NavbarProps { setIsCollapsed: React.Dispatch>; @@ -20,7 +20,6 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { settingsPanel, setSettingsPanel, user, - organization, subscriptionTiers, isFinancialAssistantActive, setIsFinancialAssistantActive @@ -30,6 +29,7 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { const userName = user?.name || ""; const email = user?.email || " "; const subscriptiontype = subscriptionTiers || " "; + const location = useLocation().pathname; const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -76,7 +76,7 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { }; return ( -
    - {/* End Sidebar scroll */} ); }; From 74cca129da0096bbfc0fcadbeca8e1046f0c3b0d Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Tue, 26 Nov 2024 13:52:34 -0400 Subject: [PATCH 201/820] Minor Fix --- frontend/src/components/Sidebar/Sidebar.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 691919e9..3c8fa447 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -45,7 +45,6 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { user // ... other context values if needed } = useAppContext(); - /** * Determines if the current user has access to a sidebar item or link based on roles and subscription tiers. * @param itemRoles - Array of roles that have access to the item/link. @@ -55,7 +54,6 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { const hasAccess = useCallback( (itemRoles: Role[], itemTiers: SubscriptionTier[]): boolean => { if (!user || !user.role) return false; - const roleMatch = itemRoles.includes(user.role); const tierMatch = itemTiers.some(tier => userSubscriptionTiers.includes(tier)); @@ -136,6 +134,9 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { } ] }, + { + divider: true + }, { section: "Premium Features", items: [ From fb1fd9fec08fbca18dc9ade8c284d9d0d51a251f Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:23:42 -0400 Subject: [PATCH 202/820] FA-233 Chat Icon change (#176) --- frontend/src/components/Sidebar/Sidebar.tsx | 4 ++-- .../src/components/StartNewChatButton/StartNewChatButton.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 5a79ab65..cb5a84da 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { IconX, - IconMessagePlus, + IconMessage, IconBell, IconRosetteDiscountCheck, IconStar, @@ -71,7 +71,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { items: [ { title: "AI Chat", - icon: , + icon: , to: "/", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin", "user"] diff --git a/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx b/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx index c1eddbac..0c8c8bd6 100644 --- a/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx +++ b/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx @@ -1,4 +1,4 @@ -import { AddRegular } from "@fluentui/react-icons"; +import { IconMessagePlus } from "@tabler/icons-react"; import styles from "./StartNewChatButton.module.css"; const StartNewChatButton = ({ isEnabled, onClick }: { isEnabled: boolean; onClick: () => void }) => { @@ -10,7 +10,7 @@ const StartNewChatButton = ({ isEnabled, onClick }: { isEnabled: boolean; onClic type="button" disabled={!isEnabled} > - + ); }; From b8fd616600bcc2fde120cddb5e336b363f1c6781 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Tue, 26 Nov 2024 14:31:50 -0400 Subject: [PATCH 203/820] FA-193 Fix issues with the sidemenu (#178) * Fix issues with the sidemenu * Minor Fix --- frontend/src/components/Sidebar/Sidebar.tsx | 33 +++++++++++---------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index cb5a84da..c0d8804e 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -45,7 +45,6 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { user // ... other context values if needed } = useAppContext(); - /** * Determines if the current user has access to a sidebar item or link based on roles and subscription tiers. * @param itemRoles - Array of roles that have access to the item/link. @@ -55,7 +54,6 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { const hasAccess = useCallback( (itemRoles: Role[], itemTiers: SubscriptionTier[]): boolean => { if (!user || !user.role) return false; - const roleMatch = itemRoles.includes(user.role); const tierMatch = itemTiers.some(tier => userSubscriptionTiers.includes(tier)); @@ -136,6 +134,9 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { } ] }, + { + divider: true + }, { section: "Premium Features", items: [ @@ -200,40 +201,42 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { } ]; - // Filtered sidebar sections based on access const accessibleSidebarSections = useMemo(() => { + let previousSectionHasItems = false; + return sidebarSections - .map(section => { + .map((section, index) => { if (section.divider) { - return section; // Always include dividers + return previousSectionHasItems ? section : null; } - + if (section.items) { - // Filter items based on access const accessibleItems = section.items .map(item => { if (item.links) { - // Filter links based on access - const accessibleLinks = item.links.filter(link => hasAccess(link.roles, link.tiers)); - + const accessibleLinks = item.links.filter(link => + hasAccess(link.roles, link.tiers) + ); + if (accessibleLinks.length > 0) { return { ...item, links: accessibleLinks }; } else { - return null; // Exclude item if no accessible links + return null; } } else { - // No links, just check access on the item itself return hasAccess(item.roles, item.tiers) ? item : null; } }) .filter(item => item !== null) as SidebarItemType[]; - + + previousSectionHasItems = accessibleItems.length > 0; + if (accessibleItems.length > 0) { return { ...section, items: accessibleItems }; } } - // Exclude sections with no accessible items + previousSectionHasItems = false; return null; }) .filter(section => section !== null) as SidebarSection[]; @@ -300,9 +303,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { })} - {/* End Sidebar navigation */}
    - {/* End Sidebar scroll */} ); }; From 555ad4a6945ae2cfd2e16297fadcfbaccf2ab983 Mon Sep 17 00:00:00 2001 From: egdagger Date: Tue, 26 Nov 2024 15:03:24 -0400 Subject: [PATCH 204/820] FA-112 Create the Disclaimer text --- frontend/src/pages/chat/Chat.module.css | 6 ++++++ frontend/src/pages/chat/Chat.tsx | 3 +++ 2 files changed, 9 insertions(+) diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 5d3bd8ca..a4fc2974 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -207,6 +207,12 @@ margin-top: 15px; } +.chatDisclaimer{ + margin-top: 5px; + color: #8d8d8d; + font-size: 0.8em; +} + .loadingLogo { font-size: 28px; } diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 186514b8..d476dafe 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -483,6 +483,9 @@ const Chat = () => { extraButtonNewChat={} />
    +
    +

    This app is in beta. Responses may not be fully accurate.

    +
    {(answers.length > 0 && activeAnalysisPanelTab && answers[selectedAnswer] && ( Date: Tue, 26 Nov 2024 15:18:07 -0400 Subject: [PATCH 205/820] Minor Fix --- .../components/HistoryPannel/ChatHistoryPannel.module.css | 2 +- .../src/components/SettingsPanel/SettingsModal.module.css | 6 +++--- frontend/src/components/SettingsPanel/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 435fae36..199b5c7d 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -47,7 +47,7 @@ position: fixed; top: 90px; right: 0; - width: 40vh; + width: auto; height: auto; display: flex; justify-content: flex-end; diff --git a/frontend/src/components/SettingsPanel/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css index 37a0e497..55f57953 100644 --- a/frontend/src/components/SettingsPanel/SettingsModal.module.css +++ b/frontend/src/components/SettingsPanel/SettingsModal.module.css @@ -17,9 +17,9 @@ .overlay { position: fixed; top: 90px; - left: 0; - width: 100vw; - height: 100vh; + right: 0; + width: auto; + height: auto; display: flex; justify-content: flex-end; align-items: flex-start; diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 280ea923..8d28b620 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -219,7 +219,7 @@ export const SettingsPanel = () => {
    {loading ? (
    -

    +

    Loading your settings

    Date: Tue, 26 Nov 2024 16:35:39 -0400 Subject: [PATCH 206/820] FA-192 Minor fixes in the Chat History modal behavior and small disclaimer in the feedback rating (#179) --- .../src/components/FeedbackRating/FeedbackRating.module.css | 1 + frontend/src/components/FeedbackRating/FeedbackRating.tsx | 1 + frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css index e13d99aa..403b1914 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.module.css +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -200,6 +200,7 @@ width: 100%; font-weight: 500; margin-top: 5px; + margin-bottom: 10px; } .saveButton:hover { diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index b36ca713..1b71156a 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -143,6 +143,7 @@ export const FeedbackRating = () => {   Send + All fields must be filled
    {errorMessage !== null &&

    {errorMessage}

    }
    diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 72cccfef..e1d5e816 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -32,7 +32,8 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete setRefreshFetchHistory, chatSelected, setChatSelected, - setNewChatDeleted + setNewChatDeleted, + setShowHistoryPanel } = useAppContext(); const handleMouseEnter = (index: string) => { @@ -86,6 +87,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete setChatSelected(chatConversationId); setChatId(chatConversationId); setConversationIsLoading(true); + setShowHistoryPanel(false); try { const data = await getChatFromHistoryPannelById(chatConversationId, user.id); From 9f625eff68a53ed2489cb74773ddc8241469bcdf Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:36:21 -0400 Subject: [PATCH 207/820] FA-234 Commeting the button (#180) --- frontend/src/components/QuestionInput/QuestionInput.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx index ce76d22d..1c6cda3a 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ b/frontend/src/components/QuestionInput/QuestionInput.tsx @@ -265,6 +265,7 @@ export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend, extr
    {extraButtonNewChat}
    + {/*
    + */}
    Date: Tue, 26 Nov 2024 16:37:16 -0400 Subject: [PATCH 208/820] FA-112 Create the Disclaimer text (#181) --- frontend/src/pages/chat/Chat.module.css | 6 ++++++ frontend/src/pages/chat/Chat.tsx | 3 +++ 2 files changed, 9 insertions(+) diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 838f9c99..eb7bd7d9 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -207,6 +207,12 @@ margin-top: 15px; } +.chatDisclaimer{ + margin-top: 5px; + color: #8d8d8d; + font-size: 0.8em; +} + .loadingLogo { font-size: 28px; } diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 186514b8..d476dafe 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -483,6 +483,9 @@ const Chat = () => { extraButtonNewChat={} />
    +
    +

    This app is in beta. Responses may not be fully accurate.

    +
    {(answers.length > 0 && activeAnalysisPanelTab && answers[selectedAnswer] && ( Date: Tue, 26 Nov 2024 16:38:28 -0400 Subject: [PATCH 209/820] FA-242 chat history overlaps chat messages (#182) * Fix issues with the sidemenu * Minor Fix * Minor Fix --- .../components/HistoryPannel/ChatHistoryPannel.module.css | 2 +- .../src/components/SettingsPanel/SettingsModal.module.css | 6 +++--- frontend/src/components/SettingsPanel/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 435fae36..199b5c7d 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -47,7 +47,7 @@ position: fixed; top: 90px; right: 0; - width: 40vh; + width: auto; height: auto; display: flex; justify-content: flex-end; diff --git a/frontend/src/components/SettingsPanel/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css index 37a0e497..55f57953 100644 --- a/frontend/src/components/SettingsPanel/SettingsModal.module.css +++ b/frontend/src/components/SettingsPanel/SettingsModal.module.css @@ -17,9 +17,9 @@ .overlay { position: fixed; top: 90px; - left: 0; - width: 100vw; - height: 100vh; + right: 0; + width: auto; + height: auto; display: flex; justify-content: flex-end; align-items: flex-start; diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 280ea923..8d28b620 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -219,7 +219,7 @@ export const SettingsPanel = () => {
    {loading ? (
    -

    +

    Loading your settings

    Date: Wed, 27 Nov 2024 10:32:00 -0400 Subject: [PATCH 210/820] FA-128 handle redirect url parameter after login --- backend/app.py | 61 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9e9cce47..d652ebbd 100644 --- a/backend/app.py +++ b/backend/app.py @@ -48,6 +48,10 @@ ) import stripe.error +from urllib.parse import urlencode, parse_qs, urlparse, urlunparse +import base64 +import json + load_dotenv() @@ -105,6 +109,9 @@ def get_secret(secretName): app.config.from_object(app_config) CORS(app) +app.secret_key = os.getenv( + "FLASK_SECRET_KEY", "your-secret-key-here" +) # Make sure to set this in production auth = Auth( app, @@ -278,12 +285,53 @@ def check_user_authorization( @auth.login_required def index(*, context): """ - Endpoint to get the current user's data from Microsoft Graph API + Main entry point - if there's a stored return URL, redirect to it """ - logger.debug(f"User context: {context}") + # Check if we have a return URL in session + if "original_url" in session: + return_url = session.pop("original_url") + return redirect(return_url) + return send_from_directory("static", "index.html") +@app.route("/auth-response") +def auth_response(): + """ + Handle the authentication response from Azure B2C + Preserves the original URL from the request + """ + try: + # Store the original URL from query parameters if present + if request.args.get("original_url"): + session["original_url"] = base64.b64decode( + request.args.get("original_url") + ).decode() + + # Complete the login process + result = auth.complete_log_in(request.args) + + # Redirect to index which will handle the return URL + return redirect(url_for("index")) + + except Exception as e: + app.logger.error(f"Authentication error: {str(e)}") + return redirect(url_for("index")) + + +# Add this middleware to capture the original URL before Azure B2C redirect +@app.before_request +def before_request(): + """ + Middleware to capture the original URL before Azure B2C redirect + """ + # Only store the URL if it's not an auth-related endpoint + if not request.path.startswith("/auth") and not request.path.startswith("/static"): + if request.query_string: + # Store the full URL in session + session["original_url"] = request.url + + # route for other static files @@ -293,15 +341,6 @@ def static_files(path): return send_from_directory("static", path) -@app.route("/auth-response") -def auth_response(): - try: - return auth.complete_log_in(request.args) - except Exception as e: - logger.error(f"Authentication error: {str(e)}") - return redirect(url_for("index")) - - @app.route("/api/auth/config") def get_auth_config(): """Return Azure AD B2C configuration for frontend""" From ce4cdbfd17b65c971fca3050dcc8375be081f33c Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:37:05 -0400 Subject: [PATCH 211/820] FA-205 set up deployment prod (#183) --- .github/workflows/azure-prod.yml | 58 ++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/azure-prod.yml diff --git a/.github/workflows/azure-prod.yml b/.github/workflows/azure-prod.yml new file mode 100644 index 00000000..f49c9bfe --- /dev/null +++ b/.github/workflows/azure-prod.yml @@ -0,0 +1,58 @@ +# .github/workflows/deploy.yml +name: Build and Deploy to Azure + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Install zip + run: sudo apt-get update && sudo apt-get install -y zip + + - name: Azure Login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Build Frontend + working-directory: frontend + run: | + npm install + npm run build + + - name: Package Backend + working-directory: backend + run: | + rm -rf backend_env + zip -r ../deploy.zip * + + - name: Deploy to Azure + uses: azure/cli@v2 + with: + azcliversion: latest + inlineScript: | + set -e + if [ ! -f deploy.zip ]; then + echo "deploy.zip not found" + exit 1 + fi + az webapp deploy \ + --subscription ${{ secrets.AZURE_PROD_SUBSCRIPTION_ID }} \ + --resource-group ${{ secrets.AZURE_PROD_RESOURCE_GROUP }} \ + --name ${{ secrets.AZURE_PROD_WEBAPP_NAME }} \ + --src-path deploy.zip \ + --type zip \ + --async true From 43ed9d406a590afba4c197bcf48a0f750f81adf9 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Wed, 27 Nov 2024 14:19:51 -0400 Subject: [PATCH 212/820] HOTFIX debug environments vars at PROD deploy (#186) --- .github/workflows/azure-dev.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 42516df8..db8e7284 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -38,6 +38,11 @@ jobs: run: | rm -rf backend_env zip -r ../deploy.zip * + - name: Debug Secrets + run: | + echo "Subscription ID: ${{ secrets.AZURE_PROD_SUBSCRIPTION_ID }}" + echo "Resource Group: ${{ secrets.AZURE_PROD_RESOURCE_GROUP }}" + echo "Web App Name: ${{ secrets.AZURE_PROD_WEBAPP_NAME }}" - name: Deploy to Azure uses: azure/cli@v2 From 3bb4c60ab0998a73b72bde567029ddb0e4078e7b Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Wed, 27 Nov 2024 14:26:36 -0400 Subject: [PATCH 213/820] Hot-fix inspect environments vars at deploy (#187) * HOTFIX debug envirments vars at PROD deploy * Fix file --- .github/workflows/azure-prod.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/azure-prod.yml b/.github/workflows/azure-prod.yml index f49c9bfe..78967603 100644 --- a/.github/workflows/azure-prod.yml +++ b/.github/workflows/azure-prod.yml @@ -38,6 +38,11 @@ jobs: run: | rm -rf backend_env zip -r ../deploy.zip * + - name: Debug Secrets + run: | + echo "Subscription ID: ${{ secrets.AZURE_PROD_SUBSCRIPTION_ID }}" + echo "Resource Group: ${{ secrets.AZURE_PROD_RESOURCE_GROUP }}" + echo "Web App Name: ${{ secrets.AZURE_PROD_WEBAPP_NAME }}" - name: Deploy to Azure uses: azure/cli@v2 From 12cf10ec1e7337ddd0ea7cfe229e8786f4111aff Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:37:05 -0400 Subject: [PATCH 214/820] FA-205 set up deployment prod (#183) --- .github/workflows/azure-prod.yml | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/azure-prod.yml diff --git a/.github/workflows/azure-prod.yml b/.github/workflows/azure-prod.yml new file mode 100644 index 00000000..3e48ee0b --- /dev/null +++ b/.github/workflows/azure-prod.yml @@ -0,0 +1,64 @@ +# .github/workflows/deploy.yml +name: Build and Deploy to Azure + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Install zip + run: sudo apt-get update && sudo apt-get install -y zip + + - name: Azure Login + uses: azure/login@v2 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Build Frontend + working-directory: frontend + run: | + npm install + npm run build + + - name: Package Backend + working-directory: backend + run: | + rm -rf backend_env + zip -r ../deploy.zip * + + - name: Debug Secrets + run: | + echo "Subscription ID: ${{ vars.AZURE_PROD_SUBSCRIPTION_ID }}" + echo "Resource Group: ${{ vars.AZURE_PROD_RESOURCE_GROUP }}" + echo "Web App Name: ${{ vars.AZURE_PROD_WEBAPP_NAME }}" + + - name: Deploy to Azure + uses: azure/cli@v2 + with: + azcliversion: latest + inlineScript: | + set -e + if [ ! -f deploy.zip ]; then + echo "deploy.zip not found" + exit 1 + fi + az webapp deploy \ + --subscription ${{ vars.AZURE_PROD_SUBSCRIPTION_ID }} \ + --resource-group ${{ vars.AZURE_PROD_RESOURCE_GROUP }} \ + --name ${{ vars.AZURE_PROD_WEBAPP_NAME }} \ + --src-path deploy.zip \ + --type zip \ + --async true From 9816e4dfe466eb4802b53ec27b221d020d84fb5f Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Wed, 27 Nov 2024 15:32:26 -0400 Subject: [PATCH 215/820] HOTFIX-set-PROD-credentials --- .github/workflows/azure-prod.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/azure-prod.yml b/.github/workflows/azure-prod.yml index 3e48ee0b..76796aab 100644 --- a/.github/workflows/azure-prod.yml +++ b/.github/workflows/azure-prod.yml @@ -20,7 +20,7 @@ jobs: - name: Azure Login uses: azure/login@v2 with: - creds: ${{ secrets.AZURE_CREDENTIALS }} + creds: ${{ secrets.AZURE_PROD_CREDENTIALS }} - name: Set up Node.js uses: actions/setup-node@v3 @@ -62,3 +62,8 @@ jobs: --src-path deploy.zip \ --type zip \ --async true + + + + + az ad sp create-for-rbac --name "webgpt0-2wc73ir547bl4" --role contributor --scopes /subscriptions/e261fb0a-3d87-49c1-8d3c-32b2bc93b6ff/resourceGroups/rg-SalesFactoryEmpolyees/providers/Microsoft.Web/sites/webgpt0-2wc73ir547bl4 --json-auth From 0d5716dd4350c61945d8b554e9d9ee967b15c50c Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:56:35 -0400 Subject: [PATCH 216/820] FA-251 Capture Document Name and Agent Type from URL parameters (#192) * FA-251 Capture Document Name and Agent Type from URL parameters * FA-251 Removing the remaining console.log * FA-251 Changing the way it takes the path and evading the use of useEffect * FA-251 Changing variable names to evade future issues and adding the conditional for the financial assistant --- frontend/src/providers/AppProviders.tsx | 32 +++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 75582cbb..c9d1c6fc 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -80,6 +80,10 @@ interface AppContextType { setSubscriptionTiers: Dispatch>; // Setter for subscriptionTiers isFinancialAssistantActive: boolean; setIsFinancialAssistantActive: Dispatch>; + documentName: string; + setDocumentName: Dispatch>; + agentType: string; + setAgentType: Dispatch>; } // Create the context with a default value @@ -108,6 +112,20 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => // New state variables for subscription tiers const [subscriptionTiers, setSubscriptionTiers] = useState([]); + // Setting variables for the Financial Agent URL + const searchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(searchParams.entries()); + + const agentParam = params["agent"]; + const documentParam = params["document"]; + + const [documentName, setDocumentName] = useState(documentParam || "defaultDocument"); + const [agentType, setAgentType] = useState(agentParam || "defaultAgent"); + + if(agentType !== agentParam && agentParam !== null){ + setAgentType(agentParam) + } + // Handle keyboard shortcuts (unchanged) const handleKeyDown = useCallback( (event: KeyboardEvent) => { @@ -255,6 +273,10 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (savedState !== null) { setIsFinancialAssistantActive(JSON.parse(savedState)); } + if(agentType == "financial"){ + setIsFinancialAssistantActive(true) + } + } catch (error) { console.error("Initialization failed:", error); toast.error("Failed to initialize user authentication."); @@ -300,7 +322,11 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => subscriptionTiers, // New state variable setSubscriptionTiers, // Setter for subscriptionTiers isFinancialAssistantActive, - setIsFinancialAssistantActive + setIsFinancialAssistantActive, + documentName, + setDocumentName, + agentType, + setAgentType }), [ showHistoryPanel, @@ -319,7 +345,9 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => isAuthenticated, isLoading, subscriptionTiers, - isFinancialAssistantActive + isFinancialAssistantActive, + documentName, + agentType ] ); From 0614ce9976c060608a9d1cb13416bbc5e9996cce Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Fri, 29 Nov 2024 10:12:36 -0400 Subject: [PATCH 217/820] FA-252 Send Chat Requests to Financial Agent Endpoint (#193) * Send Chat Requests to Financial Agent Endpoint * Switch between the orc and the financial-orc with the toggle * FA-252 remove unnecesary state and parameter agent --------- Co-authored-by: Manuel Castro --- backend/app.py | 21 +++++++++++++++---- frontend/src/api/api.ts | 1 + frontend/src/api/models.ts | 1 + .../QuestionInput/QuestionInput.tsx | 4 ++-- frontend/src/pages/chat/Chat.tsx | 20 +++++++++++++++--- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9e9cce47..9ae517ec 100644 --- a/backend/app.py +++ b/backend/app.py @@ -61,7 +61,7 @@ SUBSCRIPTION_ENDPOINT = ORCHESTRATOR_URI + "/subscriptions" INVITATIONS_ENDPOINT = ORCHESTRATOR_URI + "/invitations" STORAGE_ACCOUNT = os.getenv("STORAGE_ACCOUNT") - +FINANCIAL_ASSISTANT_ENDPOINT = ORCHESTRATOR_URI + "/financial-orc" PRODUCT_ID_DEFAULT = os.getenv("STRIPE_PRODUCT_ID") # email @@ -492,6 +492,8 @@ def chatgpt(): conversation_id = request.json["conversation_id"] question = request.json["query"] file_blob_url = request.json["url"] + agent = request.json.get("agent") + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") logging.info("[webbackend] conversation_id: " + conversation_id) @@ -499,7 +501,8 @@ def chatgpt(): logging.info(f"[webbackend] file_blob_url: {file_blob_url}") logging.info(f"[webbackend] User principal: {client_principal_id}") logging.info(f"[webbackend] User name: {client_principal_name}") - + logging.info(f"[webappend] Agent: {agent}") + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. @@ -519,7 +522,11 @@ def chatgpt(): ) try: - url = ORCHESTRATOR_ENDPOINT + if agent =="financial": + orchestrator_url = FINANCIAL_ASSISTANT_ENDPOINT + else: + orchestrator_url = ORCHESTRATOR_ENDPOINT + payload = json.dumps( { "conversation_id": conversation_id, @@ -527,11 +534,17 @@ def chatgpt(): "url": file_blob_url, "client_principal_id": client_principal_id, "client_principal_name": client_principal_name, + "agent":agent, } ) headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", url, headers=headers, data=payload) + response = requests.request("GET", orchestrator_url, headers=headers, data=payload) logging.info(f"[webbackend] response: {response.text[:500]}...") + + if response.status_code != 200: + logging.error(f"[webbackend] Error from orchestrator: {response.text}") + return jsonify({"error": "Error contacting orchestrator"}), 500 + return response.text except Exception as e: logging.exception("[webbackend] exception in /chatgpt") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 1f30dff4..0266df93 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -135,6 +135,7 @@ export async function chatApiGpt(options: ChatRequestGpt, user: any): Promise { - const { user, organization } = useAppContext(); - + const { organization } = useAppContext(); const [question, setQuestion] = useState(""); + const [fileBlobUrl, setFileBlobUrl] = useState(null); const sendQuestion = () => { diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index d476dafe..bd9fc96d 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -61,7 +61,8 @@ const Chat = () => { setChatIsCleaned, chatIsCleaned, settingsPanel, - user + user, + isFinancialAssistantActive } = useAppContext(); const lastQuestionRef = useRef(""); @@ -82,6 +83,7 @@ const Chat = () => { const triggered = useRef(false); const makeApiRequestGpt = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { + let agent = null; lastQuestionRef.current = question; lastFileBlobUrl.current = fileBlobUrl; @@ -90,6 +92,15 @@ const Chat = () => { setActiveCitation(undefined); setActiveAnalysisPanelTab(undefined); + if (isFinancialAssistantActive == true) { + agent = "financial"; + } else { + agent = "consumer"; + } + + console.log("AGENT=", agent); + console.log("isFinancialAssistant=", isFinancialAssistantActive); + try { let history: ChatTurn[] = []; if (dataConversation.length > 0) { @@ -104,6 +115,7 @@ const Chat = () => { conversation_id: chatId !== null ? chatId : userId, query: question, file_blob_url: fileBlobUrl || "", + agent: agent || "", overrides: { promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, @@ -479,13 +491,15 @@ const Chat = () => { clearOnSend placeholder={placeholderText} disabled={isLoading} - onSend={(question, fileBlobUrl) => makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null)} + onSend={(question, fileBlobUrl) => { + makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + }} extraButtonNewChat={} />

    This app is in beta. Responses may not be fully accurate.

    -
    +
    {(answers.length > 0 && activeAnalysisPanelTab && answers[selectedAnswer] && ( Date: Fri, 29 Nov 2024 10:37:42 -0400 Subject: [PATCH 218/820] HOTFIX include document into financial agents request (#194) --- frontend/src/api/api.ts | 67 +++++++++++++++----------------- frontend/src/api/models.ts | 22 +++++------ frontend/src/pages/chat/Chat.tsx | 8 ++-- 3 files changed, 47 insertions(+), 50 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 0266df93..610dc3d8 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -135,7 +135,7 @@ export async function chatApiGpt(options: ChatRequestGpt, user: any): Promise { +export async function getFinancialAssistant({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; - + try { const response = await fetch(`/api/subscription/${subscriptionId}/financialAssistant`, { method: "GET", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId, + "X-MS-CLIENT-PRINCIPAL-ID": userId } }); - + if (!response.ok) { const error = new Error(`Failed to check financial assistant status: ${response.status}`); - (error as any).status = response.status; // Añade el código de estado al error + (error as any).status = response.status; // Añade el código de estado al error throw error; } const parsedResponse = await response.json(); return parsedResponse.data; - } catch (error) { console.error("Error verifying the Financial Assistant: ", error instanceof Error ? error.message : error); throw error; } } - -export async function upgradeSubscription({ user, subscriptionId }: { user?: User ; subscriptionId: string }): Promise { +export async function upgradeSubscription({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; const userOrganizationId = user?.organizationId ?? "00000000-0000-0000-0000-000000000000"; try { - const response = await fetch(`/api/subscription/${subscriptionId}/financialAssistant`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId, - }, - body: JSON.stringify({ - organizationId: userOrganizationId, - activateFinancialAssistant: true, - }), - }); - - if (!response.ok) { - throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); - } - - const parsedResponse: SubscriptionResponse = await response.json(); - const { message, subscription } = parsedResponse.data; - - console.log("Subscription upgraded successfully:", message); - return subscription; + const response = await fetch(`/api/subscription/${subscriptionId}/financialAssistant`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": userId + }, + body: JSON.stringify({ + organizationId: userOrganizationId, + activateFinancialAssistant: true + }) + }); + + if (!response.ok) { + throw new Error(`Subscription upgrade failed: ${response.status} ${response.statusText}`); + } + + const parsedResponse: SubscriptionResponse = await response.json(); + const { message, subscription } = parsedResponse.data; + + console.log("Subscription upgraded successfully:", message); + return subscription; } catch (error) { console.error("Error upgrading subscription:", error instanceof Error ? error.message : error); throw error; } } -export async function removeFinancialAssistant({ user, subscriptionId }: { user?: User; subscriptionId: string; }): Promise { +export async function removeFinancialAssistant({ user, subscriptionId }: { user?: User; subscriptionId: string }): Promise { const userId = user?.id ?? "00000000-0000-0000-0000-000000000000"; try { @@ -402,8 +400,8 @@ export async function removeFinancialAssistant({ user, subscriptionId }: { user? method: "DELETE", headers: { "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": userId, - }, + "X-MS-CLIENT-PRINCIPAL-ID": userId + } }); if (!response.ok) { @@ -421,7 +419,6 @@ export async function removeFinancialAssistant({ user, subscriptionId }: { user? } } - export async function createInvitation({ organizationId, invitedUserEmail, userId, role }: any): Promise { try { const response = await fetch("/api/createInvitation", { diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 8a39a66f..487d0ba5 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -14,15 +14,15 @@ export const enum Approaches { } export type ConversationHistoryItem = { - id: string, - start_date: string, - content: string, + id: string; + start_date: string; + content: string; }; export type ConversationChatItem = { - role: string, - content: string -} + role: string; + content: string; +}; export type AskRequestOverrides = { semanticRanker?: boolean; @@ -53,9 +53,9 @@ export type TransactionData = { cuenta_origen: string; monto: string; telefono_destino: string; -} +}; -export type AskResponseGpt= { +export type AskResponseGpt = { conversation_id: string; answer: string; current_state: string; @@ -86,7 +86,7 @@ export type ChatRequestGpt = { query: string; file_blob_url: string; overrides?: AskRequestOverrides; - agent: string; + documentName: string; }; export type GetSettingsProps = { @@ -94,7 +94,7 @@ export type GetSettingsProps = { id: string; name: string; } | null; -} +}; export type PostSettingsProps = { user: { @@ -102,4 +102,4 @@ export type PostSettingsProps = { name: string; } | null; temperature: number; -} +}; diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index bd9fc96d..f8d67c26 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -1,10 +1,9 @@ import { useRef, useState, useEffect, useContext } from "react"; import { Checkbox, Panel, DefaultButton, TextField, SpinButton, Spinner } from "@fluentui/react"; -import { AddRegular, BroomRegular, SparkleFilled, TabDesktopMultipleBottomRegular } from "@fluentui/react-icons"; import styles from "./Chat.module.css"; -import { chatApiGpt, Approaches, AskResponse, ChatRequest, ChatRequestGpt, ChatTurn, getUserInfo, checkUser, getOrganizationSubscription } from "../../api"; +import { chatApiGpt, Approaches, AskResponse, ChatRequestGpt, ChatTurn } from "../../api"; import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; import { QuestionInput } from "../../components/QuestionInput"; import { ExampleList } from "../../components/Example"; @@ -62,7 +61,8 @@ const Chat = () => { chatIsCleaned, settingsPanel, user, - isFinancialAssistantActive + isFinancialAssistantActive, + documentName } = useAppContext(); const lastQuestionRef = useRef(""); @@ -115,7 +115,7 @@ const Chat = () => { conversation_id: chatId !== null ? chatId : userId, query: question, file_blob_url: fileBlobUrl || "", - agent: agent || "", + documentName, overrides: { promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, From b839ede61de1c3c59b0878f26649ab7f298b1669 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 29 Nov 2024 11:10:05 -0400 Subject: [PATCH 219/820] HOTFIX set keyvault for financial agent and send agent into chat payload (#195) --- backend/app.py | 23 +++++++++++++++-------- frontend/src/api/api.ts | 1 + frontend/src/api/models.ts | 1 + frontend/src/pages/chat/Chat.tsx | 1 + 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9ae517ec..39b35f38 100644 --- a/backend/app.py +++ b/backend/app.py @@ -492,8 +492,9 @@ def chatgpt(): conversation_id = request.json["conversation_id"] question = request.json["query"] file_blob_url = request.json["url"] - agent = request.json.get("agent") - + agent = request.json["agent"] + documentName = request.json["documentName"] + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") logging.info("[webbackend] conversation_id: " + conversation_id) @@ -502,11 +503,15 @@ def chatgpt(): logging.info(f"[webbackend] User principal: {client_principal_id}") logging.info(f"[webbackend] User name: {client_principal_name}") logging.info(f"[webappend] Agent: {agent}") - + try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. - keySecretName = "orchestrator-host--functionKey" + if agent == "financial": + keySecretName = "orchestrator-host--financial" + else: + keySecretName = "orchestrator-host--functionKey" + functionKey = get_secret(keySecretName) except Exception as e: logging.exception( @@ -522,7 +527,7 @@ def chatgpt(): ) try: - if agent =="financial": + if agent == "financial": orchestrator_url = FINANCIAL_ASSISTANT_ENDPOINT else: orchestrator_url = ORCHESTRATOR_ENDPOINT @@ -534,17 +539,19 @@ def chatgpt(): "url": file_blob_url, "client_principal_id": client_principal_id, "client_principal_name": client_principal_name, - "agent":agent, + "documentName": documentName, } ) headers = {"Content-Type": "application/json", "x-functions-key": functionKey} - response = requests.request("GET", orchestrator_url, headers=headers, data=payload) + response = requests.request( + "GET", orchestrator_url, headers=headers, data=payload + ) logging.info(f"[webbackend] response: {response.text[:500]}...") if response.status_code != 200: logging.error(f"[webbackend] Error from orchestrator: {response.text}") return jsonify({"error": "Error contacting orchestrator"}), 500 - + return response.text except Exception as e: logging.exception("[webbackend] exception in /chatgpt") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 610dc3d8..8dd1b794 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -135,6 +135,7 @@ export async function chatApiGpt(options: ChatRequestGpt, user: any): Promise { query: question, file_blob_url: fileBlobUrl || "", documentName, + agent, overrides: { promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, From cddc7da5315e1e9165702e31a464900871ff99a3 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:33:08 -0400 Subject: [PATCH 220/820] FA-244 Creating the CSS modules for each page and putting elements in containers (#185) --- frontend/src/pages/reports/DistributionLists.module.css | 6 ++++++ frontend/src/pages/reports/DistributionLists.tsx | 3 ++- frontend/src/pages/reports/ReportManagement.module.css | 6 ++++++ frontend/src/pages/reports/ReportManagement.tsx | 3 ++- frontend/src/pages/resources/UploadResources.module.css | 6 ++++++ frontend/src/pages/resources/UploadResources.tsx | 3 ++- frontend/src/pages/studies/RequestStudies.module.css | 6 ++++++ frontend/src/pages/studies/RequestStudies.tsx | 3 ++- 8 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/reports/DistributionLists.module.css create mode 100644 frontend/src/pages/reports/ReportManagement.module.css create mode 100644 frontend/src/pages/resources/UploadResources.module.css create mode 100644 frontend/src/pages/studies/RequestStudies.module.css diff --git a/frontend/src/pages/reports/DistributionLists.module.css b/frontend/src/pages/reports/DistributionLists.module.css new file mode 100644 index 00000000..97f3e4ec --- /dev/null +++ b/frontend/src/pages/reports/DistributionLists.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} \ No newline at end of file diff --git a/frontend/src/pages/reports/DistributionLists.tsx b/frontend/src/pages/reports/DistributionLists.tsx index 37742baf..ceab37e9 100644 --- a/frontend/src/pages/reports/DistributionLists.tsx +++ b/frontend/src/pages/reports/DistributionLists.tsx @@ -1,8 +1,9 @@ import React from "react"; +import styles from "./DistributionLists.module.css" const DistributionLists: React.FC = () => { return ( -
    +

    Distribution Lists

    Welcome to the Distribution Lists page!

    diff --git a/frontend/src/pages/reports/ReportManagement.module.css b/frontend/src/pages/reports/ReportManagement.module.css new file mode 100644 index 00000000..bd6ba61d --- /dev/null +++ b/frontend/src/pages/reports/ReportManagement.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} diff --git a/frontend/src/pages/reports/ReportManagement.tsx b/frontend/src/pages/reports/ReportManagement.tsx index da29f91f..cdc0dede 100644 --- a/frontend/src/pages/reports/ReportManagement.tsx +++ b/frontend/src/pages/reports/ReportManagement.tsx @@ -1,8 +1,9 @@ import React from "react"; +import styles from "./ReportManagement.module.css"; const ReportManagement: React.FC = () => { return ( -
    +

    Report Management

    Welcome to the Report Management page!

    diff --git a/frontend/src/pages/resources/UploadResources.module.css b/frontend/src/pages/resources/UploadResources.module.css new file mode 100644 index 00000000..bd6ba61d --- /dev/null +++ b/frontend/src/pages/resources/UploadResources.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} diff --git a/frontend/src/pages/resources/UploadResources.tsx b/frontend/src/pages/resources/UploadResources.tsx index e2515827..5825c9f2 100644 --- a/frontend/src/pages/resources/UploadResources.tsx +++ b/frontend/src/pages/resources/UploadResources.tsx @@ -1,8 +1,9 @@ import React from "react"; +import styles from "./UploadResources.module.css" const UploadResources: React.FC = () => { return ( -
    +

    Upload Resources

    Welcome to the Upload Resources page!

    diff --git a/frontend/src/pages/studies/RequestStudies.module.css b/frontend/src/pages/studies/RequestStudies.module.css new file mode 100644 index 00000000..97f3e4ec --- /dev/null +++ b/frontend/src/pages/studies/RequestStudies.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} \ No newline at end of file diff --git a/frontend/src/pages/studies/RequestStudies.tsx b/frontend/src/pages/studies/RequestStudies.tsx index 62b39311..33965246 100644 --- a/frontend/src/pages/studies/RequestStudies.tsx +++ b/frontend/src/pages/studies/RequestStudies.tsx @@ -1,8 +1,9 @@ import React from "react"; +import styles from "./RequestStudies.module.css" const RequestStudies: React.FC = () => { return ( -
    +

    Request Studies

    Welcome to the Request Studies page!

    From 2d95d776ba477e2cc69ccf354c4960038dee518a Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Mon, 2 Dec 2024 17:41:15 -0400 Subject: [PATCH 221/820] FA-245 Temporarily disable buttons that return a 404 page (#190) * FA-205 set up deployment prod (#183) * Added placeholders to pages that give 404 in the sidebar --------- Co-authored-by: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> --- .github/workflows/azure-prod.yml | 2 +- frontend/src/App.tsx | 7 ++++++- frontend/src/components/Sidebar/Sidebar.tsx | 4 ++-- frontend/src/pages/helpcenter/HelpCenter.module.css | 6 ++++++ frontend/src/pages/helpcenter/HelpCenter.tsx | 3 ++- .../pages/notifications/Notifications.module.css | 6 ++++++ frontend/src/pages/notifications/Notifications.tsx | 13 +++++++++++++ .../src/pages/resources/UploadResources.module.css | 1 + .../SubscriptionManagement.module.css | 6 ++++++ .../SubscriptionManagement.tsx | 13 +++++++++++++ .../pages/usermanagement/UserManagement.module.css | 6 ++++++ .../src/pages/usermanagement/UserManagement.tsx | 13 +++++++++++++ 12 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/helpcenter/HelpCenter.module.css create mode 100644 frontend/src/pages/notifications/Notifications.module.css create mode 100644 frontend/src/pages/notifications/Notifications.tsx create mode 100644 frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css create mode 100644 frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx create mode 100644 frontend/src/pages/usermanagement/UserManagement.module.css create mode 100644 frontend/src/pages/usermanagement/UserManagement.tsx diff --git a/.github/workflows/azure-prod.yml b/.github/workflows/azure-prod.yml index 3e48ee0b..a573baf2 100644 --- a/.github/workflows/azure-prod.yml +++ b/.github/workflows/azure-prod.yml @@ -61,4 +61,4 @@ jobs: --name ${{ vars.AZURE_PROD_WEBAPP_NAME }} \ --src-path deploy.zip \ --type zip \ - --async true + --async true \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4f15c7c1..5dc71b8a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,7 +15,9 @@ import RequestStudies from "./pages/studies/RequestStudies"; import ReportManagement from "./pages/reports/ReportManagement"; import DistributionLists from "./pages/reports/DistributionLists"; import Logout from "./pages/logout/Logout"; - +import Notifications from "./pages/notifications/Notifications"; +import SubscriptionManagement from "./pages/subscriptionmanagement/SubscriptionManagement"; +import UserManagement from "./pages/usermanagement/UserManagement"; import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; @@ -50,6 +52,7 @@ export default function App() { } /> } /> } /> + }/> @@ -74,6 +77,8 @@ export default function App() { } /> } /> } /> + }/> + }/> diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index c0d8804e..8985c199 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -121,7 +121,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { { title: "Subscription Management", icon: , - to: "/subscription", + to: "/subscription-management", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin"] }, @@ -193,7 +193,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { { title: "Help Center", icon: , - to: "/financial-assistant", + to: "/help-center", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin", "user"] } diff --git a/frontend/src/pages/helpcenter/HelpCenter.module.css b/frontend/src/pages/helpcenter/HelpCenter.module.css new file mode 100644 index 00000000..97f3e4ec --- /dev/null +++ b/frontend/src/pages/helpcenter/HelpCenter.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} \ No newline at end of file diff --git a/frontend/src/pages/helpcenter/HelpCenter.tsx b/frontend/src/pages/helpcenter/HelpCenter.tsx index 24644d20..e521b219 100644 --- a/frontend/src/pages/helpcenter/HelpCenter.tsx +++ b/frontend/src/pages/helpcenter/HelpCenter.tsx @@ -1,8 +1,9 @@ import React from "react"; +import styles from "./HelpCenter.module.css" const HelpCenter: React.FC = () => { return ( -
    +

    Help Center

    Welcome to the Help Center page!

    diff --git a/frontend/src/pages/notifications/Notifications.module.css b/frontend/src/pages/notifications/Notifications.module.css new file mode 100644 index 00000000..97f3e4ec --- /dev/null +++ b/frontend/src/pages/notifications/Notifications.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} \ No newline at end of file diff --git a/frontend/src/pages/notifications/Notifications.tsx b/frontend/src/pages/notifications/Notifications.tsx new file mode 100644 index 00000000..655d56ee --- /dev/null +++ b/frontend/src/pages/notifications/Notifications.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import styles from "./Notifications.module.css" + +const Notifications: React.FC = () => { + return ( +
    +

    Notifications

    +

    Welcome to the Notifications page!

    +
    + ); +}; + +export default Notifications; diff --git a/frontend/src/pages/resources/UploadResources.module.css b/frontend/src/pages/resources/UploadResources.module.css index bd6ba61d..6b678be7 100644 --- a/frontend/src/pages/resources/UploadResources.module.css +++ b/frontend/src/pages/resources/UploadResources.module.css @@ -4,3 +4,4 @@ width: 100%; padding: 100px 100px 100px 150px; } + diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css new file mode 100644 index 00000000..97f3e4ec --- /dev/null +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} \ No newline at end of file diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx new file mode 100644 index 00000000..c41d9161 --- /dev/null +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import styles from "./SubscriptionManagement.module.css" + +const SubscriptionManagement: React.FC = () => { + return ( +
    +

    Subscription Management

    +

    Welcome to the Subscription Management page!

    +
    + ); +}; + +export default SubscriptionManagement; diff --git a/frontend/src/pages/usermanagement/UserManagement.module.css b/frontend/src/pages/usermanagement/UserManagement.module.css new file mode 100644 index 00000000..97f3e4ec --- /dev/null +++ b/frontend/src/pages/usermanagement/UserManagement.module.css @@ -0,0 +1,6 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 100px 100px 100px 150px; +} \ No newline at end of file diff --git a/frontend/src/pages/usermanagement/UserManagement.tsx b/frontend/src/pages/usermanagement/UserManagement.tsx new file mode 100644 index 00000000..adf1c93f --- /dev/null +++ b/frontend/src/pages/usermanagement/UserManagement.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import styles from "./UserManagement.module.css" + +const UserManagement: React.FC = () => { + return ( +
    +

    User Management

    +

    Welcome to the User Management page!

    +
    + ); +}; + +export default UserManagement; From 23d089d8867b6393cce293519ee30db7740f0b13 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:42:45 -0400 Subject: [PATCH 222/820] FA-215 Change the Profile Dropdown as a button with a Modal (#191) --- frontend/src/components/Navbar/NavBar.tsx | 35 +++---------------- .../ProfilePanel/Profile.module.css | 19 ++++++++++ .../src/components/ProfilePanel/Profile.tsx | 29 +++++++++++++++ 3 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/ProfilePanel/Profile.module.css create mode 100644 frontend/src/components/ProfilePanel/Profile.tsx diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx index af6ae923..597f443f 100644 --- a/frontend/src/components/Navbar/NavBar.tsx +++ b/frontend/src/components/Navbar/NavBar.tsx @@ -1,8 +1,9 @@ import React, { useState } from "react"; import styles from "./Navbar.module.css"; -import { IconMenu2, IconMessageCircle, IconHistory, IconSettings, IconBell, IconUser, IconMail, IconListCheck } from "@tabler/icons-react"; +import { IconMenu2, IconMessageCircle, IconHistory, IconSettings, IconBell, IconAppsFilled} from "@tabler/icons-react"; import { useAppContext } from "../../providers/AppProviders"; import { Link, useLocation } from "react-router-dom"; +import { ProfilePanel } from "../ProfilePanel/Profile"; interface NavbarProps { setIsCollapsed: React.Dispatch>; @@ -142,44 +143,18 @@ const Navbar: React.FC = ({ setIsCollapsed }) => { {/* User Profile Card */}
  • -
    -
    - - -

    My Profile

    - - - -

    My Account

    - - - -

    My Task

    - - - Logout - -
    -
    + {isDropdownOpen && }
  • diff --git a/frontend/src/components/ProfilePanel/Profile.module.css b/frontend/src/components/ProfilePanel/Profile.module.css new file mode 100644 index 00000000..77bd11c4 --- /dev/null +++ b/frontend/src/components/ProfilePanel/Profile.module.css @@ -0,0 +1,19 @@ +.messageBody { + position: fixed; + top: 110px; + right: 20px; + width: auto; + max-width: 300px; + height: auto; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + z-index: 1000; + padding: 20px; + gap: 10px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); +} \ No newline at end of file diff --git a/frontend/src/components/ProfilePanel/Profile.tsx b/frontend/src/components/ProfilePanel/Profile.tsx new file mode 100644 index 00000000..d38c7a1d --- /dev/null +++ b/frontend/src/components/ProfilePanel/Profile.tsx @@ -0,0 +1,29 @@ +import { IconListCheck, IconMail, IconUser } from "@tabler/icons-react"; +import React from "react"; +import styles from "./Profile.module.css" +import { Link } from "react-router-dom"; + +export const ProfilePanel = () => { + + return ( + +
    + + +

    My Profile

    + + + +

    My Account

    + + + +

    My Task

    + + + Logout + +
    + + ); +}; \ No newline at end of file From 25ea0a5916a96a37074baa4f080dd49bb82eca6d Mon Sep 17 00:00:00 2001 From: egdagger Date: Tue, 3 Dec 2024 10:42:37 -0400 Subject: [PATCH 223/820] FA 218 Adding a conditional --- backend/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index d652ebbd..cd01c581 100644 --- a/backend/app.py +++ b/backend/app.py @@ -289,9 +289,11 @@ def index(*, context): """ # Check if we have a return URL in session if "original_url" in session: + return_url = session.pop("original_url") - return redirect(return_url) - + if request.url != return_url: + return redirect(return_url) + return send_from_directory("static", "index.html") From 740eb7b8116889b02880ae4a6a2c9ea892cf2167 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:15:11 -0400 Subject: [PATCH 224/820] Revert "FA-218 Implement Financial email user flow" (#198) --- backend/app.py | 63 +++++++++----------------------------------------- 1 file changed, 11 insertions(+), 52 deletions(-) diff --git a/backend/app.py b/backend/app.py index 761f7dab..39b35f38 100644 --- a/backend/app.py +++ b/backend/app.py @@ -48,10 +48,6 @@ ) import stripe.error -from urllib.parse import urlencode, parse_qs, urlparse, urlunparse -import base64 -import json - load_dotenv() @@ -109,9 +105,6 @@ def get_secret(secretName): app.config.from_object(app_config) CORS(app) -app.secret_key = os.getenv( - "FLASK_SECRET_KEY", "your-secret-key-here" -) # Make sure to set this in production auth = Auth( app, @@ -285,55 +278,12 @@ def check_user_authorization( @auth.login_required def index(*, context): """ - Main entry point - if there's a stored return URL, redirect to it + Endpoint to get the current user's data from Microsoft Graph API """ - # Check if we have a return URL in session - if "original_url" in session: - - return_url = session.pop("original_url") - if request.url != return_url: - return redirect(return_url) - + logger.debug(f"User context: {context}") return send_from_directory("static", "index.html") -@app.route("/auth-response") -def auth_response(): - """ - Handle the authentication response from Azure B2C - Preserves the original URL from the request - """ - try: - # Store the original URL from query parameters if present - if request.args.get("original_url"): - session["original_url"] = base64.b64decode( - request.args.get("original_url") - ).decode() - - # Complete the login process - result = auth.complete_log_in(request.args) - - # Redirect to index which will handle the return URL - return redirect(url_for("index")) - - except Exception as e: - app.logger.error(f"Authentication error: {str(e)}") - return redirect(url_for("index")) - - -# Add this middleware to capture the original URL before Azure B2C redirect -@app.before_request -def before_request(): - """ - Middleware to capture the original URL before Azure B2C redirect - """ - # Only store the URL if it's not an auth-related endpoint - if not request.path.startswith("/auth") and not request.path.startswith("/static"): - if request.query_string: - # Store the full URL in session - session["original_url"] = request.url - - # route for other static files @@ -343,6 +293,15 @@ def static_files(path): return send_from_directory("static", path) +@app.route("/auth-response") +def auth_response(): + try: + return auth.complete_log_in(request.args) + except Exception as e: + logger.error(f"Authentication error: {str(e)}") + return redirect(url_for("index")) + + @app.route("/api/auth/config") def get_auth_config(): """Return Azure AD B2C configuration for frontend""" From 7af1cfa9b0d32e0cc2ee2ab175323ff920e08434 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:41:45 -0400 Subject: [PATCH 225/820] FA-264 Improve UI style and behavior (#199) --- .../src/components/Answer/Answer.module.css | 4 +-- .../FeedbackRating/FeedbackRating.module.css | 5 +++ .../FeedbackRating/FeedbackRating.tsx | 2 +- .../QuestionInput/QuestionInput.module.css | 1 - .../src/components/Sidebar/Sidebar.module.css | 4 +++ .../StartNewChatButton.module.css | 36 +++++++++++++++++++ .../StartNewChatButton/StartNewChatButton.tsx | 21 ++++++----- frontend/src/pages/chat/Chat.module.css | 2 +- 8 files changed, 61 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/Answer/Answer.module.css b/frontend/src/components/Answer/Answer.module.css index a47e09ae..994aca2d 100644 --- a/frontend/src/components/Answer/Answer.module.css +++ b/frontend/src/components/Answer/Answer.module.css @@ -7,7 +7,7 @@ } .answerContainer .markdownContent { - font-size: 0.875rem; + font-size: 1rem; } .answerLogo { @@ -140,6 +140,6 @@ sup { @media (max-width: 680px) { .answerContainer .markdownContent { - font-size: 0.75rem; + font-size: 0.875rem; } } diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css index 403b1914..3460e09e 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.module.css +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -256,3 +256,8 @@ .message { margin-top: 10px; } + +.disclaimer{ + color: rgb(225, 37, 37); + text-align: center; +} \ No newline at end of file diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index 1b71156a..1da49699 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -143,7 +143,7 @@ export const FeedbackRating = () => {   Send - All fields must be filled + All fields must be filled
    {errorMessage !== null &&

    {errorMessage}

    }
    diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css index a1cfcbfe..4aff4dcd 100644 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ b/frontend/src/components/QuestionInput/QuestionInput.module.css @@ -5,7 +5,6 @@ background: transparent; position: relative; max-height: 200px; - overflow: hidden; } .attachmentContainer { diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css index 595a74c9..1954d687 100644 --- a/frontend/src/components/Sidebar/Sidebar.module.css +++ b/frontend/src/components/Sidebar/Sidebar.module.css @@ -121,6 +121,8 @@ font-weight: 500; margin-bottom: 4px; width: 100%; + cursor: pointer; + user-select: none; } .sidebarLinkIcon { @@ -138,6 +140,8 @@ background-color: #000000; color: #fff; box-shadow: 0px 17px 20px -8px rgba(77, 91, 236, 0.23); + cursor: pointer; + user-select: none; } .sidebarLinkActive:hover{ diff --git a/frontend/src/components/StartNewChatButton/StartNewChatButton.module.css b/frontend/src/components/StartNewChatButton/StartNewChatButton.module.css index 41c4d306..e999742a 100644 --- a/frontend/src/components/StartNewChatButton/StartNewChatButton.module.css +++ b/frontend/src/components/StartNewChatButton/StartNewChatButton.module.css @@ -33,3 +33,39 @@ margin-right: 5px; font-style: italic; } + +.tooltipContainer { + position: relative; + display: inline-block; +} + +.tooltipText { + visibility: hidden; + background-color: white; + color: black; + text-align: center; + padding: 5px; + border-radius: 5px; + position: absolute; + bottom: 110%; + left: 50%; + border-radius: 6px; + border: 2px solid #9fc51d; + transform: translateX(-50%); + white-space: nowrap; + z-index: 9999; + opacity: 0; + transition: opacity 0.3s; +} + +.tooltipContainer:hover .tooltipText { + visibility: visible; + opacity: 1; +} + +@media (hover: none) and (pointer: coarse) { + .tooltipContainer:hover .tooltipText { + visibility: hidden; + opacity: 0; + } +} diff --git a/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx b/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx index 0c8c8bd6..dab4fb94 100644 --- a/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx +++ b/frontend/src/components/StartNewChatButton/StartNewChatButton.tsx @@ -3,15 +3,18 @@ import styles from "./StartNewChatButton.module.css"; const StartNewChatButton = ({ isEnabled, onClick }: { isEnabled: boolean; onClick: () => void }) => { return ( - +
    + + Start a new chat +
    ); }; diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index eb7bd7d9..9e387bd6 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -55,7 +55,7 @@ } .chatEmptyStateTitle { - font-size: 3rem; + font-size: 4rem; margin-bottom: 0px; } } From a44612f121cc2a2647ff564780e8147b73d78efd Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:42:33 -0400 Subject: [PATCH 226/820] FA-112 Adding CSS styles to make the text responsive (#200) --- frontend/src/pages/chat/Chat.module.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css index 9e387bd6..99578b67 100644 --- a/frontend/src/pages/chat/Chat.module.css +++ b/frontend/src/pages/chat/Chat.module.css @@ -211,6 +211,14 @@ margin-top: 5px; color: #8d8d8d; font-size: 0.8em; + text-align: center; + +} + +@media only screen and (max-height: 999px) { + .chatDisclaimer{ + margin-left: 30px; + } } .loadingLogo { From 07377bf8bb37c9832503d2278da248bf2cdb4562 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Tue, 3 Dec 2024 20:51:29 -0400 Subject: [PATCH 227/820] Fix UI issues (#202) --- frontend/src/components/Answer/Answer.module.css | 4 ++++ .../components/HistoryPannel/ChatHistoryPannel.module.css | 6 +++--- frontend/src/components/Sidebar/Sidebar.tsx | 1 + .../components/UserChatMessage/UserChatMessage.module.css | 4 ++++ frontend/src/pages/layout/_Layout.module.css | 1 - 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Answer/Answer.module.css b/frontend/src/components/Answer/Answer.module.css index 994aca2d..d422075f 100644 --- a/frontend/src/components/Answer/Answer.module.css +++ b/frontend/src/components/Answer/Answer.module.css @@ -8,6 +8,10 @@ .answerContainer .markdownContent { font-size: 1rem; + font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } .answerLogo { diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 199b5c7d..2cbe9ae8 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -174,7 +174,7 @@ justify-content: space-between; align-items: center; border-radius: 5px; - background-color: #ffffff; + background-color: #cccccc; transition: 0.2s; } @@ -188,10 +188,10 @@ padding: 6px; justify-content: center; align-items: center; - background-color: #f5f5f5; + background-color: #cccccc; border-radius: 6px; cursor: pointer; - border: 1px solid #e3e3e3; + border: 1px solid #cccccc; transition: 0.3s; margin: auto; margin-right: 5px; diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 8985c199..8c505cb8 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -31,6 +31,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { const [activeItem, setActiveItem] = useState(null); const handleItemClick = (itemTitle: string) => { setActiveItem(itemTitle); + setIsCollapsed(true); }; const handleOnClickCloseSideBar = () => { diff --git a/frontend/src/components/UserChatMessage/UserChatMessage.module.css b/frontend/src/components/UserChatMessage/UserChatMessage.module.css index 89af492e..f32443c0 100644 --- a/frontend/src/components/UserChatMessage/UserChatMessage.module.css +++ b/frontend/src/components/UserChatMessage/UserChatMessage.module.css @@ -13,6 +13,10 @@ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); outline: transparent solid 1px; font-size: 1rem; + font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } @media (max-width: 680px) { diff --git a/frontend/src/pages/layout/_Layout.module.css b/frontend/src/pages/layout/_Layout.module.css index 0574ae3c..e68beec4 100644 --- a/frontend/src/pages/layout/_Layout.module.css +++ b/frontend/src/pages/layout/_Layout.module.css @@ -13,7 +13,6 @@ flex-direction: column; flex: 1; height: 100%; - margin-top: 80px; } .bodyWrapperContainer { From 2ba38808ff17802b48874393e7c26456fe7b3b65 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:34:26 -0400 Subject: [PATCH 228/820] Filter conversations (#203) --- frontend/src/api/models.ts | 1 + .../components/HistoryPannel/ChatHistoryListItem.tsx | 10 ++++++++-- frontend/src/providers/AppProviders.tsx | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index cb172984..f5bf5999 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -17,6 +17,7 @@ export type ConversationHistoryItem = { id: string; start_date: string; content: string; + type: string; }; export type ConversationChatItem = { diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index e1d5e816..888b18f3 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -33,7 +33,8 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete chatSelected, setChatSelected, setNewChatDeleted, - setShowHistoryPanel + setShowHistoryPanel, + isFinancialAssistantActive, } = useAppContext(); const handleMouseEnter = (index: string) => { @@ -174,7 +175,12 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const sortedDataByDate = dataHistory.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); + // either default for usual conversations or financial for financial conversations + // != "financial" for default conversations + const dataDefaultOrFinancial = isFinancialAssistantActive ? + dataHistory.filter(item => item.type == "financial") : + dataHistory.filter(item => item.type != "financial"); + const sortedDataByDate = dataDefaultOrFinancial.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); const uniqueItems = new Set(); diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index c9d1c6fc..2e414def 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -34,6 +34,7 @@ export interface ConversationHistoryItem { id: string; start_date: string; content: string; + type: string; // Add other properties as needed } From 5b06d7043304608888c8c5f78b357d3f5a3cf04b Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Tue, 3 Dec 2024 21:57:29 -0400 Subject: [PATCH 229/820] Revert "Filter conversations (#203)" (#204) This reverts commit 2ba38808ff17802b48874393e7c26456fe7b3b65. --- frontend/src/api/models.ts | 1 - .../components/HistoryPannel/ChatHistoryListItem.tsx | 10 ++-------- frontend/src/providers/AppProviders.tsx | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index f5bf5999..cb172984 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -17,7 +17,6 @@ export type ConversationHistoryItem = { id: string; start_date: string; content: string; - type: string; }; export type ConversationChatItem = { diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index 888b18f3..e1d5e816 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -33,8 +33,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete chatSelected, setChatSelected, setNewChatDeleted, - setShowHistoryPanel, - isFinancialAssistantActive, + setShowHistoryPanel } = useAppContext(); const handleMouseEnter = (index: string) => { @@ -175,12 +174,7 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - // either default for usual conversations or financial for financial conversations - // != "financial" for default conversations - const dataDefaultOrFinancial = isFinancialAssistantActive ? - dataHistory.filter(item => item.type == "financial") : - dataHistory.filter(item => item.type != "financial"); - const sortedDataByDate = dataDefaultOrFinancial.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); + const sortedDataByDate = dataHistory.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); const uniqueItems = new Set(); diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 2e414def..c9d1c6fc 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -34,7 +34,6 @@ export interface ConversationHistoryItem { id: string; start_date: string; content: string; - type: string; // Add other properties as needed } From 0a7f38fe9944be562847395ebb10cf6b831fbbe5 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Wed, 4 Dec 2024 10:15:50 -0400 Subject: [PATCH 230/820] Revert "Revert "Filter conversations (#203)" (#204)" (#205) This reverts commit 5b06d7043304608888c8c5f78b357d3f5a3cf04b. --- frontend/src/api/models.ts | 1 + .../components/HistoryPannel/ChatHistoryListItem.tsx | 10 ++++++++-- frontend/src/providers/AppProviders.tsx | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index cb172984..f5bf5999 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -17,6 +17,7 @@ export type ConversationHistoryItem = { id: string; start_date: string; content: string; + type: string; }; export type ConversationChatItem = { diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index e1d5e816..888b18f3 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -33,7 +33,8 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete chatSelected, setChatSelected, setNewChatDeleted, - setShowHistoryPanel + setShowHistoryPanel, + isFinancialAssistantActive, } = useAppContext(); const handleMouseEnter = (index: string) => { @@ -174,7 +175,12 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const sortedDataByDate = dataHistory.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); + // either default for usual conversations or financial for financial conversations + // != "financial" for default conversations + const dataDefaultOrFinancial = isFinancialAssistantActive ? + dataHistory.filter(item => item.type == "financial") : + dataHistory.filter(item => item.type != "financial"); + const sortedDataByDate = dataDefaultOrFinancial.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); const uniqueItems = new Set(); diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index c9d1c6fc..2e414def 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -34,6 +34,7 @@ export interface ConversationHistoryItem { id: string; start_date: string; content: string; + type: string; // Add other properties as needed } From 97f84a6343d7916a93729c8f1d513f9a2c52ba57 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Wed, 4 Dec 2024 12:38:08 -0400 Subject: [PATCH 231/820] Release 0.1.2 (#206) * Fix UI issues (#202) * Filter conversations (#203) * Revert "Filter conversations (#203)" (#204) This reverts commit 2ba38808ff17802b48874393e7c26456fe7b3b65. * Revert "Revert "Filter conversations (#203)" (#204)" (#205) This reverts commit 5b06d7043304608888c8c5f78b357d3f5a3cf04b. --------- Co-authored-by: ramirezmorac2 Co-authored-by: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> --- frontend/src/api/models.ts | 1 + frontend/src/components/Answer/Answer.module.css | 4 ++++ .../components/HistoryPannel/ChatHistoryListItem.tsx | 10 ++++++++-- .../HistoryPannel/ChatHistoryPannel.module.css | 6 +++--- frontend/src/components/Sidebar/Sidebar.tsx | 1 + .../UserChatMessage/UserChatMessage.module.css | 4 ++++ frontend/src/pages/layout/_Layout.module.css | 1 - frontend/src/providers/AppProviders.tsx | 1 + 8 files changed, 22 insertions(+), 6 deletions(-) diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index cb172984..f5bf5999 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -17,6 +17,7 @@ export type ConversationHistoryItem = { id: string; start_date: string; content: string; + type: string; }; export type ConversationChatItem = { diff --git a/frontend/src/components/Answer/Answer.module.css b/frontend/src/components/Answer/Answer.module.css index 994aca2d..d422075f 100644 --- a/frontend/src/components/Answer/Answer.module.css +++ b/frontend/src/components/Answer/Answer.module.css @@ -8,6 +8,10 @@ .answerContainer .markdownContent { font-size: 1rem; + font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } .answerLogo { diff --git a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx index e1d5e816..888b18f3 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryListItem.tsx @@ -33,7 +33,8 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete chatSelected, setChatSelected, setNewChatDeleted, - setShowHistoryPanel + setShowHistoryPanel, + isFinancialAssistantActive, } = useAppContext(); const handleMouseEnter = (index: string) => { @@ -174,7 +175,12 @@ export const ChatHistoryPanelList: React.FC = ({ onDelete const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const sortedDataByDate = dataHistory.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); + // either default for usual conversations or financial for financial conversations + // != "financial" for default conversations + const dataDefaultOrFinancial = isFinancialAssistantActive ? + dataHistory.filter(item => item.type == "financial") : + dataHistory.filter(item => item.type != "financial"); + const sortedDataByDate = dataDefaultOrFinancial.sort((a, b) => Number(new Date(a.start_date)) - Number(new Date(b.start_date))); const uniqueItems = new Set(); diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css index 199b5c7d..2cbe9ae8 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css +++ b/frontend/src/components/HistoryPannel/ChatHistoryPannel.module.css @@ -174,7 +174,7 @@ justify-content: space-between; align-items: center; border-radius: 5px; - background-color: #ffffff; + background-color: #cccccc; transition: 0.2s; } @@ -188,10 +188,10 @@ padding: 6px; justify-content: center; align-items: center; - background-color: #f5f5f5; + background-color: #cccccc; border-radius: 6px; cursor: pointer; - border: 1px solid #e3e3e3; + border: 1px solid #cccccc; transition: 0.3s; margin: auto; margin-right: 5px; diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 8985c199..8c505cb8 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -31,6 +31,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { const [activeItem, setActiveItem] = useState(null); const handleItemClick = (itemTitle: string) => { setActiveItem(itemTitle); + setIsCollapsed(true); }; const handleOnClickCloseSideBar = () => { diff --git a/frontend/src/components/UserChatMessage/UserChatMessage.module.css b/frontend/src/components/UserChatMessage/UserChatMessage.module.css index 89af492e..f32443c0 100644 --- a/frontend/src/components/UserChatMessage/UserChatMessage.module.css +++ b/frontend/src/components/UserChatMessage/UserChatMessage.module.css @@ -13,6 +13,10 @@ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); outline: transparent solid 1px; font-size: 1rem; + font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } @media (max-width: 680px) { diff --git a/frontend/src/pages/layout/_Layout.module.css b/frontend/src/pages/layout/_Layout.module.css index 0574ae3c..e68beec4 100644 --- a/frontend/src/pages/layout/_Layout.module.css +++ b/frontend/src/pages/layout/_Layout.module.css @@ -13,7 +13,6 @@ flex-direction: column; flex: 1; height: 100%; - margin-top: 80px; } .bodyWrapperContainer { diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index c9d1c6fc..2e414def 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -34,6 +34,7 @@ export interface ConversationHistoryItem { id: string; start_date: string; content: string; + type: string; // Add other properties as needed } From 556fe5ce6e5907ccbeaed6ac2fc68cc9b6a5ee30 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Fri, 6 Dec 2024 13:51:13 -0400 Subject: [PATCH 232/820] FA-272-Create the Financial Assistant activation pop-up (#207) --- .../FinancialAssistantPopup.module.css | 33 ++++++++++++++++ .../FinancialAssistantPopup.tsx | 39 +++++++++++++++++++ frontend/src/pages/chat/Chat.tsx | 4 +- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.module.css create mode 100644 frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.tsx diff --git a/frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.module.css b/frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.module.css new file mode 100644 index 00000000..917c3b42 --- /dev/null +++ b/frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.module.css @@ -0,0 +1,33 @@ +.body { + position: fixed; + top: 110px; + right: 30%; + width: auto; + max-width: 300px; + height: auto; + display: flex; + padding: 20px; + gap: 10px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + text-align: center; + flex-direction: column; + justify-content: flex-end; + align-items: flex-start; + z-index: 1000; +} + +.chatHistoryText{ + font-size: 0.7em; + text-align: center; + margin-left: 8%; +} + + +@media (max-width: 900px){ + .body{ + right: 8%; + } +} diff --git a/frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.tsx b/frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.tsx new file mode 100644 index 00000000..043c9636 --- /dev/null +++ b/frontend/src/components/FinancialAssistantPopup/FinancialAssistantPopup.tsx @@ -0,0 +1,39 @@ +import styles from "./FinancialAssistantPopup.module.css"; +import { useEffect, useState } from "react"; +import { useAppContext } from "../../providers/AppProviders"; + +const FinancialPopup = () => { + const { isFinancialAssistantActive} = useAppContext(); + const [isPopupVisible, setIsPopupVisible] = useState(false); + + useEffect(() => { + let timer: NodeJS.Timeout; + + if (isFinancialAssistantActive || !isFinancialAssistantActive) { + + setIsPopupVisible(true) + timer = setTimeout(() => { + setIsPopupVisible(false); + }, 1500); + } + + return () => { + clearTimeout(timer); + }; + }, [isFinancialAssistantActive]); + const popupContent = isFinancialAssistantActive ? "activated" : "deactivated"; + const historyDisclaimer = isFinancialAssistantActive ? "financial" : "consumer"; + + return ( +
    + {isPopupVisible && ( +
    +

    The Financial Assistant has been {popupContent}

    +

    Chat History has been set to {historyDisclaimer} mode

    +
    + )} +
    + ); +}; + +export default FinancialPopup; \ No newline at end of file diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 4b5d53b5..9c589562 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -19,6 +19,7 @@ import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPane import { FeedbackRating } from "../../components/FeedbackRating/FeedbackRating"; import { SettingsPanel } from "../../components/SettingsPanel"; import StartNewChatButton from "../../components/StartNewChatButton/StartNewChatButton"; +import FinancialPopup from "../../components/FinancialAssistantPopup/FinancialAssistantPopup"; const userLanguage = navigator.language; let error_message_text = ""; @@ -383,6 +384,7 @@ const Chat = () => {
    {settingsPanel && }
    +
    @@ -405,7 +407,7 @@ const Chat = () => { className={!conversationIsLoading ? styles.chatMessageStream : styles.conversationIsLoading} aria-label="Chat messages" tabIndex={0} - > + > {conversationIsLoading && } {dataConversation.length > 0 ? dataConversation.map((item, index) => { From b2f435972cd6f2e9c328020f22dbbdd507167ef5 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Fri, 6 Dec 2024 14:01:23 -0400 Subject: [PATCH 233/820] FA-273 Fix Modals Settings, Chat History and fix the chat size (#209) --- .../HistoryPannel/ChatHistoryPanel.tsx | 21 ++- .../src/components/SettingsPanel/index.tsx | 148 +++++++++--------- frontend/src/pages/layout/_Layout.module.css | 2 +- 3 files changed, 95 insertions(+), 76 deletions(-) diff --git a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx index 47783271..2c4e9149 100644 --- a/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx +++ b/frontend/src/components/HistoryPannel/ChatHistoryPanel.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useRef } from "react"; import { AddFilled } from "@fluentui/react-icons"; import styles from "./ChatHistoryPannel.module.css"; import { useAppContext } from "../../providers/AppProviders"; @@ -14,8 +14,25 @@ export const ChatHistoryPanel: React.FC = ({ functionDele const handleClosePannel = () => { setShowHistoryPanel(!showHistoryPanel); }; + + const panelRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (panelRef.current && !panelRef.current.contains(event.target as Node)) { + setShowHistoryPanel(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [setShowHistoryPanel]); + return ( -
    +
    Chat history
    diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx index 8d28b620..6349b0c5 100644 --- a/frontend/src/components/SettingsPanel/index.tsx +++ b/frontend/src/components/SettingsPanel/index.tsx @@ -1,7 +1,6 @@ -import React, { useState, useEffect, useCallback, useContext } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { AddFilled, SaveFilled, ErrorCircleFilled } from "@fluentui/react-icons"; -import { Checkbox } from "@fluentui/react/lib/Checkbox"; -import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Modal, Stack, Text, Spinner, Slider } from "@fluentui/react"; +import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Stack, Spinner, Slider } from "@fluentui/react"; import styles from "./SettingsModal.module.css"; import { getSettings, postSettings } from "../../api/api"; import { mergeStyles } from "@fluentui/react/lib/Styling"; @@ -185,82 +184,85 @@ export const SettingsPanel = () => { // Display a message or render a different component return
    Please log in to view your settings.
    ; } + const panelRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (panelRef.current && !panelRef.current.contains(event.target as Node)) { + setSettingsPanel(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [setSettingsPanel]); return ( -
    - - { - setIsDialogOpen(false); - }} - onConfirm={() => { - setIsLoadingSettings(true); - handleSubmit(); - }} - /> - - -
    -
    Configuration
    -
    -
    -
    - -
    +
    + { + setIsDialogOpen(false); + }} + onConfirm={() => { + setIsLoadingSettings(true); + handleSubmit(); + }} + /> + + +
    +
    Configuration
    +
    +
    +
    +
    - {loading ? ( -
    -

    - Loading your settings -

    - -
    - ) : ( -
    -
    -
    - Creativity Scale -
    - setTemperature(e.toString())} - aria-labelledby="temperature-slider" - /> - setIsDialogOpen(true)} - aria-label="Save settings" - > - -   Save - +
    + {loading ? ( +
    +

    Loading your settings

    + +
    + ) : ( +
    +
    +
    + Creativity Scale
    + setTemperature(e.toString())} + aria-labelledby="temperature-slider" + /> + setIsDialogOpen(true)} aria-label="Save settings"> + +   Save +
    - )} - - +
    + )} + +
    ); }; diff --git a/frontend/src/pages/layout/_Layout.module.css b/frontend/src/pages/layout/_Layout.module.css index e68beec4..f6c0abc1 100644 --- a/frontend/src/pages/layout/_Layout.module.css +++ b/frontend/src/pages/layout/_Layout.module.css @@ -5,7 +5,7 @@ .bodyWrapper { position: relative; - height: 80vh; + height: 88vh; } .bodyWrapperInner { From a659595fdcf0fcb3fe8e216f17dcf09969f60c09 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:05:48 -0400 Subject: [PATCH 234/820] Fa 270 Create Financial Agent Welcome Page (#211) * FA-270 Create Financial Agent Welcome Page conditional * FA-270 Create Financial Agent Welcome Page --- frontend/src/pages/chat/Chat.tsx | 33 ++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 9c589562..d0d7bca3 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -391,16 +391,29 @@ const Chat = () => { {!lastQuestionRef.current && dataConversation.length <= 0 ? (
    0 && !conversationIsLoading ? styles.chatMessageStream : styles.chatEmptyState}> {conversationIsLoading && } - -
    - Sales Factory logo -

    FreddAid

    - -

    - Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver - actionable insights. -

    -
    + { !isFinancialAssistantActive && +
    + Sales Factory logo +

    FreddAid

    + +

    + Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to deliver + actionable insights. +

    +
    + } + + { isFinancialAssistantActive && +
    + Sales Factory logo +

    FinlAI

    + +

    + Your financial ally, delivering real-time insights and strategic guidance to help you stay ahead of opportunities + and threats in an ever-changing financial landscape. +

    +
    + }
    ) : (
    Date: Mon, 9 Dec 2024 07:23:42 -0400 Subject: [PATCH 235/820] FA-247 html links in sources for hd test are returning a page not found error (#208) * Fix Modals Settings, Chat History and fix the chat size * When you click on the links in an answer, it opens in another window. * Place that style in the css module * Add "https://" to the beginning of source links to avoid 404 errors --- frontend/src/api/api.ts | 1 - .../src/components/Answer/Answer.module.css | 11 +++++++ frontend/src/components/Answer/Answer.tsx | 31 ++++++++++--------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 8dd1b794..7fb6d5ee 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -246,7 +246,6 @@ export function getCitationFilePath(citation: string): string { const parsedResponse = JSON.parse(xhr.responseText); storage_account = parsedResponse["storageaccount"]; } - console.log("storage account:" + storage_account); return `https://${storage_account}.blob.core.windows.net/documents/${citation}`; } diff --git a/frontend/src/components/Answer/Answer.module.css b/frontend/src/components/Answer/Answer.module.css index d422075f..2b78ec62 100644 --- a/frontend/src/components/Answer/Answer.module.css +++ b/frontend/src/components/Answer/Answer.module.css @@ -119,6 +119,17 @@ sup { width: fit-content; } +.citationContainer{ + font-weight: "500px"; + line-height: "24px"; + text-align: "center"; + border-radius: "4px"; + padding: "0px"; + color: #123bb6; + text-decoration: "none"; +} + + @keyframes loading { 0% { content: ""; diff --git a/frontend/src/components/Answer/Answer.tsx b/frontend/src/components/Answer/Answer.tsx index fd1afb5a..4664a1f8 100644 --- a/frontend/src/components/Answer/Answer.tsx +++ b/frontend/src/components/Answer/Answer.tsx @@ -1,8 +1,8 @@ import { useMemo } from "react"; import { Stack, IconButton } from "@fluentui/react"; import DOMPurify from "dompurify"; -import ReactMarkdown from 'react-markdown'; -import remarkGfm from 'remark-gfm' +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import rehypeRaw from "rehype-raw"; import styles from "./Answer.module.css"; @@ -74,7 +74,17 @@ export const Answer = ({ - + ( + + {props.children} + + ) + }} + > {sanitizedAnswerHtml} @@ -85,19 +95,12 @@ export const Answer = ({ {citation_label_text}: {parsedAnswer.citations.map((url, i) => { const path = getFilePath(url); + if (!url.startsWith("https://") && !url.endsWith(".pdf") && !url.endsWith(".docx") && !url.endsWith(".doc")) { + url = "https://" + url; + } return ( <> -
    - {`[${++i}]`} -
    +
    {`[${++i}]`}
    { if (event.key === "Enter") { From 62deb3957cc39ccbd8f863a6bccdb4dc4bc48afc Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Wed, 11 Dec 2024 09:03:22 -0400 Subject: [PATCH 236/820] FA-276 fix mobile view for some UI pages (#210) * Fix Modals Settings, Chat History and fix the chat size * When you click on the links in an answer, it opens in another window. * Fix mobile views and desktop view changes --- frontend/src/pages/admin/Admin.module.css | 25 ++++- frontend/src/pages/admin/Admin.tsx | 102 ++++++++++-------- .../pages/invitations/Invitations.module.css | 14 ++- .../src/pages/invitations/Invitations.tsx | 3 +- frontend/src/pages/layout/_Layout.module.css | 1 + .../organization/Organization.module.css | 18 +++- 6 files changed, 115 insertions(+), 48 deletions(-) diff --git a/frontend/src/pages/admin/Admin.module.css b/frontend/src/pages/admin/Admin.module.css index 50425a33..b5710d50 100644 --- a/frontend/src/pages/admin/Admin.module.css +++ b/frontend/src/pages/admin/Admin.module.css @@ -2,7 +2,8 @@ display: flex; flex-direction: column; width: 100%; - padding: 100px 100px 100px 150px; + padding: 50px 50px 50px 50px; + background-color: white; } .separator { @@ -14,6 +15,8 @@ .row { display: flex; flex-direction: row; + flex-wrap: wrap; + gap: 10px; align-items: center; justify-content: space-between; justify-items: center; @@ -26,6 +29,8 @@ margin-left: 20px; } + + .closeButton { position: fixed; top: 0; @@ -69,6 +74,8 @@ .tableContainer { border-radius: 10px; overflow: hidden; + overflow-x: auto; + width: 100%; border: 1px solid #d9d9d9; margin-top: 35px; } @@ -104,3 +111,19 @@ margin-right: 5px; transform: scaleX(-1); } + +@media (max-width: 768px) { + .button { + width: 100%; + padding: 0px 0px 0px 0px; + font-size: 18px; + justify-content: flex-start; + } + .option{ + font-size: 12px; + } + .title{ + margin-bottom: 10px; + margin-left: 0px; + } +} \ No newline at end of file diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index dc6ff4c7..4afe82fe 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -289,7 +289,17 @@ export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; ); }; -export const DeleteUserDialog = ({ isOpen, onDismiss, onConfirm, isDeletingUser }: { isOpen: boolean; onDismiss: any; onConfirm: any; isDeletingUser: boolean; }) => { +export const DeleteUserDialog = ({ + isOpen, + onDismiss, + onConfirm, + isDeletingUser +}: { + isOpen: boolean; + onDismiss: any; + onConfirm: any; + isDeletingUser: boolean; +}) => { return ( - {isDeletingUser && ( -
    - -
    - )} - -
    + {isDeletingUser && ( +
    + + /> +
    + )} + +
    - { onConfirm(); }} - text="Delete user" - /> -
    -
    + text="Delete user" + /> +
    +
    ); }; @@ -391,7 +401,6 @@ const Admin = () => { return
    Please log in to view the user list.
    ; } - useEffect(() => { const getUserList = async () => { if (!user) { @@ -443,8 +452,8 @@ const Admin = () => { }, [search]); const handleDeleteClick = (user: any) => { - setSelectedUser(user); - setIsDeleting(true); + setSelectedUser(user); + setIsDeleting(true); }; const deleteUserFromOrganization = (id: string) => { @@ -513,7 +522,7 @@ const Admin = () => { { }} />
    - - {loading?null:} + + {loading ? null : } { @@ -551,7 +560,7 @@ const Admin = () => { onConfirm={() => { deleteUserFromOrganization(selectedUser?.id); }} - isDeletingUser={isDeletingUser} + isDeletingUser={isDeletingUser} /> {loading ? ( { -
    diff --git a/frontend/src/pages/invitations/Invitations.module.css b/frontend/src/pages/invitations/Invitations.module.css index e003afbb..6bade078 100644 --- a/frontend/src/pages/invitations/Invitations.module.css +++ b/frontend/src/pages/invitations/Invitations.module.css @@ -2,12 +2,15 @@ display: flex; flex-direction: column; width: 100%; - padding: 100px 100px 100px 150px; + padding: 50px 50px 50px 50px; + background-color: white; } .row { display: flex; flex-direction: row; + flex-wrap: wrap; + gap: 10px; align-items: center; justify-content: space-between; justify-items: center; @@ -32,6 +35,8 @@ overflow: hidden; border: 1px solid #d9d9d9; margin-top: 35px; + overflow-x: auto; + width: 100%; } .table thead { @@ -75,3 +80,10 @@ .textJustify { text-align: justify; } + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } +} \ No newline at end of file diff --git a/frontend/src/pages/invitations/Invitations.tsx b/frontend/src/pages/invitations/Invitations.tsx index 41f37f8b..11fff0b0 100644 --- a/frontend/src/pages/invitations/Invitations.tsx +++ b/frontend/src/pages/invitations/Invitations.tsx @@ -76,7 +76,6 @@ const Invitations = () => { return
    Please log in to view your invitations.
    ; } - return (
    <> @@ -87,7 +86,7 @@ const Invitations = () => { Date: Wed, 11 Dec 2024 09:04:17 -0400 Subject: [PATCH 237/820] FA-278 Fix Feedback Rating modal responsiveness and Dropdown (#212) --- .../FeedbackRating/FeedbackRating.module.css | 10 +++ .../FeedbackRating/FeedbackRating.tsx | 88 +++++++++---------- 2 files changed, 54 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.module.css b/frontend/src/components/FeedbackRating/FeedbackRating.module.css index 3460e09e..3472b1bb 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.module.css +++ b/frontend/src/components/FeedbackRating/FeedbackRating.module.css @@ -47,6 +47,16 @@ z-index: 9999; } +@media (max-width: 900px){ + .cardFeedbackWrapper{ + width: 70vw; + height: auto; + top: 16%; + left: 15%; + background-color: transparent; + } +} + .cardFeedback { background-color: #f5f5f5; border: 1px solid #e3e3e3; diff --git a/frontend/src/components/FeedbackRating/FeedbackRating.tsx b/frontend/src/components/FeedbackRating/FeedbackRating.tsx index 1da49699..eee2540e 100644 --- a/frontend/src/components/FeedbackRating/FeedbackRating.tsx +++ b/frontend/src/components/FeedbackRating/FeedbackRating.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useContext } from "react"; -import { Dropdown, TextField, Button, Spinner, DefaultButton } from "@fluentui/react"; +import { Dropdown, TextField, Button, Spinner, DefaultButton, ResponsiveMode } from "@fluentui/react"; import styles from "./FeedbackRating.module.css"; import { useAppContext } from "../../providers/AppProviders"; import { AddFilled, SendRegular, ThumbLikeFilled, ThumbDislikeFilled } from "@fluentui/react-icons"; @@ -99,56 +99,56 @@ export const FeedbackRating = () => { } return ( -
    -
    -
    -
    Feedback
    -
    -
    - +
    +
    +
    +
    Feedback
    +
    +
    + +
    -
    -
    -
    - {isLoading && ( -
    - +
    +
    + {isLoading && ( +
    + +
    + )} + + + +
    + +
    - )} - - - -
    - - + +   Send + + + All fields must be filled
    - -   Send - - - All fields must be filled + {errorMessage !== null &&

    {errorMessage}

    }
    - {errorMessage !== null &&

    {errorMessage}

    }
    -
    ); }; From 7010e2a0bbc700c423e27c19525bc6a91fe632a7 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Wed, 11 Dec 2024 09:10:13 -0400 Subject: [PATCH 238/820] FA-164 Add Financial assistant section in Admin Features (#213) --- frontend/src/components/Sidebar/Sidebar.tsx | 24 +++++++----- .../FinancialAssistant.module.css | 19 ++++++++- .../financialassistant/FinancialAssistant.tsx | 39 ++++++++----------- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 8c505cb8..a17634a6 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -14,7 +14,8 @@ import { IconChecklist, IconHeadset, IconDots, - IconSubtask + IconSubtask, + IconReportMoney } from "@tabler/icons-react"; import salesLogo from "../../img/logo.png"; import styles from "./Sidebar.module.css"; @@ -110,6 +111,13 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { to: "/organization", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin"] + }, + { + title: "Financial Assistant", + icon: , + to: "/financialassistant", + tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], + roles: ["admin"] } ] }, @@ -204,21 +212,19 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { const accessibleSidebarSections = useMemo(() => { let previousSectionHasItems = false; - + return sidebarSections .map((section, index) => { if (section.divider) { return previousSectionHasItems ? section : null; } - + if (section.items) { const accessibleItems = section.items .map(item => { if (item.links) { - const accessibleLinks = item.links.filter(link => - hasAccess(link.roles, link.tiers) - ); - + const accessibleLinks = item.links.filter(link => hasAccess(link.roles, link.tiers)); + if (accessibleLinks.length > 0) { return { ...item, links: accessibleLinks }; } else { @@ -229,9 +235,9 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { } }) .filter(item => item !== null) as SidebarItemType[]; - + previousSectionHasItems = accessibleItems.length > 0; - + if (accessibleItems.length > 0) { return { ...section, items: accessibleItems }; } diff --git a/frontend/src/pages/financialassistant/FinancialAssistant.module.css b/frontend/src/pages/financialassistant/FinancialAssistant.module.css index e003afbb..3c4313a5 100644 --- a/frontend/src/pages/financialassistant/FinancialAssistant.module.css +++ b/frontend/src/pages/financialassistant/FinancialAssistant.module.css @@ -2,7 +2,7 @@ display: flex; flex-direction: column; width: 100%; - padding: 100px 100px 100px 150px; + padding: 50px 50px 50px 50px; } .row { @@ -51,6 +51,23 @@ justify-content: flex-end; } +.spinnerContainer { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; +} + +.spinner { + font-size: 36px; + width: 150px; + height: 150px; +} + +.messageBarText{ + font-size: 16px; +} + .searchIcon { font-size: 18px; margin-right: 5px; diff --git a/frontend/src/pages/financialassistant/FinancialAssistant.tsx b/frontend/src/pages/financialassistant/FinancialAssistant.tsx index 5cf47566..ec46ae33 100644 --- a/frontend/src/pages/financialassistant/FinancialAssistant.tsx +++ b/frontend/src/pages/financialassistant/FinancialAssistant.tsx @@ -1,16 +1,7 @@ import React, { useEffect, useState } from "react"; import { useAppContext } from "../../providers/AppProviders"; import { getFinancialAssistant, upgradeSubscription, removeFinancialAssistant } from "../../api"; // Asegúrate de importar la función -import { - Spinner, - PrimaryButton, - DefaultButton, - Dialog, - DialogType, - DialogFooter, - MessageBar, - MessageBarType, -} from "@fluentui/react"; +import { Spinner, PrimaryButton, DefaultButton, Dialog, DialogType, DialogFooter, MessageBar, MessageBarType } from "@fluentui/react"; import styles from "./FinancialAssistant.module.css"; const FinancialAssistant = () => { @@ -31,13 +22,13 @@ const FinancialAssistant = () => { const { financial_assistant_active } = await getFinancialAssistant({ user: { ...user, - organizationId: user.organizationId, + organizationId: user.organizationId }, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); setSubscriptionStatus(financial_assistant_active); } catch (error: any) { - console.log(error) + console.log(error); if (error.status === false) { setSubscriptionStatus(false); setError("Financial Assistant feature is not present in this subscription."); @@ -61,6 +52,8 @@ const FinancialAssistant = () => { await upgradeSubscription({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); setSubscriptionStatus(true); setShowSubscribeDialog(false); + //This reloads the page so the financial assistant toggle appears after click + window.location.reload(); } catch { setError("An error occurred while subscribing to the Financial Assistant feature."); } finally { @@ -75,6 +68,8 @@ const FinancialAssistant = () => { await removeFinancialAssistant({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); setSubscriptionStatus(false); setShowUnsubscribeDialog(false); + //This reloads the page so the financial assistant toggle disappears after click + window.location.reload(); } catch { setError("An error occurred while unsubscribing from the Financial Assistant feature."); } finally { @@ -87,7 +82,11 @@ const FinancialAssistant = () => { } if (loading) { - return ; + return ( +
    + +
    + ); } return ( @@ -99,23 +98,17 @@ const FinancialAssistant = () => {
    {subscriptionStatus ? ( <> - + You are subscribed to the Financial Assistant feature. - setShowUnsubscribeDialog(true)} - /> + setShowUnsubscribeDialog(true)} /> ) : ( <> - + You are not subscribed to the Financial Assistant feature. - setShowSubscribeDialog(true)} - /> + setShowSubscribeDialog(true)} /> )}
    From 099c67a52c7819354bba1b72f405e9f6687e75a0 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Mon, 16 Dec 2024 08:35:07 -0400 Subject: [PATCH 239/820] Set Up Cosmos DB for Data Storage and Connection (#214) --- backend/requirements.txt | 2 ++ backend/shared/cosmo_db.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 backend/shared/cosmo_db.py diff --git a/backend/requirements.txt b/backend/requirements.txt index 8fa3be5a..d57c41c9 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,5 @@ +azure-cosmos==4.5.1 +azure-identity==1.15.0 Flask==2.2.2 langchain==0.0.78 flask-cors==3.0.10 diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py new file mode 100644 index 00000000..ded64232 --- /dev/null +++ b/backend/shared/cosmo_db.py @@ -0,0 +1,42 @@ +import os +from azure.cosmos import CosmosClient +from azure.identity import DefaultAzureCredential +import uuid +import logging +from datetime import datetime + + +AZURE_DB_ID = os.environ.get("AZURE_DB_ID") +AZURE_DB_NAME = os.environ.get("AZURE_DB_NAME") +AZURE_DB_URI = f"https://{AZURE_DB_ID}.documents.azure.com:443/" + +def get_cosmos_container(): + try: + credential = DefaultAzureCredential() + client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") + db = client.get_database_client(database=AZURE_DB_NAME) + container = db.get_container_client("reports") + logging.info("Connection to Cosmos DB established successfully.") + return container + except Exception as e: + logging.error(f"Error connecting to Cosmos DB: {e}") + raise + +def create_reports(container, data): + try: + data["id"] = str(uuid.uuid4()) + container.upsert_item(data) + logging.info(f"Document insert: {data}") + except Exception as e: + logging.error(f"Error inserting data into Cosmos DB: {e}") + raise + +def get_reports(container, query): + + try: + items = list(container.query_items(query=query, enable_cross_partition_query=True)) + logging.info(f"Successful query: {len(items)} items found.") + return items + except Exception as e: + logging.error(f"Error querying Cosmos DB: {e}") + raise \ No newline at end of file From 14a1ad2abb26a8a8d7bcba343dd889e8f114133f Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:52:49 -0500 Subject: [PATCH 240/820] FA-289 added endpoint for financial ingestion (#215) * added endpoint for financial ingestion * addressed Manuel's comments --- backend/app.py | 107 ++++++++++++++ backend/app_config.py | 9 ++ backend/financial_doc_processor.py | 182 +++++++++++++++++++++++ backend/requirements.txt | 3 + backend/utils.py | 229 ++++++++++++++++++++++++++++- 5 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 backend/financial_doc_processor.py diff --git a/backend/app.py b/backend/app.py index 39b35f38..aa9286ed 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1965,5 +1965,112 @@ def determine_subscription_tiers(subscription): return tiers +################################################ +# Financial Doc Ingestion +################################################ + +from financial_doc_processor import * +from utils import * +from sec_edgar_downloader import Downloader +from app_config import FILING_TYPES, BASE_FOLDER + +@app.route('/api/SECEdgar/financialdocuments', methods=['GET']) +def process_financial_documents(): + try: + # # Check and install wkhtmltopdf if needed + if not check_and_install_wkhtmltopdf(): + return jsonify({ + "status": "error", + "message": "Failed to install required dependency wkhtmltopdf" + }), 500 + + # Initialize the SEC Edgar Downloader + dl = Downloader( + os.getenv("USER_AGENT_NAME", "SalesFactory"), + os.getenv("USER_AGENT_EMAIL", "nam.tran@salesfactory.com") + ) + # Get parameters from request body + data = request.get_json() + + if not data: + return jsonify({ + "status": "error", + "message": "No data provided" + }), 400 + + # Validate the payload + is_valid, error_message = validate_payload(data) + if not is_valid: + return jsonify({ + "status": "error", + "message": error_message + }), 400 + + equity_ids = data.get('equity_ids', []) # required to provide a list of equity ids + filing_types = data.get('filing_types', FILING_TYPES) # use default if not provided (10-Q, 10-K, 8-K) + + # Download SEC filings + for equity_id in equity_ids: + for filing_type in filing_types: + try: + logger.info(f"Downloading {filing_type} for {equity_id}") + dl.get(filing_type, equity_id, limit=1, download_details=True) + except Exception as e: + logger.error(f"Failed to download {filing_type} for {equity_id}: {str(e)}") + return jsonify({ + "status": "error", + "message": f"Failed to download {filing_type} for {equity_id}: {str(e)}" + }), 500 + + # Collect documents path for all downloaded files + document_paths = collect_filing_documents( + EQUITY_IDS=equity_ids, + FILING_TYPES=filing_types, + get_downloaded_files=get_downloaded_files + ) + + results = {} + # Validate collected documents paths + if validate_document_paths(document_paths): + logger.info("Document collection completed successfully") + + # Upload to blob storage + results = upload_to_blob(document_paths, container_client, base_folder=BASE_FOLDER) + + # Check if all uploads were successful + all_uploads_successful = True + for equity, filings in results.items(): + for filing_type, result in filings.items(): + if result["status"] != "success": + all_uploads_successful = False + break + + # Only cleanup if all uploads were successful + if all_uploads_successful: + if cleanup_resources(): + logger.info("Successfully cleaned up all files in sec-edgar-filings") + else: + logger.error("Failed to clean up files in sec-edgar-filings") + else: + logger.warning("Skipping cleanup as some uploads failed") + + return jsonify({ + "status": "success", + "message": "Documents processed successfully", + "results": results + }), 200 + else: + return jsonify({ + "status": "error", + "message": "Document collection validation failed" + }), 400 + + except Exception as e: + logger.error(f"API execution failed: {str(e)}") + return jsonify({ + "status": "error", + "message": str(e) + }), 500 + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/app_config.py b/backend/app_config.py index 422238db..0006da07 100644 --- a/backend/app_config.py +++ b/backend/app_config.py @@ -25,3 +25,12 @@ # B2C policy configuration B2C_POLICY = SIGNUPSIGNIN_USER_FLOW # Default policy + +# financial ingestion config.py +ALLOWED_FILING_TYPES = ["10-Q", "10-K", "8-K", "DEF 14A"] +FILING_TYPES = ["10-Q", "10-K", "8-K", "DEF 14A"] +BLOB_CONTAINER_NAME = "documents" +BASE_FOLDER = "financial" + + + diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py new file mode 100644 index 00000000..dcb78db9 --- /dev/null +++ b/backend/financial_doc_processor.py @@ -0,0 +1,182 @@ +# document_processor.py +from azure.storage.blob import BlobServiceClient +from utils import convert_html_to_pdf +import os +from pathlib import Path +from collections import defaultdict +from typing import Dict, List +import logging +from app_config import BLOB_CONTAINER_NAME + +# configure logging +logging.basicConfig( + level = logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def get_downloaded_files(equity_id: str, filing_type: str): + filings_dir = os.path.join(os.getcwd(), "sec-edgar-filings", equity_id, filing_type) + # reformat the directory path to be a valid path + + if not os.path.exists(filings_dir): + logger.warning(f"The directory {filings_dir} does not exist") + return None + # Walk through all subdirectories + for root, dirs, files in os.walk(filings_dir): + # Find the primary-document.html file + for file in files: + # Look specifically for primary-document.html + if file == "primary-document.html": + return os.path.join(root, file) + logger.warning(f"No primary-document.html file found for {equity_id} {filing_type}") + return None + +def collect_filing_documents( + EQUITY_IDS: List[str], + FILING_TYPES: List[str], + get_downloaded_files: callable +) -> Dict[str, Dict[str, str]]: + """ + Collect filing documents for multiple equities and filing types and convert to PDF. + """ + if not EQUITY_IDS: + raise ValueError("EQUITY_IDS list cannot be empty") + if not FILING_TYPES: + raise ValueError("FILING_TYPES list cannot be empty") + + document_paths: Dict[str, Dict[str, str]] = defaultdict(dict) + + try: + for equity in EQUITY_IDS: + logger.info(f"Processing equity: {equity}") + + for filing_type in FILING_TYPES: + try: + logger.debug(f"Fetching {filing_type} for {equity}") + html_path = get_downloaded_files(equity, filing_type) + + if html_path: + # Convert HTML path to PDF path + pdf_path = Path(html_path).with_suffix('.pdf') + + # Convert HTML to PDF + success = convert_html_to_pdf( + input_path=html_path, + output_path=pdf_path + ) + + if success: + document_paths[equity][filing_type] = str(pdf_path) + logger.debug(f"Converted and stored PDF for {equity} {filing_type}: {pdf_path}") + else: + logger.warning(f"Failed to convert {filing_type} for {equity}") + else: + logger.warning(f"No {filing_type} document found for {equity}") + + except Exception as e: + logger.error(f"Error processing {filing_type} for {equity}: {str(e)}") + continue + + if not document_paths[equity]: + logger.warning(f"No documents found for equity: {equity}") + + except Exception as e: + logger.error(f"Unexpected error during document collection: {str(e)}") + raise + + return dict(document_paths) + + +blob_connection_string = os.getenv("BLOB_CONNECTION_STRING") +if not blob_connection_string: + raise ValueError("BLOB_CONNECTION_STRING environment variable is not set") +blob_container_name = BLOB_CONTAINER_NAME + +blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string) +container_client = blob_service_client.get_container_client(blob_container_name) + + +def upload_to_blob(document_paths: dict, container_client, base_folder: str = "financial") -> Dict[str, Dict[str, Dict[str, str]]]: + """ + Upload files to Azure Blob Storage with organized folder structure based on filing type. + + Args: + document_paths (dict): Nested dictionary with equity IDs and their filing types + container_client: Azure blob container client + base_folder (str): Base folder name in blob storage + + Returns: + dict: Dictionary of upload results with equity IDs and filing types as keys + """ + upload_results = {} + + for equity, filings in document_paths.items(): + upload_results[equity] = {} + + for filing_type, document_path in filings.items(): + try: + # Construct blob path using the filing type directly + blob_path = f"{base_folder}/{filing_type}/{equity}.pdf" + + # Upload file with determined path + with open(document_path, "rb") as data: + container_client.upload_blob( + name=blob_path, + data=data, + overwrite=True # Overwrite if file exists + ) + + upload_results[equity][filing_type] = { + "status": "success", + "blob_path": blob_path + } + + except Exception as e: + upload_results[equity][filing_type] = { + "status": "failed", + "error": str(e) + } + logger.error(f"Failed to upload {equity} {filing_type}: {str(e)}") + raise + + return upload_results + + +def validate_document_paths(document_paths: Dict[str, Dict[str, str]]) -> bool: + """ + Validate the collected document paths. + + Args: + document_paths (Dict[str, Dict[str, str]]): Collected document paths + + Returns: + bool: True if validation passes, False otherwise + """ + + try: + # Check if any documents were collected + if not document_paths: + logger.error("No documents were collected") + return False + + # Validate each path exists OR path does not end with .pdf + for equity, filings in document_paths.items(): + if not filings: + logger.warning(f"No filings found for equity {equity}") + continue + + for filing_type, path in filings.items(): + logger.info(f"Checking PDF requirements for {equity} {filing_type} ") + if not str(path).lower().endswith('.pdf'): + logger.error(f"file for {equity} {filing_type} is not a PDF: {path}") + logger.info(f"PDF found for {equity}") + + if not Path(path).exists(): + logger.error(f"File not found for {equity} {filing_type}: {path}") + return False + return True + + except Exception as e: + logger.error(f"Error during validation: {str(e)}") + return False diff --git a/backend/requirements.txt b/backend/requirements.txt index d57c41c9..6fd4828a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,3 +16,6 @@ PyJWT==2.8.0 python-jose==3.3.0 ms_identity_python[flask] @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip tenacity==8.5.0 +azure-functions +sec-edgar-downloader==5.0.2 +pdfkit==1.0.0 \ No newline at end of file diff --git a/backend/utils.py b/backend/utils.py index 50e4d547..01e2be28 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -1,6 +1,6 @@ from functools import wraps import logging -from flask import request, jsonify +from flask import request, jsonify, Flask from http import HTTPStatus from typing import Tuple, Dict, Any @@ -61,3 +61,230 @@ def decorated_function(*args, **kwargs): return create_error_response("Missing required client principal ID", HTTPStatus.UNAUTHORIZED) return f(*args, **kwargs) return decorated_function + +################################################ +# Financial Doc Ingestion Utils +################################################ + +# utils.py +import os +import logging +from pathlib import Path +import pdfkit +from typing import Dict, Any, Tuple, Optional, Union +import logging +import shutil +from app_config import ALLOWED_FILING_TYPES + + +# configure logging +logging.basicConfig( + level = logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +logger = logging.getLogger(__name__) + +def validate_payload(data: Dict[str, Any]) -> Tuple[bool, str]: + """ + Validate the request payload. + + Args: + data (dict): The request payload + + Returns: + tuple: (is_valid: bool, error_message: str) + """ + # Check if equity_ids exists and is not empty + if not data.get('equity_ids'): + return False, "equity_ids is required" + + # Check if equity_ids is a list + if not isinstance(data['equity_ids'], list): + return False, "equity_ids must be a list" + + # Check if equity_ids is not empty + if len(data['equity_ids']) == 0: + return False, "equity_ids cannot be empty" + + # Validate filing_types if provided + if 'filing_types' in data: + if not isinstance(data['filing_types'], list): + return False, "filing_types must be a list" + + # Check if all filing types are valid + invalid_types = [ft for ft in data['filing_types'] if ft not in ALLOWED_FILING_TYPES] + if invalid_types: + return False, f"Invalid filing type(s): {', '.join(invalid_types)}. Allowed types are: {', '.join(ALLOWED_FILING_TYPES)}" + + return True, "" + + +def convert_html_to_pdf( + input_path: Union[str, Path], + output_path: Union[str, Path], + options: Optional[Dict] = None +) -> bool: + """ + Convert HTML file to PDF using wkhtmltopdf. + + Args: + input_path (Union[str, Path]): Path to the input HTML file + output_path (Union[str, Path]): Path where the PDF will be saved + wkhtmltopdf_path (Optional[str]): Path to wkhtmltopdf executable + options (Optional[Dict]): Additional options for PDF conversion + + Returns: + bool: True if conversion was successful, False otherwise + + Raises: + FileNotFoundError: If input file or wkhtmltopdf executable doesn't exist + OSError: If there's an error during PDF conversion + Exception: For other unexpected errors + """ + + try: + # Convert paths to Path objects for better path handling + input_path = Path(input_path) + output_path = Path(output_path) + + # Validate input file exists + if not input_path.exists(): + raise FileNotFoundError(f"Input file not found: {input_path}") + + # Create output directory if it doesn't exist + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Default options if none provided + if options is None: + options = { + 'quiet': '', + 'enable-local-file-access': '', + 'encoding': 'UTF-8', + 'no-stop-slow-scripts': '', + 'disable-smart-shrinking': '' + } + + logger.info(f"Converting {input_path} to PDF...") + + # Perform conversion + pdfkit.from_file( + str(input_path), + str(output_path), + options=options + ) + + # Verify the output file was created + if not output_path.exists(): + raise OSError("PDF file was not created") + + logger.info(f"Successfully converted to PDF: {output_path}") + return True + + except FileNotFoundError as e: + logger.error(f"File not found error: {str(e)}") + raise + + except OSError as e: + logger.error(f"PDF conversion error: {str(e)}") + # Clean up partial output file if it exists + if output_path.exists(): + output_path.unlink() + raise + + except Exception as e: + logger.error(f"Unexpected error during PDF conversion: {str(e)}") + # Clean up partial output file if it exists + if output_path.exists(): + output_path.unlink() + raise + + + +def check_and_install_wkhtmltopdf(): + """Check if wkhtmltopdf is installed and configured properly""" + import subprocess + import sys + import os + + try: + # For Windows, add wkhtmltopdf to PATH if not already present + if sys.platform == 'win32': + wkhtmltopdf_path = r'C:\Program Files\wkhtmltopdf\bin' + logger.info(f"Windows detected") + if os.path.exists(wkhtmltopdf_path): + logger.info(f"wkhtmltopdf directory found at {wkhtmltopdf_path}") + if wkhtmltopdf_path not in os.environ['PATH']: + logger.info(f"Adding wkhtmltopdf to PATH: {wkhtmltopdf_path}") + os.environ['PATH'] += os.pathsep + wkhtmltopdf_path + else: + logger.warning(f"wkhtmltopdf directory not found at {wkhtmltopdf_path}") + return install_wkhtmltopdf() + + # Try to run wkhtmltopdf --version + result = subprocess.run( + ['wkhtmltopdf', '--version'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True, + text=True + ) + logger.info(f"wkhtmltopdf is installed and configured. Version: {result.stdout.strip()}") + return True + + except (subprocess.SubprocessError, FileNotFoundError): + logger.warning("wkhtmltopdf not found or not properly configured") + return install_wkhtmltopdf() + except Exception as e: + logger.error(f"Unexpected error checking wkhtmltopdf: {str(e)}") + return False + +def install_wkhtmltopdf(): + """Attempt to install wkhtmltopdf based on the operating system""" + import subprocess + import sys + import webbrowser + + if sys.platform == 'win32': + # For Windows, provide download link and instructions + download_url = "https://wkhtmltopdf.org/downloads.html" + logger.error( + "Automatic installation not supported on Windows. " + "Please install wkhtmltopdf manually:\n" + "1. Download from: " + download_url + "\n" + "2. Install to default location (C:\\Program Files\\wkhtmltopdf)\n" + "3. Add C:\\Program Files\\wkhtmltopdf\\bin to your system PATH" + ) + return False + + elif sys.platform.startswith('linux'): + try: + logger.info("Installing wkhtmltopdf on Linux...") + # Update package list + subprocess.run(['sudo', 'apt-get', 'update'], check=True) + # Install wkhtmltopdf + subprocess.run(['sudo', 'apt-get', 'install', '-y', 'wkhtmltopdf'], check=True) + logger.info("wkhtmltopdf installed successfully") + return True + except subprocess.SubprocessError as e: + logger.error(f"Failed to install wkhtmltopdf: {str(e)}") + return False + else: + logger.error(f"Unsupported operating system: {sys.platform}") + return False + +def cleanup_resources() -> bool: + # Delete all files in the sec-edgar-filings directory + try: + filings_dir = os.path.join(os.getcwd(), "sec-edgar-filings") + if os.path.exists(filings_dir): + logger.info(f"Deleting all files in {filings_dir}") + shutil.rmtree(filings_dir) + logger.info(f"Deleted all files in {filings_dir}") + return True + else: + logger.info(f"No files to delete in {filings_dir} - directory does not exist") + return True + except Exception as e: + logger.error(f"Error during cleanup: {str(e)}") + return False From e07c861a41d9e0083dc031cc55cbac3dd7be2209 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Wed, 18 Dec 2024 06:30:53 -0400 Subject: [PATCH 241/820] FA-283 Build API Endpoints for CRUD Operations on Reports and Sources (#216) * Creation of CRUD for the resource container, as well as a function in api.ts to obtain the reports depending on their type * addressed Manuel's comments --- backend/app.py | 106 +++++++++++++++++++++++ backend/shared/cosmo_db.py | 166 +++++++++++++++++++++++++++++++++---- frontend/src/api/api.ts | 16 ++++ 3 files changed, 273 insertions(+), 15 deletions(-) diff --git a/backend/app.py b/backend/app.py index aa9286ed..7e11496d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -48,6 +48,13 @@ ) import stripe.error +from shared.cosmo_db import( + create_report, + get_report, + update_report, + delete_report, + get_report_by_type, +) load_dotenv() @@ -633,6 +640,105 @@ def deleteChatConversation(chat_id): logging.exception("[webbackend] exception in /delete-chat-conversation") return jsonify({"error": str(e)}), 500 +#get report by id argument +@app.route("/api/reports/", methods=["GET"]) +def getReport(report_id): + """ + Endpoint to get a report by ID. + """ + try: + report = get_report(report_id) + return jsonify(report), 200 + except NotFound as e: + logging.warning(f"Report with id {report_id} not found.") + return jsonify({"error": f"Report with this id {report_id} not found"}), 404 + except Exception as e: + logging.exception(f"An error occurred retrieving the report with id {report_id}") + return jsonify({"error": "Internal Server Error"}), 500 + +#get report by type argument +@app.route("/api/reports", methods=["GET"]) +def getReportsType(): + """ + Endpoint to obtain reports by type. + """ + report_type = request.args.get("type") + if not report_type: + return jsonify({"error": "The 'type' query parameter is required"}), 400 + + try: + reports = get_report_by_type(report_type) + return jsonify(reports), 200 + + except NotFound as e: + logging.warning(f"Reports with type {report_type} not found.") + return jsonify({"error": f"Report of this type {report_type} was not found"}), 404 + except Exception as e: + logging.exception(f"Error retrieving reports with type {report_type}") + return jsonify({"Error retrieving reports with type"}), 500 + +#create report +@app.route("/api/reports", methods=["POST"]) +def createReport(): + """ + Endpoint to create a new report. + """ + try: + data = request.get_json() + + if not data: + return jsonify({"error": "Invalid or missing JSON payload"}), 400 + + new_report = create_report(data) + return jsonify(new_report), 201 + + except Exception as e: + logging.exception("Error creating report") + return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + +#update report +@app.route("/api/reports/", methods=["PUT"]) +def updateReport(report_id): + """ + Endpoint to update a report by ID. + """ + try: + updated_data = request.get_json() + + if updated_data is None: + return jsonify({"error": "Invalid or missing JSON payload"}), 400 + + updated_report = update_report(report_id, updated_data) + return "", 204 + + except NotFound as e: + logging.warning(f"Tried to update a report that doesn't exist: {report_id}") + return jsonify({"error": f"Tried to update a report with this id {report_id} that does not exist"}), 404 + + except Exception as e: + logging.exception(f"Error updating report with ID {report_id}") # Logs the full exception + return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + +#delete report +@app.route("/api/reports/", methods=["DELETE"]) +def deleteReport(report_id): + """ + Endpoint to delete a report by ID. + """ + try: + delete_report(report_id) + + return "",204 + + except NotFound as e: + # If the report does not exist, return 404 Not Found + logging.warning(f"Report with id {report_id} not found.") + return jsonify({"error": f"Report with id {report_id} not found."}), 404 + + except Exception as e: + logging.exception(f"Error deleting report with id {report_id}") + return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + # methods to provide access to speech services and blob storage account blobs diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index ded64232..f953c007 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -1,42 +1,178 @@ import os from azure.cosmos import CosmosClient from azure.identity import DefaultAzureCredential +from azure.cosmos.exceptions import CosmosResourceNotFoundError, AzureError import uuid import logging from datetime import datetime - +from werkzeug.exceptions import NotFound AZURE_DB_ID = os.environ.get("AZURE_DB_ID") AZURE_DB_NAME = os.environ.get("AZURE_DB_NAME") AZURE_DB_URI = f"https://{AZURE_DB_ID}.documents.azure.com:443/" -def get_cosmos_container(): +def get_cosmos_container_report(): + """ + Establishes the connection to the Cosmos DB `reports` container. + """ + credential = DefaultAzureCredential() + client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") + db = client.get_database_client(database=AZURE_DB_NAME) + container = db.get_container_client("reports") + try: - credential = DefaultAzureCredential() - client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") - db = client.get_database_client(database=AZURE_DB_NAME) - container = db.get_container_client("reports") logging.info("Connection to Cosmos DB established successfully.") return container + + except AzureError as az_err: + logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") + raise Exception(f"Azure connection error: {az_err}") from az_err + except Exception as e: - logging.error(f"Error connecting to Cosmos DB: {e}") - raise + logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") + raise Exception(f"Unexpected connection error: {e}") from e -def create_reports(container, data): +def create_report(data): + """ + Creates a new document in the container. + """ try: - data["id"] = str(uuid.uuid4()) + container = get_cosmos_container_report() container.upsert_item(data) - logging.info(f"Document insert: {data}") + data["id"] = str(uuid.uuid4()) + data["generatedAt"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + logging.info(f"Document created: {data}") + return data except Exception as e: logging.error(f"Error inserting data into Cosmos DB: {e}") raise -def get_reports(container, query): +def get_report(report_id): + """ + Retrieves a specific document (report) from the Cosmos DB container using its `id` as partition key. + + Parameters: + report_id (str): The ID of the report to retrieve. + + Returns: + dict: The report document retrieved from the database. + + Raises: + Exception: For any other unexpected error that occurs during retrieval. + CosmosResourceNotFoundError: If the report with the specified ID does not exist in the database. + """ + container = get_cosmos_container_report() try: - items = list(container.query_items(query=query, enable_cross_partition_query=True)) - logging.info(f"Successful query: {len(items)} items found.") + report = container.read_item(item=report_id, partition_key=report_id) + logging.info(f"Report successfully retrieved: {report}") + return report + + except CosmosResourceNotFoundError: + logging.warning(f"Report with id '{report_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving report with id '{report_id}'") + raise + +def get_report_by_type(report_type): + """ + Retrieves documents from the Cosmos DB container using the `type` attribute. + + Parameters: + report_type (str): The type of reports to retrieve. + + Returns: + list: A list of report documents matching the specified type. + + Raises: + CosmosResourceNotFoundError: If no reports with the specified type are found. + Exception: For any other unexpected error that occurs during retrieval. + """ + container = get_cosmos_container_report() + + query = "SELECT * FROM c WHERE c.type = @type" + parameters = [{"name": "@type", "value": report_type}] + + try: + items = list(container.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True + )) + + if not items: + logging.warning(f"No reports found with type '{report_type}'.") + raise NotFound + + logging.info(f"Reports successfully retrieved for type '{report_type}': {items}") return items + + except CosmosResourceNotFoundError: + logging.warning(f"No reports found with type '{report_type}'.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving reports with type '{report_type}': {e}") + raise + +def update_report(report_id, updated_data): + """ + Updates an existing document using its `id` as the partition key. + + Handles database errors and raises exceptions as needed. + """ + container = get_cosmos_container_report() + + try: + current_report = get_report(report_id) + + except CosmosResourceNotFoundError: + logging.warning(f"Report with id '{report_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error while retrieving report with id '{report_id}'") + raise + + try: + current_report.update(updated_data) + + current_report["id"] = report_id + + # Perform the upsert operation + container.upsert_item(current_report) + logging.info(f"Report updated successfully: {current_report}") + return current_report + + except CosmosResourceNotFoundError: + logging.error(f"Failed to upsert item: Report ID '{report_id}' not found during upsert.") + raise NotFound(f"Cannot upsert report because it does not exist with id '{report_id}'") + + except AzureError as az_err: + logging.error(f"AzureError while performing upsert: {az_err}") + raise Exception("Error with Azure Cosmos DB operation.") from az_err + + except Exception as e: + logging.error(f"Unexpected error while updating report with id '{report_id}': {e}") + raise + +def delete_report(report_id): + """ + Deletes a specific document using its `id` as partition key. + """ + container = get_cosmos_container_report() + + try: + container.delete_item(item=report_id, partition_key=report_id) + logging.info(f"Report with id {report_id} deleted successfully.") + return {"message": f"Report with id {report_id} deleted successfully."} + + except CosmosResourceNotFoundError: + logging.warning(f"Report with id '{report_id}' not found in Cosmos DB.") + raise NotFound + except Exception as e: - logging.error(f"Error querying Cosmos DB: {e}") + logging.error(f"Error deleting report with id {report_id}: {e}") raise \ No newline at end of file diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 7fb6d5ee..a4bf1381 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -580,3 +580,19 @@ export async function getInvitations({ user }: any): Promise { return { data: null }; } } + +export async function getReportsByType({ type }: { type: string;}) { + const response = await fetch(`/api/reports?type=${encodeURIComponent(type)}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + } + }); + + if (response.status > 299 || !response.ok) { + throw Error(`Error getting reports of type ${type}`); + } + + const reports = await response.json(); + return reports; +} \ No newline at end of file From df408ec987fbf727395d61fe3d4bf086c4f15cd4 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Wed, 18 Dec 2024 05:32:36 -0500 Subject: [PATCH 242/820] FA 293 endpoint summarization (#217) * summarization endpoint added * fix document retrieval for summarization endpoint * added error handling --- backend/app.py | 166 ++++++++++++- backend/app_config.py | 3 + backend/financial_doc_processor.py | 366 +++++++++++++++++++++++++---- backend/llm_config.py | 92 ++++++++ backend/requirements.txt | 7 +- backend/summarization.py | 118 ++++++++++ 6 files changed, 698 insertions(+), 54 deletions(-) create mode 100644 backend/llm_config.py create mode 100644 backend/summarization.py diff --git a/backend/app.py b/backend/app.py index 7e11496d..b257cc9c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2082,6 +2082,11 @@ def determine_subscription_tiers(subscription): @app.route('/api/SECEdgar/financialdocuments', methods=['GET']) def process_financial_documents(): + # payload example: +# { +# "equity_ids": ["AAPL", "MSFT"], +# "filing_types": ["10-Q", "10-K"] +# } try: # # Check and install wkhtmltopdf if needed if not check_and_install_wkhtmltopdf(): @@ -2134,14 +2139,16 @@ def process_financial_documents(): FILING_TYPES=filing_types, get_downloaded_files=get_downloaded_files ) - + + blob_manager = BlobStorageManager() # from financial_doc_processor + results = {} # Validate collected documents paths if validate_document_paths(document_paths): logger.info("Document collection completed successfully") # Upload to blob storage - results = upload_to_blob(document_paths, container_client, base_folder=BASE_FOLDER) + results = blob_manager.upload_to_blob(document_paths) # Check if all uploads were successful all_uploads_successful = True @@ -2177,6 +2184,159 @@ def process_financial_documents(): "status": "error", "message": str(e) }), 500 - + +# main.py +from app_config import IMAGE_PATH +from summarization import DocumentSummarizer + +@app.route('/api/SECEdgar/financialdocuments/summary', methods=['POST']) +def generate_summary(): + # payload example: + # { + # "equity_name": "MS", + # "financial_type": "10-K" + # } + try: + try: + data = request.get_json() + if not data: + return jsonify({ + 'error': 'Invalid request', + 'details': 'Request body is requred and must be a valid JSON object' + }), 400 + equity_name = data.get('equity_name') + financial_type = data.get('financial_type') + + if not all([equity_name, financial_type]): + return jsonify({ + 'error': 'Missing required fields', + 'details': 'equity_name and financial_type are required' + }), 400 + + if not isinstance(equity_name, str) or not isinstance(financial_type, str): + return jsonify({ + 'error': 'Invalid input type', + 'details': 'equity_name and financial_type must be strings' + }), 400 + + if not equity_name.strip() or not financial_type.strip(): + return jsonify({ + 'error': 'Empty input', + 'details': 'equity_name and financial_type cannot be empty' + }), 400 + + except ValueError as e: + return jsonify({ + 'error': 'Invalid input', + 'details': f"Failed to parse request body: {str(e)}" + }), 400 + + # Initialize components with error handling + try: + blob_manager = BlobStorageManager() + summarizer = DocumentSummarizer() + except ConnectionError as e: + logging.error(f"Failed to connect to blob storage: {e}") + return jsonify({ + 'error': 'Connection error', + 'details': 'Failed to connect to storage service' + }), 503 + except Exception as e: + logging.error(f"Failed to initialize components: {e}") + return jsonify({ + 'error': 'Service initialization failed', + 'details': str(e) + }), 500 + + # Reset directories + try: + reset_local_dirs() + except PermissionError as e: + logging.error(f"Permission error while cleaning up directories: {str(e)}") + return jsonify({ + 'error': 'Permission error', + 'details': 'Failed to clean up directories due to permission issues' + }), 500 + except OSError as e: + logging.error(f"OS error while reseting directories: {str(e)}") + return jsonify({ + 'error': 'System error', + 'details': 'Failed to prepare working directories' + }), 500 + except Exception as e: + logging.error(f"Failed to clean up directories: {e}") + return jsonify({ + 'error': 'Cleanup failed', + 'details': 'Failed to clean up directories to prepare for processing' + }), 500 + + # Download documents + try: + downloaded_files = blob_manager.download_documents(equity_name=equity_name) + if not downloaded_files: + return jsonify({ + 'error': 'No documents found', + 'details': f'No documents found for equity: {equity_name}' + }), 404 + except Exception as e: # some error codes for blob isn't supported in the current version + logging.error(f"Failed to download documents: {str(e)}") + return jsonify({ + 'error': 'Download error', + 'details': 'Failed to download documents from storage service' + }), 503 + # Process documents + for file_path in downloaded_files: + doc_id = extract_pdf_pages_to_images(file_path, IMAGE_PATH) + + # Generate summaries + all_summaries = summarizer.process_document_images(IMAGE_PATH) + final_summary = summarizer.generate_final_summary(all_summaries) + + # Save the summary locally and upload to blob + local_output_path = f'pdf/{financial_type}_{equity_name}_summary.pdf' + save_str_to_pdf(final_summary, local_output_path) + + # Upload summary to blob + document_paths = create_document_paths(local_output_path, equity_name, financial_type) + + # upload to blob and get the blob path/remote links + upload_results = blob_manager.upload_to_blob(document_paths) + + blob_path = upload_results[equity_name][financial_type]['blob_path'] + blob_url = upload_results[equity_name][financial_type]['blob_url'] + + # Clean up local directories + try: + reset_local_dirs() + except Exception as e: + logging.error(f"Failed to clean up directories: {e}") + + return jsonify({ + 'status': 'success', + 'equity_name': equity_name, + 'financial_type': financial_type, + 'blob_path': blob_path, + 'remote_blob_url': blob_url, + 'summary': final_summary, + }), 200 + + except Exception as e: + logging.error(f"Unexpected error: {e}", exc_info=True) + return jsonify({ + 'error': 'Internal server error', + 'details': str(e) + }), 500 + finally: + # Ensure cleanup happens + try: + reset_local_dirs() + except PermissionError as e: + logging.error(f"Permission error while cleaning up directories: {str(e)}") + except OSError as e: + logging.error(f"OS error while reseting directories: {str(e)}") + except Exception as e: + logging.error(f"Failed to clean up: {e}") + + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/app_config.py b/backend/app_config.py index 0006da07..0d99de88 100644 --- a/backend/app_config.py +++ b/backend/app_config.py @@ -32,5 +32,8 @@ BLOB_CONTAINER_NAME = "documents" BASE_FOLDER = "financial" +# Paths in financial summarization +IMAGE_PATH = 'images' +PDF_PATH = './pdf' diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index dcb78db9..1b738deb 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -1,12 +1,34 @@ # document_processor.py -from azure.storage.blob import BlobServiceClient -from utils import convert_html_to_pdf + import os +import logging +import base64 +import uuid +import shutil from pathlib import Path from collections import defaultdict from typing import Dict, List -import logging -from app_config import BLOB_CONTAINER_NAME + +import pandas as pd +import fitz +from dotenv import load_dotenv +from azure.storage.blob import BlobServiceClient, ContentSettings +from reportlab.pdfgen import canvas +from reportlab.lib.pagesizes import letter +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import SimpleDocTemplate, Paragraph +from reportlab.lib.styles import getSampleStyleSheet + +from utils import convert_html_to_pdf +from app_config import BLOB_CONTAINER_NAME, PDF_PATH + + +BLOB_CONNECTION_STRING = os.getenv('BLOB_CONNECTION_STRING') +BLOB_CONTAINER_NAME = os.getenv('BLOB_CONTAINER_NAME') + +# Load environment variables +load_dotenv() # configure logging logging.basicConfig( @@ -97,52 +119,6 @@ def collect_filing_documents( container_client = blob_service_client.get_container_client(blob_container_name) -def upload_to_blob(document_paths: dict, container_client, base_folder: str = "financial") -> Dict[str, Dict[str, Dict[str, str]]]: - """ - Upload files to Azure Blob Storage with organized folder structure based on filing type. - - Args: - document_paths (dict): Nested dictionary with equity IDs and their filing types - container_client: Azure blob container client - base_folder (str): Base folder name in blob storage - - Returns: - dict: Dictionary of upload results with equity IDs and filing types as keys - """ - upload_results = {} - - for equity, filings in document_paths.items(): - upload_results[equity] = {} - - for filing_type, document_path in filings.items(): - try: - # Construct blob path using the filing type directly - blob_path = f"{base_folder}/{filing_type}/{equity}.pdf" - - # Upload file with determined path - with open(document_path, "rb") as data: - container_client.upload_blob( - name=blob_path, - data=data, - overwrite=True # Overwrite if file exists - ) - - upload_results[equity][filing_type] = { - "status": "success", - "blob_path": blob_path - } - - except Exception as e: - upload_results[equity][filing_type] = { - "status": "failed", - "error": str(e) - } - logger.error(f"Failed to upload {equity} {filing_type}: {str(e)}") - raise - - return upload_results - - def validate_document_paths(document_paths: Dict[str, Dict[str, str]]) -> bool: """ Validate the collected document paths. @@ -180,3 +156,293 @@ def validate_document_paths(document_paths: Dict[str, Dict[str, str]]) -> bool: except Exception as e: logger.error(f"Error during validation: {str(e)}") return False + + + +# Create directory if it does not exist +def ensure_directory_exists(directory_path): + path = Path(directory_path) + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + print(f"Directory created: {directory_path}") + else: + print(f"Directory already exists: {directory_path}") + + +# Convert pages from PDF to images +def extract_pdf_pages_to_images(pdf_path, image_dir): + # Validate image_out directory exists + doc_id = str(uuid.uuid4()) + image_out_dir = os.path.join(image_dir, doc_id) + ensure_directory_exists(image_out_dir) + + # Open the PDF file and iterate pages + print ('Extracting images from PDF...') + try: + pdf_document = fitz.open(pdf_path) + except Exception as e: + logger.error(f"Error opening PDF: {str(e)}") + return None + + # get the file name without extension + file_name = os.path.splitext(os.path.basename(pdf_path))[0] + + for page_number in range(len(pdf_document)): + page = pdf_document.load_page(page_number) + image = page.get_pixmap() + image_out_file = os.path.join(image_out_dir, f'{file_name}_{page_number + 1}.png') + image.save(image_out_file) + + pdf_document.close() + return doc_id + +# save the summary to pdf to upload to blob later +def save_str_to_pdf(text: str, output_path: str) -> None: + """ + Save a given text string to a PDF file with full Unicode support using ReportLab. + + Args: + text (str): The text content to be saved in the PDF. + output_path (str): The file path where the PDF will be saved. + + Raises: + Exception: If there is an error during the PDF creation or saving process. + """ + try: + # Create the PDF document + doc = SimpleDocTemplate( + output_path, + pagesize=letter, + rightMargin=72, + leftMargin=72, + topMargin=72, + bottomMargin=72 + ) + + # Create the story (content) + styles = getSampleStyleSheet() + story = [] + + # Add the text as a paragraph + para = Paragraph(text, styles['Normal']) + story.append(para) + + # Build the PDF + doc.build(story) + + logger.info(f"PDF saved successfully to {output_path}") + + except Exception as e: + logger.error(f"Error saving PDF: {str(e)}") + raise + + +def remove_directory(directory_path): + try: + if os.path.exists(directory_path): + shutil.rmtree(directory_path) + print(f"Directory '{directory_path}' has been removed successfully.") + else: + print(f"Directory '{directory_path}' does not exist.") + except Exception as e: + print(f"An error occurred while removing the directory: {e}") + +def reset_local_dirs(): + if os.path.exists('json'): + remove_directory('json') + if os.path.exists('images'): + remove_directory('images') + if os.path.exists('pdf'): + remove_directory('pdf') + +def create_document_paths(output_path: str, equity_name: str, financial_type: str) -> dict: + """ + Create a document paths dictionary structure compatible with upload_to_blob function. + + Args: + output_path (str): Path to the document (e.g., 'pdf/10-K_AAPL_summary.pdf') + equity_name (str): Name of the equity (e.g., 'AAPL') + financial_type (str): Type of financial document (e.g., '10-K') + + Returns: + dict: Nested dictionary structure for upload_to_blob function + + Example: + >>> path = 'pdf/10-K_AAPL_summary.pdf' + >>> create_document_paths(path, 'AAPL', '10-K') + { + 'AAPL': { + '10-K': 'pdf/10-K_AAPL_summary.pdf' + } + } + """ + return { + equity_name: { + financial_type: output_path + } + } + +class BlobStorageError(Exception): + """Base exception for blob storage operations""" + pass + +class BlobConnectionError(BlobStorageError): + """Failed to connect to blob storage""" + pass + +class BlobAuthenticationError(BlobStorageError): + """Authentication failed for blob storage""" + pass + +class BlobNotFoundError(BlobStorageError): + """Blob not found in storage""" + pass + +class BlobUploadError(BlobStorageError): + """Failed to upload blob""" + pass + +class BlobDownloadError(BlobStorageError): + """Failed to download blob""" + pass + +class BlobStorageManager: + def __init__(self): + try: + self.blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONNECTION_STRING) + self.container_client = self.blob_service_client.get_container_client(BLOB_CONTAINER_NAME) + self.blob_base_folder = 'financial' + except ValueError as e: + raise BlobConnectionError(f"Invalid connection string: {str(e)}") + except Exception as e: + raise BlobConnectionError(f"Failed to initialize blob storage: {str(e)}") + + def download_documents(self, equity_name: str, + financial_type: str = '10-K', + exclude_summary: bool = True, + local_data_path: str = PDF_PATH) -> List[str]: + """ + Download documents from blob storage. + + Args: + equity_name (str): Name of the equity + financial_type (str): Type of financial document + exclude_summary (bool): Whether to exclude summary documents + local_data_path (str): Local path to save documents + + Returns: + List[str]: List of downloaded file paths + + Raises: + BlobAuthenticationError: If authentication fails + BlobNotFoundError: If no documents are found + BlobDownloadError: If download fails + OSError: If local file operations fail + """ + downloaded_files = [] + try: + # Create local directory + try: + os.makedirs(local_data_path, exist_ok=True) + except OSError as e: + raise OSError(f"Failed to create local directory: {str(e)}") + + base_path = f"{self.blob_base_folder}/{financial_type}" + + try: + # List all blobs + all_blobs = list(self.container_client.list_blobs(name_starts_with=base_path)) + except Exception as e: + raise BlobNotFoundError(f"Failed to list blobs: {str(e)}") + + # Filter for exact equity name matches + import re + equity_pattern = re.compile( + f"{re.escape(base_path)}/{re.escape(equity_name)}(_summary)?\.pdf$" + ) + + filtered_blobs = [ + blob for blob in all_blobs + if equity_pattern.match(blob.name) and + (not exclude_summary or "_summary" not in blob.name) + ] + + if not filtered_blobs: + raise BlobNotFoundError(f"No matching documents found for {equity_name}") + + logger.info(f"Found {len(filtered_blobs)} matching documents for {equity_name}") + + for blob in filtered_blobs: + try: + logger.info(f'Downloading {blob.name}') + blob_client = self.container_client.get_blob_client(blob.name) + file_name = f'{financial_type}_{os.path.basename(blob.name)}' + local_file_path = os.path.join(local_data_path, file_name) + + with open(local_file_path, "wb") as file: + data = blob_client.download_blob() + file.write(data.readall()) + + downloaded_files.append(local_file_path) + logger.info(f"Successfully downloaded: {file_name}") + except OSError as e: + logger.error(f"Error downloading {blob.name}: {str(e)}") + raise OSError(f"Failed to write file {local_file_path}: {str(e)}") + except Exception as e: + logger.error(f"Error downloading {blob.name}: {str(e)}") + raise BlobDownloadError(f"Failed to download {blob.name}: {str(e)}") + + except Exception as e: + logger.error(f"Error in blob storage operations: {str(e)}") + raise + + return downloaded_files + + # make sure the document_paths is a dict with the structure of create_document_paths + def upload_to_blob(self, document_paths: dict) -> Dict: + """ + Upload files to Azure Blob Storage with organized folder structure based on filing type. + + Args: + document_paths (dict): Nested dictionary with equity IDs and their filing types + container_client: Azure blob container client + base_folder (str): Base folder name in blob storage + + Returns: + dict: Dictionary of upload results with equity IDs and filing types as keys + """ + if not isinstance(document_paths, dict): + raise ValueError("document_paths must be a dictionary") + + upload_results = {} + for equity, filings in document_paths.items(): + upload_results[equity] = {} + for filing_type, document_path in filings.items(): + try: + if not os.path.exists(document_path): + raise FileNotFoundError(f"File not found: {document_path}") + + blob_path = f"{self.blob_base_folder}/{filing_type}/{equity}_summary.pdf" if "summary" in document_path else f"{self.blob_base_folder}/{filing_type}/{equity}.pdf" + + with open(document_path, "rb") as data: + try: + self.container_client.upload_blob( + name=blob_path, + data=data, + overwrite=True, + content_settings=ContentSettings(content_type='application/pdf') + ) + except Exception as e: + raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") + + # get the blob url for the uploaded file + blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" + upload_results[equity][filing_type] = {"status": "success", + "blob_path": blob_path, + "blob_url": blob_url} + logger.info(f'Document has been uploaded to {blob_path}') + except Exception as e: + upload_results[equity][filing_type] = {"status": "failed", "error": str(e)} + logger.error(f"Failed to upload {equity} {filing_type}: {str(e)}") + return upload_results + diff --git a/backend/llm_config.py b/backend/llm_config.py new file mode 100644 index 00000000..8ad3f9f6 --- /dev/null +++ b/backend/llm_config.py @@ -0,0 +1,92 @@ +# llm_config.py +from pydantic import BaseModel, Field +from typing import Dict, Optional +import json +from openai import AzureOpenAI +import os +from dotenv import load_dotenv + +load_dotenv() + +class LLMConfig(BaseModel): + api_base: str = Field(default=os.getenv('OPENAI_API_BASE')) + api_key: str = Field(default=os.getenv('OPENAI_API_KEY')) + api_version: str = Field(default=os.getenv('OPENAI_API_VERSION')) + model_name: str = Field(default=os.getenv('OPENAI_GPT_MODEL')) + + class Config: + frozen = True # Makes the config immutable + +class PromptTemplate(BaseModel): + image_analysis: str = Field( + default=""" + You are a professional document analyst tasked with creating clear, concise summaries. + + Guidelines: + - Focus on key information, main points, and essential details + - Use clear, professional language + - Maintain factual accuracy and objectivity + - Present information directly without meta-commentary + - Write in complete, well-structured sentences + - Exclude phrases like "this image shows" or "I can see" + - Make sure you capture all important financial figures + - Limit to 4-6 impactful sentences + + Format your response as a straightforward summary without any introductory or concluding remarks. + """ + ) + + final_summary: str = Field( + default=""" + You are a professional financial analyst tasked with synthesizing multiple document sections into one cohesive summary. + + Guidelines: + - Create a flowing narrative that connects key points logically + - Identify and emphasize the most significant themes and findings + - Maintain chronological or logical order where appropriate + - Avoid repetition and redundant information + - Use transitional phrases to connect related ideas + - Ensure consistency in terminology and tone + - Focus on the broader picture while including crucial details + - Write in a clear, professional style + + Your summary should read as a single, unified document rather than a collection of separate points. + """ + ) + + class Config: + frozen = True + +class LLMManager: + def __init__(self): + self.prompts = PromptTemplate() + self._clients: Dict[str, AzureOpenAI] = {} + self.config: Dict[str, LLMConfig] = { + "gpt4o": LLMConfig( + api_base=os.getenv('OPENAI_API_BASE'), + api_key=os.getenv('OPENAI_API_KEY'), + api_version=os.getenv('OPENAI_API_VERSION'), + model_name=os.getenv('OPENAI_GPT_MODEL') + ), + "embedding": LLMConfig( + api_base=os.getenv('OPENAI_API_BASE'), + api_key=os.getenv('OPENAI_API_KEY'), + api_version=os.getenv('OPENAI_API_VERSION'), + model_name=os.getenv('OPENAI_EMBEDDING_MODEL') + ) + } + + def get_client(self, client_type: str = "gpt4o") -> AzureOpenAI: + """Get or create an Azure OpenAI client""" + if client_type not in self._clients: + config = self.config[client_type] + self._clients[client_type] = AzureOpenAI( + api_key=config.api_key, + api_version=config.api_version, + base_url=f"{config.api_base}/openai/deployments/{config.model_name}" + ) + return self._clients[client_type] + + def get_prompt(self, prompt_type: str) -> str: + """Get a prompt template by type""" + return getattr(self.prompts, prompt_type) \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 6fd4828a..cb7cea4d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,4 +18,9 @@ ms_identity_python[flask] @ https://github.com/azure-samples/ms-identity-python/ tenacity==8.5.0 azure-functions sec-edgar-downloader==5.0.2 -pdfkit==1.0.0 \ No newline at end of file +pdfkit==1.0.0 +openai<=1.57.4 +tiktoken<=0.8.0 +pandas<=2.2.3 +PyMuPDF==1.23.7 +reportlab==4.2.5 \ No newline at end of file diff --git a/backend/summarization.py b/backend/summarization.py new file mode 100644 index 00000000..f4f31624 --- /dev/null +++ b/backend/summarization.py @@ -0,0 +1,118 @@ +# summarization.py +import base64 +import logging +import os +from pathlib import Path +import pandas as pd +from llm_config import LLMManager + +logger = logging.getLogger(__name__) + +class DocumentSummarizer: + def __init__(self): + self.llm_manager = LLMManager() + self.gpt_client = self.llm_manager.get_client("gpt4o") + self.image_sys_prompt = self.llm_manager.get_prompt("image_analysis") + self.final_summary_prompt = self.llm_manager.get_prompt("final_summary") + + def encode_image(self, image_path: str) -> str: + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode('utf-8') + + def summarize_image(self, image_path: str) -> str: + """ + Summarize the content of an image using the GPT model. + + This method encodes the image to a base64 string, sends it to the GPT model + along with a system prompt and user message, and returns the generated summary. + + Args: + image_path (str): The file path to the image to be summarized. + + Returns: + str: The summary of the image content generated by the GPT model. + """ + base64_image = self.encode_image(image_path) + response = self.gpt_client.chat.completions.create( + model=self.llm_manager.config["gpt4o"].model_name, + messages=[ + {"role": "system", "content": self.image_sys_prompt}, + { + "role": "user", + "content": [ + {"type": "text", "text": "Please describe what you see in this image in 2-3 sentences."}, + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}} + ] + } + ], + max_tokens=300 + ) + return response.choices[0].message.content + + def process_document_images(self, image_base_dir: str) -> str: + """ + Process images in the specified directory to generate summaries. + + This function iterates through all subdirectories within the given base directory, + processes each image file (assumed to be in PNG format), and generates a summary + for each image using the `summarize_image` method. The summaries are then compiled + into a single string, ordered by page number. + + Args: + image_base_dir (str): The base directory containing subdirectories of images to process. + + Returns: + str: A concatenated string of summaries for all processed images, ordered by page number. + """ + summaries = [] + file_names = [] + page_numbers = [] + + image_base_dir = Path(image_base_dir) + for folder_path in image_base_dir.iterdir(): + if folder_path.is_dir(): + logger.info(f"Processing folder: {folder_path.name}") + for img_file in folder_path.glob("*.png"): + try: + summary = self.summarize_image(str(img_file)) + file_name = img_file.name.split('.')[0] + page_num = int(file_name.split('_')[-1]) + + summaries.append(summary) + file_names.append(file_name) + page_numbers.append(page_num) + + logger.info(f"Processed: {img_file.name}") + except Exception as e: + logger.error(f"Error processing {img_file.name}: {str(e)}") + + summary_df = pd.DataFrame({ + 'page_number': page_numbers, + 'file_name': file_names, + 'summary': summaries + }).sort_values('page_number') + + return "\n".join(summary_df['summary'].tolist()) + + def generate_final_summary(self, all_summaries: str) -> str: + """ + Generate a final summary from all individual summaries. + + This function uses the GPT4o model to generate a comprehensive summary + from the concatenated string of all individual summaries. + + Args: + all_summaries (str): A concatenated string of summaries for all processed images. + + Returns: + str: The final summary generated by the GPT model. + """ + response = self.gpt_client.chat.completions.create( + model=self.llm_manager.config["gpt4o"].model_name, + messages=[ + {"role": "system", "content": self.final_summary_prompt}, + {"role": "user", "content": [{"type": "text", "text": all_summaries}]} + ], + max_tokens=1500 + ) + return response.choices[0].message.content \ No newline at end of file From e858631534945c59408ff7fd336f74c259c09b41 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:43:50 -0500 Subject: [PATCH 243/820] FA-290 Fetching Multiple Source Data (#218) Co-authored-by: Manuel Castro --- backend/app.py | 80 +++++++++++++++++++++- backend/requirements.txt | 5 +- backend/tavily_tool.py | 141 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 backend/tavily_tool.py diff --git a/backend/app.py b/backend/app.py index b257cc9c..166b62fb 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2185,7 +2185,84 @@ def process_financial_documents(): "message": str(e) }), 500 -# main.py + +from tavily_tool import TavilySearch + +@app.route('/api/web-search', methods = ['POST']) +def web_search(): + """ + Endpoint for multiple web search + + Expected Json body: + { + "query": "search query", //required + "mode": "news" or "general", default is "news" //required + "max_results": optional int, default = 2 //optional + "include_domains": optional list of strings, default = None //optional + } + """ + try: + data = request.get_json() + + # validate reuqired fields: + if not data or 'query' not in data: + logger.error("Missing required field: 'query'") + return jsonify({ + 'error': "Missing required field: 'query'" + }), 400 + + # get optional parameters + mode = data.get('mode', 'news') + max_results = data.get('max_results', 2) + if not isinstance(max_results, int) or max_results < 1: + logger.error(f"Invalid max_results: {max_results}") + return jsonify({ + 'error': "Invalid max_results. Please provide a positive integer." + }), 400 + include_domains = data.get('include_domains', None) + if include_domains is not None and not isinstance(include_domains, list): + logger.error(f"Invalid include_domains: {include_domains}") + return jsonify({ + 'error': "Invalid include_domains. Please provide a list of strings." + }), 400 + + # initialize searcher + logger.info("Initializing TavilySearch") + try: + searcher = TavilySearch(max_results=max_results, + include_domains=include_domains) + except ValueError as e: + logger.error(f"Error initializing TavilySearch: {e}") + return jsonify({ + 'error': f"Invalid configuration: {str(e)}" + }), 400 + + # perform search based on mode. If mode is not provided, default to news + if mode.lower() == 'news': + logger.info("Performing news search") + results = searcher.search_news(data['query']) + elif mode.lower() == 'general': + logger.info("Performing general search") + results = searcher.search_general(data['query']) + else: + logger.error("Invalid mode. Please use 'news' or 'general'.") + return jsonify({ + 'error': "Invalid mode. Please use 'news' or 'general'." + }), 400 + + # format results + logger.info("Formatting search results") + formatted_results = searcher.format_result(results) + + logger.info("Search completed successfully") + return jsonify(formatted_results) + + except Exception as e: + logger.error(f"An error occurred: {e}") + return jsonify({ + 'error': "An error occurred while processing the request." + }), 500 + from app_config import IMAGE_PATH from summarization import DocumentSummarizer @@ -2338,5 +2415,6 @@ def generate_summary(): logging.error(f"Failed to clean up: {e}") + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/requirements.txt b/backend/requirements.txt index cb7cea4d..94c0c694 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,8 +19,11 @@ tenacity==8.5.0 azure-functions sec-edgar-downloader==5.0.2 pdfkit==1.0.0 +pydantic<=2.10.3 +tavily-python==0.5.0 openai<=1.57.4 tiktoken<=0.8.0 pandas<=2.2.3 PyMuPDF==1.23.7 -reportlab==4.2.5 \ No newline at end of file +reportlab==4.2.5 + diff --git a/backend/tavily_tool.py b/backend/tavily_tool.py new file mode 100644 index 00000000..6528a766 --- /dev/null +++ b/backend/tavily_tool.py @@ -0,0 +1,141 @@ +from tavily import TavilyClient +import os +import logging +from dotenv import load_dotenv +load_dotenv() + + +logging.basicConfig( + level = logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class TavilySearch: + """A wrapper class for the Tavily API client that provides search functionality. + + This class provides methods to search for both news and general information + using the Tavily API, with built-in error handling and result formatting. + + Args: + api_key (str, optional): Tavily API key. Defaults to TAVILY_API_KEY environment variable. + max_results (int, optional): Maximum number of results to return. Defaults to 2. + include_domains (list[str], optional): List of domains to include in search. Defaults to empty list. + + Example: + >>> from tavily_tool import TavilySearch + >>> searcher = TavilySearch(max_results=3) + >>> results = searcher.search_news("AI developments") + >>> formatted = searcher.format_result(results) + """ + def __init__(self, + api_key: str = os.environ.get("TAVILY_API_KEY"), + max_results: int = 2, + include_domains: list[str] = None): + "Initialize Tavily client" + if not api_key: + logger.error("TAVILY_API_KEY is not set in the environment variables") + raise ValueError("TAVILY_API_KEY is required") + self.api_key = api_key + try: + self.client = TavilyClient(api_key=self.api_key) + except Exception as e: + logger.error(f"Error initializing Tavily client: {str(e)}") + raise ValueError("Failed to initialize Tavily client") + + self.max_results = max_results + self.include_domains = include_domains + + def search_news(self, query: str) -> str: + "Conduct Tavily Search for recent news" + if not query: + logger.error("Query is required") + return {"error": "Search query cannot be empty"} + logger.info(f"Conducting news search for query: {query}") + try: + response = self.client.search( + query = query, + search_depth = "advanced", + max_results = self.max_results, + topic = "news", + days = 30, + include_domains = self.include_domains + ) + logger.info("News search completed successfully") + return response + except Exception as e: + logger.error(f"Error conducting news search: {str(e)}") + return {"error": f"Error conducting news search: {str(e)}"} + + def search_general(self, query: str) -> str: + "Conduct search for general information" + if not query: + logger.error("Query is required") + return {"error": "Search query cannot be empty"} + logger.info(f"Conducting general search for query: {query}") + try: + response = self.client.search( + query = query, + search_depth = "advanced", + max_results = self.max_results, + topic = "general", + days = 30, + include_domains = self.include_domains + ) + logger.info("General search completed successfully") + return response + except Exception as e: + logger.error(f"Error conducting general search: {str(e)}") + return {"error": f"Error conducting general search: {str(e)}"} + + def format_result(self, response: dict) -> str: + """ + Format Tavily search results into an organized dictionary. + + Args: + response (dict): Tavily API response dictionary + + Returns: + str: Formatted dict of search results + """ + # check if the response is a dictionary + if not isinstance(response, dict): + error_msg = f"Invalid response type: expected dict, got {type(response)}" + logger.error(error_msg) + return {"error": error_msg} + + try: + if not response.get('results'): + logger.warning("No results found in the response") + return {'error': 'No results found'} + formatted_results = { + "query": response.get("query", ""), + "results": [] + } + + # Format each result + for result in response.get("results", []): + # Just use the raw date string + date_str = result.get("published_date", "") + + # build the result dictionary + result_dict = { + "title": result.get("title", ""), + "date": date_str, + "url": result.get("url", ""), + "content": result.get("content", "No content available") + } + formatted_results["results"].append(result_dict) + logger.debug("Search results formatted successfully") + return formatted_results + + except Exception as e: + logger.error(f"Error formatting search results: {str(e)}") + return {"error": f"Error formatting search results: {str(e)}"} + +if __name__ == "__main__": + tavily_search = TavilySearch(max_results=3, include_domains=["techcrunch.com", "bloomberg.com", "reuters.com"]) + news_query = "Google AI chatbot" + news_results = tavily_search.search_general(news_query) + print(tavily_search.format_result(news_results)) + From 23bf85c1103a5bef82016b54f034a88a9b82250a Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 19 Dec 2024 08:26:36 -0500 Subject: [PATCH 244/820] bump langchain to v3 in backend (#220) --- .gitignore | 7 ++++++- backend/langchainadapters.py | 5 +++-- backend/requirements.previous.txt | Bin 0 -> 1270 bytes backend/requirements.txt | 22 +++++++++++----------- 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 backend/requirements.previous.txt diff --git a/.gitignore b/.gitignore index af1eb595..7516aa7e 100644 --- a/.gitignore +++ b/.gitignore @@ -154,4 +154,9 @@ static/ settings.json -flask_session/ \ No newline at end of file +flask_session/ + +# locking some dependencies +pyproject.toml +poetry.lock + diff --git a/backend/langchainadapters.py b/backend/langchainadapters.py index 74c1a81e..d40cb6c1 100644 --- a/backend/langchainadapters.py +++ b/backend/langchainadapters.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional -from langchain.callbacks.base import BaseCallbackHandler -from langchain.schema import AgentAction, AgentFinish, LLMResult +from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.outputs import LLMResult +from langchain_core.agents import AgentAction, AgentFinish def ch(text: str) -> str: s = text if isinstance(text, str) else str(text) diff --git a/backend/requirements.previous.txt b/backend/requirements.previous.txt new file mode 100644 index 0000000000000000000000000000000000000000..01da5d6871e4f0af8cd54524eae50c2a986e7f5c GIT binary patch literal 1270 zcmai!-EPxR5QOI%iFe=uaN4w#7DNz8s22#NB9MB4s>;nzV;aZ7PC?1T1K*qtv0W}u zaiZFi!3xDVyU$Xt_|1`C?kzO zO!#vxh-q|}PLFsVm$`hh)GZn3{44lxhQky)(5cQ6!)(tG+G^O0JKo`jyG)m8smZhD z_a0Vb`uSYnch06TtamJdI7_;$X+<4t^i-(HsITJAHU3C^t}gWbuNM!`?_k-$l9PV{ zXGZ=hm0OdwJj7dJ#c56@p2`hT72nZgs~+k_QsYRYYdW|B8wjgxr!a=cU3RXD&w_~8 zAdPSzkM&fq(%thtXFIt}pY=%}^p?0_DREX zy;*i*_#+&Xk!M1ERuT^V3BvAhcJF|@8ob^%XJ2tLV>g^T{L%HXXEPx>HhGK_VspG% z_D1N>skk7|hOFIbny>G+igWLbciLRuniKYUz`G>43bfdX-il?yn4#+ytFfkLuf~1~ mE%w<*I&`6TI^WlGl$?B;GI%O0_-nHGEfag=H(5B=D*gZ?9>z5Q literal 0 HcmV?d00001 diff --git a/backend/requirements.txt b/backend/requirements.txt index 94c0c694..d7ba64d2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,22 +1,22 @@ +langchain==0.3.13 +langchain-core==0.3.27 azure-cosmos==4.5.1 azure-identity==1.15.0 -Flask==2.2.2 -langchain==0.0.78 +flask==2.2.2 flask-cors==3.0.10 -Werkzeug==2.2.2 +werkzeug==2.2.2 requests==2.28.2 python-dotenv==1.0.0 -azure-identity -azure-keyvault-secrets +azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 stripe==10.5.0 cachetools>=5.3.3,<5.6.0 -Flask-SQLAlchemy==2.5.1 -PyJWT==2.8.0 +flask-sqlalchemy==2.5.1 +pyjwt==2.8.0 python-jose==3.3.0 -ms_identity_python[flask] @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip +ms-identity-python @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip#sha256=a665d05ef0ecec78a746cda9e138d795cf257f0d58827e1ebdedb5e06e9d7f19 tenacity==8.5.0 -azure-functions +azure-functions==1.21.3 sec-edgar-downloader==5.0.2 pdfkit==1.0.0 pydantic<=2.10.3 @@ -24,6 +24,6 @@ tavily-python==0.5.0 openai<=1.57.4 tiktoken<=0.8.0 pandas<=2.2.3 -PyMuPDF==1.23.7 +pymupdf==1.23.7 reportlab==4.2.5 - +Flask-Session==0.8.0 \ No newline at end of file From ac98a8a6bbe2f28b3df2f5b094ec3fc7bc36284c Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 19 Dec 2024 11:36:47 -0400 Subject: [PATCH 245/820] Revert "bump langchain to v3 in backend (#220)" (#222) This reverts commit 23bf85c1103a5bef82016b54f034a88a9b82250a. --- .gitignore | 7 +------ backend/langchainadapters.py | 5 ++--- backend/requirements.previous.txt | Bin 1270 -> 0 bytes backend/requirements.txt | 22 +++++++++++----------- 4 files changed, 14 insertions(+), 20 deletions(-) delete mode 100644 backend/requirements.previous.txt diff --git a/.gitignore b/.gitignore index 7516aa7e..af1eb595 100644 --- a/.gitignore +++ b/.gitignore @@ -154,9 +154,4 @@ static/ settings.json -flask_session/ - -# locking some dependencies -pyproject.toml -poetry.lock - +flask_session/ \ No newline at end of file diff --git a/backend/langchainadapters.py b/backend/langchainadapters.py index d40cb6c1..74c1a81e 100644 --- a/backend/langchainadapters.py +++ b/backend/langchainadapters.py @@ -1,7 +1,6 @@ from typing import Any, Dict, List, Optional -from langchain_core.callbacks import BaseCallbackHandler -from langchain_core.outputs import LLMResult -from langchain_core.agents import AgentAction, AgentFinish +from langchain.callbacks.base import BaseCallbackHandler +from langchain.schema import AgentAction, AgentFinish, LLMResult def ch(text: str) -> str: s = text if isinstance(text, str) else str(text) diff --git a/backend/requirements.previous.txt b/backend/requirements.previous.txt deleted file mode 100644 index 01da5d6871e4f0af8cd54524eae50c2a986e7f5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmai!-EPxR5QOI%iFe=uaN4w#7DNz8s22#NB9MB4s>;nzV;aZ7PC?1T1K*qtv0W}u zaiZFi!3xDVyU$Xt_|1`C?kzO zO!#vxh-q|}PLFsVm$`hh)GZn3{44lxhQky)(5cQ6!)(tG+G^O0JKo`jyG)m8smZhD z_a0Vb`uSYnch06TtamJdI7_;$X+<4t^i-(HsITJAHU3C^t}gWbuNM!`?_k-$l9PV{ zXGZ=hm0OdwJj7dJ#c56@p2`hT72nZgs~+k_QsYRYYdW|B8wjgxr!a=cU3RXD&w_~8 zAdPSzkM&fq(%thtXFIt}pY=%}^p?0_DREX zy;*i*_#+&Xk!M1ERuT^V3BvAhcJF|@8ob^%XJ2tLV>g^T{L%HXXEPx>HhGK_VspG% z_D1N>skk7|hOFIbny>G+igWLbciLRuniKYUz`G>43bfdX-il?yn4#+ytFfkLuf~1~ mE%w<*I&`6TI^WlGl$?B;GI%O0_-nHGEfag=H(5B=D*gZ?9>z5Q diff --git a/backend/requirements.txt b/backend/requirements.txt index d7ba64d2..94c0c694 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,22 +1,22 @@ -langchain==0.3.13 -langchain-core==0.3.27 azure-cosmos==4.5.1 azure-identity==1.15.0 -flask==2.2.2 +Flask==2.2.2 +langchain==0.0.78 flask-cors==3.0.10 -werkzeug==2.2.2 +Werkzeug==2.2.2 requests==2.28.2 python-dotenv==1.0.0 -azure-keyvault-secrets==4.9.0 +azure-identity +azure-keyvault-secrets azure-storage-blob==12.19.0 stripe==10.5.0 cachetools>=5.3.3,<5.6.0 -flask-sqlalchemy==2.5.1 -pyjwt==2.8.0 +Flask-SQLAlchemy==2.5.1 +PyJWT==2.8.0 python-jose==3.3.0 -ms-identity-python @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip#sha256=a665d05ef0ecec78a746cda9e138d795cf257f0d58827e1ebdedb5e06e9d7f19 +ms_identity_python[flask] @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip tenacity==8.5.0 -azure-functions==1.21.3 +azure-functions sec-edgar-downloader==5.0.2 pdfkit==1.0.0 pydantic<=2.10.3 @@ -24,6 +24,6 @@ tavily-python==0.5.0 openai<=1.57.4 tiktoken<=0.8.0 pandas<=2.2.3 -pymupdf==1.23.7 +PyMuPDF==1.23.7 reportlab==4.2.5 -Flask-Session==0.8.0 \ No newline at end of file + From 870c39b46e1745f9fd7b6121457f551d5556c135 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 19 Dec 2024 12:08:39 -0400 Subject: [PATCH 246/820] Revert "Revert "bump langchain to v3 in backend (#220)" (#222)" (#223) This reverts commit ac98a8a6bbe2f28b3df2f5b094ec3fc7bc36284c. --- .gitignore | 7 ++++++- backend/langchainadapters.py | 5 +++-- backend/requirements.previous.txt | Bin 0 -> 1270 bytes backend/requirements.txt | 22 +++++++++++----------- 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 backend/requirements.previous.txt diff --git a/.gitignore b/.gitignore index af1eb595..7516aa7e 100644 --- a/.gitignore +++ b/.gitignore @@ -154,4 +154,9 @@ static/ settings.json -flask_session/ \ No newline at end of file +flask_session/ + +# locking some dependencies +pyproject.toml +poetry.lock + diff --git a/backend/langchainadapters.py b/backend/langchainadapters.py index 74c1a81e..d40cb6c1 100644 --- a/backend/langchainadapters.py +++ b/backend/langchainadapters.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional -from langchain.callbacks.base import BaseCallbackHandler -from langchain.schema import AgentAction, AgentFinish, LLMResult +from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.outputs import LLMResult +from langchain_core.agents import AgentAction, AgentFinish def ch(text: str) -> str: s = text if isinstance(text, str) else str(text) diff --git a/backend/requirements.previous.txt b/backend/requirements.previous.txt new file mode 100644 index 0000000000000000000000000000000000000000..01da5d6871e4f0af8cd54524eae50c2a986e7f5c GIT binary patch literal 1270 zcmai!-EPxR5QOI%iFe=uaN4w#7DNz8s22#NB9MB4s>;nzV;aZ7PC?1T1K*qtv0W}u zaiZFi!3xDVyU$Xt_|1`C?kzO zO!#vxh-q|}PLFsVm$`hh)GZn3{44lxhQky)(5cQ6!)(tG+G^O0JKo`jyG)m8smZhD z_a0Vb`uSYnch06TtamJdI7_;$X+<4t^i-(HsITJAHU3C^t}gWbuNM!`?_k-$l9PV{ zXGZ=hm0OdwJj7dJ#c56@p2`hT72nZgs~+k_QsYRYYdW|B8wjgxr!a=cU3RXD&w_~8 zAdPSzkM&fq(%thtXFIt}pY=%}^p?0_DREX zy;*i*_#+&Xk!M1ERuT^V3BvAhcJF|@8ob^%XJ2tLV>g^T{L%HXXEPx>HhGK_VspG% z_D1N>skk7|hOFIbny>G+igWLbciLRuniKYUz`G>43bfdX-il?yn4#+ytFfkLuf~1~ mE%w<*I&`6TI^WlGl$?B;GI%O0_-nHGEfag=H(5B=D*gZ?9>z5Q literal 0 HcmV?d00001 diff --git a/backend/requirements.txt b/backend/requirements.txt index 94c0c694..d7ba64d2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,22 +1,22 @@ +langchain==0.3.13 +langchain-core==0.3.27 azure-cosmos==4.5.1 azure-identity==1.15.0 -Flask==2.2.2 -langchain==0.0.78 +flask==2.2.2 flask-cors==3.0.10 -Werkzeug==2.2.2 +werkzeug==2.2.2 requests==2.28.2 python-dotenv==1.0.0 -azure-identity -azure-keyvault-secrets +azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 stripe==10.5.0 cachetools>=5.3.3,<5.6.0 -Flask-SQLAlchemy==2.5.1 -PyJWT==2.8.0 +flask-sqlalchemy==2.5.1 +pyjwt==2.8.0 python-jose==3.3.0 -ms_identity_python[flask] @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip +ms-identity-python @ https://github.com/azure-samples/ms-identity-python/archive/refs/heads/0.9.zip#sha256=a665d05ef0ecec78a746cda9e138d795cf257f0d58827e1ebdedb5e06e9d7f19 tenacity==8.5.0 -azure-functions +azure-functions==1.21.3 sec-edgar-downloader==5.0.2 pdfkit==1.0.0 pydantic<=2.10.3 @@ -24,6 +24,6 @@ tavily-python==0.5.0 openai<=1.57.4 tiktoken<=0.8.0 pandas<=2.2.3 -PyMuPDF==1.23.7 +pymupdf==1.23.7 reportlab==4.2.5 - +Flask-Session==0.8.0 \ No newline at end of file From e1e5dbb566a4cde6eb4cab36fe8ff84bdf85c670 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 19 Dec 2024 11:12:02 -0500 Subject: [PATCH 247/820] fix sudo issue for pdfkit (#221) --- backend/utils.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/backend/utils.py b/backend/utils.py index 01e2be28..c446ac0b 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -243,10 +243,10 @@ def install_wkhtmltopdf(): """Attempt to install wkhtmltopdf based on the operating system""" import subprocess import sys - import webbrowser + import platform if sys.platform == 'win32': - # For Windows, provide download link and instructions + # Windows installation code remains the same download_url = "https://wkhtmltopdf.org/downloads.html" logger.error( "Automatic installation not supported on Windows. " @@ -260,15 +260,39 @@ def install_wkhtmltopdf(): elif sys.platform.startswith('linux'): try: logger.info("Installing wkhtmltopdf on Linux...") - # Update package list - subprocess.run(['sudo', 'apt-get', 'update'], check=True) - # Install wkhtmltopdf - subprocess.run(['sudo', 'apt-get', 'install', '-y', 'wkhtmltopdf'], check=True) + + # Try to determine the package manager + if subprocess.run(['which', 'apt-get'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: + # Debian/Ubuntu + install_cmd = ['apt-get', 'install', '-y', 'wkhtmltopdf'] + elif subprocess.run(['which', 'yum'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: + # CentOS/RHEL + install_cmd = ['yum', 'install', '-y', 'wkhtmltopdf'] + else: + logger.error("Could not determine package manager. Please install wkhtmltopdf manually.") + return False + + # Try to install without sudo first + try: + subprocess.run(install_cmd, check=True) + except subprocess.CalledProcessError: + # If that fails, try with sudo if available + if subprocess.run(['which', 'sudo'], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0: + install_cmd.insert(0, 'sudo') + subprocess.run(install_cmd, check=True) + else: + logger.error("Installation requires root privileges. Please install wkhtmltopdf manually.") + return False + logger.info("wkhtmltopdf installed successfully") return True + except subprocess.SubprocessError as e: logger.error(f"Failed to install wkhtmltopdf: {str(e)}") return False + except Exception as e: + logger.error(f"Unexpected error during installation: {str(e)}") + return False else: logger.error(f"Unsupported operating system: {sys.platform}") return False From 11eb1e2460619756bb5dca96462190ba0be74032 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:29:48 -0500 Subject: [PATCH 248/820] improve error handling and fix document upload/download (#224) --- backend/app.py | 17 ++++------------- backend/financial_doc_processor.py | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/backend/app.py b/backend/app.py index 166b62fb..76970803 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2348,19 +2348,10 @@ def generate_summary(): }), 500 # Download documents - try: - downloaded_files = blob_manager.download_documents(equity_name=equity_name) - if not downloaded_files: - return jsonify({ - 'error': 'No documents found', - 'details': f'No documents found for equity: {equity_name}' - }), 404 - except Exception as e: # some error codes for blob isn't supported in the current version - logging.error(f"Failed to download documents: {str(e)}") - return jsonify({ - 'error': 'Download error', - 'details': 'Failed to download documents from storage service' - }), 503 + + downloaded_files = blob_manager.download_documents(equity_name=equity_name, + financial_type=financial_type) + # Process documents for file_path in downloaded_files: doc_id = extract_pdf_pages_to_images(file_path, IMAGE_PATH) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 1b738deb..dd440ec5 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -318,7 +318,7 @@ def __init__(self): raise BlobConnectionError(f"Failed to initialize blob storage: {str(e)}") def download_documents(self, equity_name: str, - financial_type: str = '10-K', + financial_type: str, exclude_summary: bool = True, local_data_path: str = PDF_PATH) -> List[str]: """ From 03a9fcbc69eb17e144d7aa525ed9619c0859e29c Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Fri, 20 Dec 2024 06:37:43 -0500 Subject: [PATCH 249/820] add filter date range in the payload (#227) --- backend/app.py | 11 ++++++++--- backend/tavily_tool.py | 30 +++++++++++++++++++----------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/backend/app.py b/backend/app.py index 76970803..187547c9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2198,13 +2198,14 @@ def web_search(): "query": "search query", //required "mode": "news" or "general", default is "news" //required "max_results": optional int, default = 2 //optional - "include_domains": optional list of strings, default = None //optional + "include_domains": optional list of strings, default = None // + "days": optional int, default = 30 // } """ try: data = request.get_json() - # validate reuqired fields: + # validate required fields: if not data or 'query' not in data: logger.error("Missing required field: 'query'") return jsonify({ @@ -2225,12 +2226,16 @@ def web_search(): return jsonify({ 'error': "Invalid include_domains. Please provide a list of strings." }), 400 + + days = data.get('days', 30) + # initialize searcher logger.info("Initializing TavilySearch") try: searcher = TavilySearch(max_results=max_results, - include_domains=include_domains) + include_domains=include_domains, + days = days) except ValueError as e: logger.error(f"Error initializing TavilySearch: {e}") return jsonify({ diff --git a/backend/tavily_tool.py b/backend/tavily_tool.py index 6528a766..2e3ebc93 100644 --- a/backend/tavily_tool.py +++ b/backend/tavily_tool.py @@ -31,6 +31,7 @@ class TavilySearch: def __init__(self, api_key: str = os.environ.get("TAVILY_API_KEY"), max_results: int = 2, + search_days: int = 30, include_domains: list[str] = None): "Initialize Tavily client" if not api_key: @@ -43,8 +44,23 @@ def __init__(self, logger.error(f"Error initializing Tavily client: {str(e)}") raise ValueError("Failed to initialize Tavily client") - self.max_results = max_results + # Validate include_domains + if include_domains is not None: + if not isinstance(include_domains, list): + logger.error("include_domains must be a list of strings") + raise ValueError("include_domains must be a list of strings") + if not all(isinstance(domain, str) for domain in include_domains): + logger.error("All domains in include_domains must be strings") + raise ValueError("All domains in include_domains must be strings") self.include_domains = include_domains + + # max results and search days have to be greater than 0 + if max_results <= 0: + logger.warning("Max results must be greater than 0, setting to 2") + self.max_results = 2 + if search_days <= 0: + logger.warning("Search days must be greater than 0, setting to 30") + self.search_days = 30 def search_news(self, query: str) -> str: "Conduct Tavily Search for recent news" @@ -58,7 +74,7 @@ def search_news(self, query: str) -> str: search_depth = "advanced", max_results = self.max_results, topic = "news", - days = 30, + days = self.search_days, include_domains = self.include_domains ) logger.info("News search completed successfully") @@ -79,7 +95,6 @@ def search_general(self, query: str) -> str: search_depth = "advanced", max_results = self.max_results, topic = "general", - days = 30, include_domains = self.include_domains ) logger.info("General search completed successfully") @@ -131,11 +146,4 @@ def format_result(self, response: dict) -> str: except Exception as e: logger.error(f"Error formatting search results: {str(e)}") - return {"error": f"Error formatting search results: {str(e)}"} - -if __name__ == "__main__": - tavily_search = TavilySearch(max_results=3, include_domains=["techcrunch.com", "bloomberg.com", "reuters.com"]) - news_query = "Google AI chatbot" - news_results = tavily_search.search_general(news_query) - print(tavily_search.format_result(news_results)) - + return {"error": f"Error formatting search results: {str(e)}"} \ No newline at end of file From f00cebe9f120be8a0c969827d95946e1ff12cc15 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Fri, 20 Dec 2024 09:25:52 -0400 Subject: [PATCH 250/820] FA-306 update cosmos db container and implement post endpoint for curation and companny sumarization reports (#225) * Update Cosmos DB Container and Implement POST Endpoint for curation and compannySumarization Reports * edited comments * Add create report curation function in api.ts * Endpoint to get all reports from the "reports" container * Addressing Manuel's comments --- backend/app.py | 103 +++++++++++++++++++++++++++---------- backend/shared/cosmo_db.py | 26 ++++++---- frontend/src/api/api.ts | 32 ++++++++++-- 3 files changed, 119 insertions(+), 42 deletions(-) diff --git a/backend/app.py b/backend/app.py index 187547c9..d4463bb5 100644 --- a/backend/app.py +++ b/backend/app.py @@ -53,7 +53,7 @@ get_report, update_report, delete_report, - get_report_by_type, + get_filtered_reports ) load_dotenv() @@ -640,7 +640,7 @@ def deleteChatConversation(chat_id): logging.exception("[webbackend] exception in /delete-chat-conversation") return jsonify({"error": str(e)}), 500 -#get report by id argument +#get report by id argument from Container Reports @app.route("/api/reports/", methods=["GET"]) def getReport(report_id): """ @@ -656,28 +656,7 @@ def getReport(report_id): logging.exception(f"An error occurred retrieving the report with id {report_id}") return jsonify({"error": "Internal Server Error"}), 500 -#get report by type argument -@app.route("/api/reports", methods=["GET"]) -def getReportsType(): - """ - Endpoint to obtain reports by type. - """ - report_type = request.args.get("type") - if not report_type: - return jsonify({"error": "The 'type' query parameter is required"}), 400 - - try: - reports = get_report_by_type(report_type) - return jsonify(reports), 200 - - except NotFound as e: - logging.warning(f"Reports with type {report_type} not found.") - return jsonify({"error": f"Report of this type {report_type} was not found"}), 404 - except Exception as e: - logging.exception(f"Error retrieving reports with type {report_type}") - return jsonify({"Error retrieving reports with type"}), 500 - -#create report +#create Reports curation and companySummarization container Reports @app.route("/api/reports", methods=["POST"]) def createReport(): """ @@ -688,15 +667,60 @@ def createReport(): if not data: return jsonify({"error": "Invalid or missing JSON payload"}), 400 + + # Validate the 'name' field + if "name" not in data: + return jsonify({"error": "Field 'name' is required"}), 400 + + # Validate the 'type' field + if "type" not in data: + return jsonify({"error": "Field 'type' is required"}), 400 + + if data["type"] not in ["curation", "companySummarization"]: + return jsonify({"error": "Invalid 'type'. Must be 'curation' or 'companySummarization'"}), 400 + + # Validate fields according to type + if data["type"] == "companySummarization": + required_fields = ["reportTemplate", "companyTickers"] + missing_fields = [field for field in required_fields if field not in data] + + if missing_fields: + return jsonify({"error": f"Missing required fields: {', '.join(missing_fields)}"}), 400 + + # Validate 'reportTemplate' + valid_templates = ["10-K", "10-Q", "8-K", "DEF 14A"] + if data["reportTemplate"] not in valid_templates: + return jsonify({"error": f"'reportTemplate' must be one of: {', '.join(valid_templates)}"}), 400 + + elif data["type"] == "curation": + required_fields = ["category"] + missing_fields = [field for field in required_fields if field not in data] + + if missing_fields: + return jsonify({"error": f"Missing required fields: {', '.join(missing_fields)}"}), 400 + + # Validate 'category' + valid_categories = ["Ecommerce", "Weekly Economic", "Monthly Economic"] + if data["category"] not in valid_categories: + return jsonify({"error": f"'category' must be one of: {', '.join(valid_categories)}"}), 400 + + # Validar the 'status' field + if "status" not in data: + return jsonify({"error": "Field 'status' is required"}), 400 + valid_statuses = ["active", "archived"] + if data["status"] not in valid_statuses: + return jsonify({"error": f"'status' must be one of: {', '.join(valid_statuses)}"}), 400 + + # Delegate report creation new_report = create_report(data) return jsonify(new_report), 201 - + except Exception as e: logging.exception("Error creating report") return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 -#update report +#update Reports curation and companySummarization container Reports @app.route("/api/reports/", methods=["PUT"]) def updateReport(report_id): """ @@ -719,7 +743,7 @@ def updateReport(report_id): logging.exception(f"Error updating report with ID {report_id}") # Logs the full exception return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 -#delete report +#delete report from Container Reports @app.route("/api/reports/", methods=["DELETE"]) def deleteReport(report_id): """ @@ -740,6 +764,31 @@ def deleteReport(report_id): return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + + +@app.route("/api/reports", methods=["GET"]) +def getFilteredType(): + """ + Endpoint to obtain reports by type or retrieve all reports if no type is specified. + """ + report_type = request.args.get("type") + + try: + if report_type: + reports = get_filtered_reports(report_type) + else: + reports = get_filtered_reports() + + return jsonify(reports), 200 + + except NotFound as e: + logging.warning(f"No reports found for type '{report_type}'.") + return jsonify({"error": f"No reports found for type '{report_type}'."}), 404 + + except Exception as e: + logging.exception(f"Error retrieving reports.") + return jsonify({"error": "Internal Server Error"}), 500 + # methods to provide access to speech services and blob storage account blobs diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index f953c007..7dcd7001 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -38,9 +38,10 @@ def create_report(data): """ try: container = get_cosmos_container_report() - container.upsert_item(data) data["id"] = str(uuid.uuid4()) - data["generatedAt"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + data["createAt"] = datetime.utcnow().isoformat() + "Z" + data["updatedAt"] = datetime.utcnow().isoformat() + "Z" + container.upsert_item(data) logging.info(f"Document created: {data}") return data except Exception as e: @@ -76,25 +77,28 @@ def get_report(report_id): logging.error(f"Unexpected error retrieving report with id '{report_id}'") raise -def get_report_by_type(report_type): +def get_filtered_reports(report_type=None): """ - Retrieves documents from the Cosmos DB container using the `type` attribute. + Retrieves documents from the Cosmos DB container using the `type` attribute or returns all reports. Parameters: - report_type (str): The type of reports to retrieve. + report_type (str, optional): The type of reports to retrieve. If None, retrieves all reports. Returns: - list: A list of report documents matching the specified type. + list: A list of report documents. Raises: - CosmosResourceNotFoundError: If no reports with the specified type are found. + CosmosResourceNotFoundError: If no reports with the specified type are found (when filtered). Exception: For any other unexpected error that occurs during retrieval. """ container = get_cosmos_container_report() + if report_type: + query = "SELECT * FROM c WHERE c.type = @type" + parameters = [{"name": "@type", "value": report_type}] + else: + query = "SELECT * FROM c" + parameters = [] - query = "SELECT * FROM c WHERE c.type = @type" - parameters = [{"name": "@type", "value": report_type}] - try: items = list(container.query_items( query=query, @@ -103,7 +107,7 @@ def get_report_by_type(report_type): )) if not items: - logging.warning(f"No reports found with type '{report_type}'.") + logging.warning(f"No reports found.") raise NotFound logging.info(f"Reports successfully retrieved for type '{report_type}': {items}") diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index a4bf1381..fa0f9693 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -581,16 +581,40 @@ export async function getInvitations({ user }: any): Promise { } } -export async function getReportsByType({ type }: { type: string;}) { - const response = await fetch(`/api/reports?type=${encodeURIComponent(type)}`, { +//create report type "curation" or "companySummarization" +export async function createReport(reportData: object) { + const response = await fetch(`/api/reports`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(reportData), + }); + + if (response.status > 299 || !response.ok) { + throw Error("Error creating a new report"); + } + + const newReport = await response.json(); + return newReport; +} + +//This function, if sent with the "type" parameter, receives a request with the required report. If nothing is sent, it will receive all the reports from the container. +export async function getFilteredReports(type?: string) { + const url = type + ? `/api/reports?type=${encodeURIComponent(type)}` + : `/api/reports`; + + const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", - } + }, }); if (response.status > 299 || !response.ok) { - throw Error(`Error getting reports of type ${type}`); + const errorType = type ? `type ${type}` : "all reports"; + throw new Error(`Error getting reports for ${errorType}`); } const reports = await response.json(); From 8a32c9316f74a64aab1cd365a2134e9474fca56d Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:41:01 +0000 Subject: [PATCH 251/820] fix requests dependency conflicts with jupyterlab --- backend/app.py | 4 ++-- backend/requirements.txt | 2 +- backend/tavily_tool.py | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/app.py b/backend/app.py index d4463bb5..08d38442 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2276,7 +2276,7 @@ def web_search(): 'error': "Invalid include_domains. Please provide a list of strings." }), 400 - days = data.get('days', 30) + search_days = data.get('search_days', 30) # initialize searcher @@ -2284,7 +2284,7 @@ def web_search(): try: searcher = TavilySearch(max_results=max_results, include_domains=include_domains, - days = days) + search_days = search_days) except ValueError as e: logger.error(f"Error initializing TavilySearch: {e}") return jsonify({ diff --git a/backend/requirements.txt b/backend/requirements.txt index d7ba64d2..db99eeac 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,7 @@ azure-identity==1.15.0 flask==2.2.2 flask-cors==3.0.10 werkzeug==2.2.2 -requests==2.28.2 +requests python-dotenv==1.0.0 azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 diff --git a/backend/tavily_tool.py b/backend/tavily_tool.py index 2e3ebc93..9b05d4d8 100644 --- a/backend/tavily_tool.py +++ b/backend/tavily_tool.py @@ -58,9 +58,14 @@ def __init__(self, if max_results <= 0: logger.warning("Max results must be greater than 0, setting to 2") self.max_results = 2 + else: + self.max_results = max_results + if search_days <= 0: logger.warning("Search days must be greater than 0, setting to 30") self.search_days = 30 + else: + self.search_days = search_days def search_news(self, query: str) -> str: "Conduct Tavily Search for recent news" From 97015b72019915f0504d56331b18fe22d6dbc699 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 23 Dec 2024 01:40:33 -0500 Subject: [PATCH 252/820] FA-294 Curation Report --- .../Curation_Report_Agent.png | Bin 0 -> 31974 bytes backend/app.py | 93 ++++ backend/curation_report_generator.py | 458 ++++++++++++++++++ .../curation_report_config.py | 10 + .../curation_report_tools/__init__.py | 0 .../curation_report_tools/web_search.py | 150 ++++++ .../curation_report_utils.py | 27 ++ backend/financial_doc_processor.py | 157 +++++- backend/llm_config.py | 36 +- backend/prompts/curation_reports/__init__.py | 0 backend/prompts/curation_reports/ecommerce.py | 204 ++++++++ backend/prompts/curation_reports/general.py | 65 +++ .../curation_reports/monthly_economics.py | 3 + .../curation_reports/weekly_economics.py | 3 + backend/requirements.txt | 10 +- 15 files changed, 1191 insertions(+), 25 deletions(-) create mode 100644 backend/Agent_Graph_Images/Curation_Report_Agent.png create mode 100644 backend/curation_report_generator.py create mode 100644 backend/financial_agent_utils/curation_report_config.py create mode 100644 backend/financial_agent_utils/curation_report_tools/__init__.py create mode 100644 backend/financial_agent_utils/curation_report_tools/web_search.py create mode 100644 backend/financial_agent_utils/curation_report_utils.py create mode 100644 backend/prompts/curation_reports/__init__.py create mode 100644 backend/prompts/curation_reports/ecommerce.py create mode 100644 backend/prompts/curation_reports/general.py create mode 100644 backend/prompts/curation_reports/monthly_economics.py create mode 100644 backend/prompts/curation_reports/weekly_economics.py diff --git a/backend/Agent_Graph_Images/Curation_Report_Agent.png b/backend/Agent_Graph_Images/Curation_Report_Agent.png new file mode 100644 index 0000000000000000000000000000000000000000..6843d7defda528c33e39cafc7df4433ed3fe671f GIT binary patch literal 31974 zcma%jWk6J2)GpTRh=2n~2oeK=bV;WUAYIZe-Jo=b4j>^NA_CIg-69Rr-Q68B^xTc_ z`+nd3ZhelhIZSN1+ zI1-0~O3|_6T~aML#d$T7tS8a*v<7rqet|>@xt;y}D%7=4ML#qhg!$1h<28E9pP!hi zd#a@Sg_NMj7W_2IIwDl2XjO9?85u00c<)jy5)gp5s#00GP_~ys!D1Y#9edjw;XWg@ zcE~oTY)V`_Ih94Ekrk%hTiMl@<9bn6#*(R_#IzxRD5UkK_Juq{bOhD5dXem#d`xx_J44F72^`P)R+319ig z&(f^D$od9`6)GG?$!o**U|jWw<9LhYmUZ*4)SJ{2R|gzOY-gJ zq~}a!9br*-9-K|@a3|wDEOi>90AH2)qTs^A>vrtEZas2a48uagQ;Q;6a_TKV6wdEq zEGL(bc;RA50A~yf)3E;btJJ~Fw>F=sBk$pwu&rpX8#WjX?>&QM8B(?#_AIB%9_B1;?-lpFi%28V|Hu}ID@&+OjJUMufdDa{Eo zS|kV1KNT1MeEE&8bT1lOtTtViuzgzl_44GeH_9gKaU~@~Z{0}1DQ)6%D=y)=kuW2(=qpYD}uCLGL zdFd=CC+B|G%ggJ<%}*(%SjZ=l6pxFO&qN%XrIlQ66S0qs%eT_ZClaQ%6;-&cTpK@Y zxdf>BPEq9Q&oy|wfBzl^gSiU|F)@Yo80ThXG3Yk%GcYhbd)6{Lt0N`#&;~zIz-@JH zEi*Y8n~aPMAK%!}(9p{2v)uB@&J;zs=~CP0m6a80YHHnv`iblu)~>)-Mph%=I}ct# zk`MI*0*UIWGQNd}HyM|H{P^(+Mq80y^S+OYiV6g#sHmu^spt6LHYsVZap{(uTdAUQW-OkhhUsG9y$WHdrwUHw3>ba?_gCQlZNX1E zJ3CtpsHv!W;$E~54GoQrsZxYPe90b3j*pLbEFE~TM6n=VA&FnX&9mM;5p@{><9q%* zQzoACf$vXmZ|{}v*q~MiC+*#tT7t)qm6Vms4Lc(^EJm|(bEVSXoaN5(kV29pZnJ9Q zofAM@)j5=^jt?VZHkG(}cz$n=Z0#>J6LVP)4-Lh0zh8`9+McbS>hEWvp!m@AHVh}A zFN#2l-C=%G_^y>WW(=M7ls9cb4CmYFw(Oi7IAp-IjMbpc)WV|C{R{`zqF)S!S65db z4xm4L$jHce;0y*69UJR4>F2JqLtta$XwRFGaj!8EG8q;+{`7q6xlxQ`Po39MM>yqJ zp)T_9u&d|hXJrLRXd#$Z~)t@Zf+S+O} zTNmDGcv^8`(%{Y)*=gDKHJ2`3;*pe=*e3fDJ03w9jAL>ltHHwOPN$!rpR%$tc+%j{ zpD`g2LOw?ZIHb8~fB&7b@`%IMXab)TlaP?t=5TgMNQlK)fp)2}OkbjaYN1X-Y^)RN z`ieL6Y-K9Ga@9$$dvd}(^JC`leXyaGlqLK2I*${KUpm5;r9SlxS@f}O{e;y*CB1c2 zFpa2X-nO{!tKMp|lF`!A+I(7&ovkh}zrXU%#AMB=JBGs|&f!yVF#r3-me}TMEo%vh z&e&$K$VPHix8lFDl(Ow8yCmu5UtZ&3-nSBG3>Tup)ymbL+bA0u8IhEf)WtF>o%s5d z(NjQ0K|w)Xo$}nD&~Ia7V+6Tbo$w^#A}ikU0~m$4IHR03zKUx1_Ic$%H%93j5jhmC zEiof`Efn2{@wWyqa->5Rx4b8pvPF4tNK@z~zu-+A`~J3`j{q@YhG$4{Sl^l_OZ>YO zdqP$qf-JhD^PT5@lr^+k!&Y3(s5LWF{^jBUgwM(<(5d|p3@u7NY>P17+3OKBh~ zGSVYIuAllHD;#}Zz#nt?fPhxpy*fRSh#P|HR>@fN=L^ zDLqWGgHy*FziqNM$)DsshwUZC7-a@TkVbK1uEaa}Qho&a2lH@2rZv}%YA-R`3_Yss zZdcAG1|A2mY646pEH}7?!py9=Do>3ex+C$~<9Ll+HTxy|^@7)q+URIFgWT%0c-3S% z`)fP{>uROCIp@b4igtE(m)kSOErvtmPiu0Bgtg=KCMC*yaVcIUki&-AEXRu&H7f|8 zH0h}3crX1&I)pPC8~1qiG3_&g4;`-d5SK`ymA`~%iOOJ}{CM;58A2m4D5#U^*6{5I z$@nBBRbIDm&Qd*p?!S%mk}(WwUR+j&xHB6CPRFFBJ>%~7dGmL!m6v)b@aQi46L5vE zSAPZ0g9wl%3PM`NFzr1YIOEybSy8$yZ5R~o6S!V$OUqqX^*86H9*aZD z)zsAbP4|=KA?}33hv&a7vXvwq@#ZPPnU})CbW#!)>P#MOKj+hm2%x31-^!a@SIJ1MjtlexMi~U5uyHR{LF6l`9*3p zoMlS1jE^JL9(;f7$EFgc%9tIa;kb4}D84!**C{PNnS0n0bY|^yjzP7)?Nc+;7$Iiz z-Lt{H1f6^*H!hl@#(}fXt`6362U>)j?=~D4dJn%jHFZYb@Z)`!cV6|Kd-`17u?H0? zWnaF@a+^Jna8oE~Ql+rieOcUZs^)2o$5TpeRAAJK!zf_Hd*@vB`>Qg0_13eIv|nGp z?a}xp58fWPyGN8$^CnQd#{4hO)?4I%zuG_R=w^|j{l5-`*+fJ~$Hhgza>SES(9rmL ze7?dZYj`PwnnRtI$I+$Gh>H(-bcmCYa37E4t{D;xK8~cmuT|&lw@vrhJB2zeE32p~ zt(7cK+C88|gu(j6;!9(*=)fsMPEJSlG<-pRQRg|P{h94_a`T0)ZOLTZS3UNx1utz5 zZ0ND!aq3SrE3KXPSrpXc%*^yS^nSAEVkJP~dO>SMPrkAc23H+rwvLR{yZ(;ju)F2o z+?46Kx*cH9o_cg>Ywz&;{xS0DhEPnWAwC`+7V$1Vo6+djvTCKZc4<+OMy2tV&&S%u zCFZqd<0EJHN64n8SBmTQuEr{_`<;al{Uvh*48o@KFZb>p#o|0xOk!TE;?Z~VSgDuw zq5#qMW?_1x(~^M!*&Wq~%*?bR#K?YO04laPmZYsB;yA#_g{m-I)PYT!!Y-!3J! zwGF=eBddyq*a(xn>~eB)HnkLVugGa+^>8BIZB6EINY&rJIN2V!7!O!~E=MYp#7O{q z_IS1D3s2LJAEvJB{20vzT0BA)CuuvHk18xo{Ghy!{56faVyA(h%7&e`I2qOs%7in1 z!67>+yR`${fo6|8($bTSUP-pc<37ia%pMY4hV;?%x`u}KefqssF|=XNn{Yk z$U~rcd0BP9|HQW1{F?H%i^iqs!`MQ|BVw|rgFT}`_ubGFeY8}ZIxL!-BpXJgE?Razzhb5iz?@T#alLk(xYG|<8wO#hLW!s1@ z6pkWa$tV4Mw)`t@DI;TNtydiZEh#A>Wwq^3XhFy(@R9}3=4U6Dd+u;vs`kwE(7&55 zs(P4dg6I{7JRu@tzWn_NqnTGjeNOySqZ7)pBTC)HrP^Qr#E(&9VcG^w@I9%Vn?Xz0 zM)J|V5TC;)znOlP`>1B>b(ByNC*_N)wjO5`wog}AR}4M?lLiJuy$MzassTZ;{f(!M z8am#nD(i|9Nb;_uK%3mLkh^nZP^so|vH&}gRnMk4lYbC&(RSpgDx>G+soq$DwEEh zQR-{=2g1}NZ#<^>qN=*KYPf_lVO2lu zlP2$XJ3nTHuQz=ZDORha;I|ZYCcMU(<>>3~SvWX_!1!uOlX`2ulcRn#X(M;%n$|vf z-P;n;!tjw>6PKSg*-IHPJvphDO%l`qP;UL;qxH68Hzy}5I(n`_20uV#b(I7PKf4DP zYj22r-g9bis64`ZUL}b^m96vvinF=~L;O?~SvMW4nFwrX&{J2Zg@0<*@4mmf_VBrX zuwJP$b#PKnp-q+3-Y9Y-`#0XizP{km#?uE~Zn1}}rChc=T5-}UupAnC`VslDOYU#E zeq3UwNtP8|surf?Sm0&kWYM53otssoEgalyLp6(?&>w&ufuZ^+S0E63V0wjMQ$Nu&=32Oo!1 zwu@R$`yzkuY`gM%oS!NWK0Nz^hlke^xjEW%tGQ^XSd92EW^oaYi79ibCW zlcs(Mdv=MM+sME>VqA%iikYr^Vq-9a2Q##qSs7Tm4@wfRjkeE?Q25>WCzkk{G`}n? zt=QtW7t>3%u*O=dNZ{h?64lEnWHoLcjc*2vRmTCY|Me9zu@R+)(M*W(+ST=IT--f4 zjp_6#oLZFRhB zU*UM;v#=~1N=}V-cX9hv%0~GzK08H@ukh!s!aPdl5R(3S$zN2GldF9SzeM7bV z?(|OKsT8X}(yrQkG@+)<#zt0taBi#sY$xhShpY1uFS3~=HW%vnp4X7Lc&Dm&2hzi% zn2}!JUKOPfm7gl5vOL&$JF#&ZAbj{)710mfTd?!gF#T>rSnaY`L4pm!gFd0Lp56+d z<+5rfKCcxhHs+5vQjxnes|TkZ7ggp*4x3L)b?2^^+GKf2+!#1dzN9KPW_4WAt1=3b zxUqAaqWkIT=`C1{G6e-|WTvKWZ7vR!=@V&^`Zs?Ln0WrH&tk6Ez4h>O0T{0%CoyLK2|h1^qgD7yzf z_4$}A=ur|oYgt;_>0o;4t0ijOye#UxTbtx-y`w8s5zQ8Zmuuw~!?n+F9(Y!L&&e@w zFX$;QZfDZHTGOKG*vpl7&!4a9da1vDEm}TZ z9a*Grq?W)eYCVgb=}yLPJ@tbjq@|~vDvImR$66N`DM&--raF1v{Sqk&GS6z&qt9)1 zm{y9s>Xg;NnV6j1o5ogLlltu-IE5v;Y2?Eh)S?sfb9?9B4MP5JK@RvtSR|AP(`F_F&Lqj$}}W3!GD)d!w-1xkk6qLP3f=Kh{XEy;H*K7%%5L zCQh`}c#eI;EuI9dL7YK12Wg!XrAQ7Z+g{IhiDxCvxkC&)Biuw? z8m`jALfQE=NGK;jrsbVtX;0LJMQum{5t-@aK|TINlAXU=->-MQfcL9HPr zB7%^oc~*HU_M$54`sMh1$em7S@|k&$<)0_Z+E z_~k!bZVDMtvmBRX8TEYSc&CG({&ztp_6#TdpIam*CK3@5{h5`Wn`J=^XS_h}F#}@h zpNG^pFp!dz{CAE3J;A}jadW#SM?j5!$4tft({fWby_HHZzbPJ__un$bO5GGc<5=L{ zWw2q}-fj)ujHlHu{6Q>q7rAAUo0cZ#2(!ZaoY7)z;l)$8t20_^!l%<1 zqVVR;8B@7WD5(&>!}|AM`md(0wI$MVs1>`LmpWpZM`rxU5ey7c8 zzRS=tNLRZ#xg{_++2ED8v|TH|xxHqja8rMt?1eHiFi`(9pVKoWJWt0jQ^WkITUx1T zC>CEDWO}Yj*Tqk@#?hTXm_0%vKZyYf&~x8BvnMrd)h=l)!N;+HSnZWq`qD|mBOA&fhkFe~yGMTzXY?%A^* z>ODV)N((bh=I|3l4FXytn4R85YG!6eGt$%RyuMnHihbSMsx1te zskZyllx6T*wP1R(`p6kP?8S-Sn~gC!PYH47A|okjSJ#+^hLEJB_Kxt6P&kExk&@D{ zbg2i?(IeENV`g-MoECjSV()8^_0%g1XIPV^){Wxob4-3SPE3IEuG2Fc_$J%QWh)pg^Rgcy3LUO6ln-NJyL= zEX9VBHRk1ZhXzggg7>?bkr`?tuZv*}AtfG7gCTk=#lGhVdfnnZEmv65UQ1cxXCIHiyx|vuu&O zxT*!0k&%&wneAl!<1Uw{Uu}%t-50N~iYF%Enwq(Do(~5G7OuLg{rGzisR8fW7gw557G)axPIy(S zX!9Kp&ly!UpbfY*I!sJr~9Jf z9cf!HSBe>@D5UP-Vr#gr_ERm&$*`u63GJVcUO_O)b~)cK z+VD9(B^I2Pk;tBmhVx!AQ{Q8NpRg@p7&+ku7VH4pdmy-A0Z@83_?2YzQ{{N-#1D@5NL z@9^>y%2%Y06cr_@C@5~>*vR|(_ZgR}Lz1VbFYDdp3e{MpbTTU{R=c8W0Xv!$9^PzR`r#ue@PqZ5 z|C%oZaKYyEUFF%)^F=QZZ$|oTKxZaYOWTb}i(7rxL!X7v4VefQr#@Ts0gO`2wM%H4 zUe$W05p@h+byw`7;#4Jb6ES_U+S4ab6xG!7k`I0lWyP0}z%z%gF8O1aKD%CD%8V6| zd}Z+!3)R8L#}6eHP*1esy`s;L^YQsE6^rF|^3AX`uGznqmY$vxfrCRD*t(c3EWDB` z@$8Me%|^cFBa)|<`_AlI8XCsd*4N|3!pp)>Nv!(51;|Ntw+KDEToQvUZ1Hl%u4`a% zaD;sP*i&xd^jwSPEn5wKzOgy=|=xg{2B__5zJC~5Ylw-j6_q9TQIE8rBL_q&s zjN4RxPAV41_?H^1lXX#b^@9edgPGc6EmOAev% z^K)KIuZMnm@mwgPCr@y(-?Q+ZPwzGM6_sv1dyId0_7%KrGx#YlH|(|-yHv2)moi#XIf z*)G-%5i$-&hLDIop#HS?E>b*gj$8MGGLnjfL`PWA!i9fd2jq8(#j(E8_~hgt)6<4S z8M>4dSbyF9Ma*gW&VfX4YeHUK)RGaNGo^l z_qx}N1ajGPCl|?UYD!&~Y9^s{{dLc_-^oaezj}oY|I|KYIyrksMur0+|LWBe7O5R~ z&O`i5ZpZEZYP+#tai!-!eRs^wzkT?iFCbv$5>Ma0lC!u~udp9lTC~69(sOnudiagq zCn!kR`Lvfxib7dQt)M`Fj*gC!(g4K&MePLUb~0A|`2y|s#;cTs!rs`oBObIujs5-d zl?wN=vPUl8j!gvf@i=UaFO5MFD8T; znwh~<2bgr~CbHrdYhPhv+`V;6Cp~@T$rBy{SNj<8XU}D|!b24m6ppftKrF9yN^*BK zgeGwWr3WJ;w!YmG?|+Ai|uBU%9G#ATpkpy%>UGVQ~`rkqEdRbZ@9CD=!Ew z8?-rFSXk7%pN)>I$*IPAf!%cYzD57NgG2e{82X&62xOu5b=qJ}ygV3NAo{>c_k%;& zssA-2(STn|tM=Lf`?%$5z9wq6?(FULWx#2hcWUa++0hu-%+CCu*VpZ6X`iU5IJhVA z+M7PdQNBLx_Dk|4GBM5Kv(K`y+}4ebR+g2maB=Z{@ZgaPe@f;v5G_&r%?wUX&tJSq zU212xBg8OrzwrcFg*6V90gB6!dsTn zxmASyK#wiDJ%ogaz=@HAqxr|n{f*rZ)pj32)&$^W@tqR5k4Q>RN(yg5!{PCEWK@*x z#+#q6PvN-i(VWBjZajM38Mo?S3 z!C%bAJzleSYD)B34!=Fk?&TTrrs<7m-U-;Rhm|3v7 zwdL&0?P2iZ%^sc-1n=i>*xcu&IZGp>zdkmFbsCP0=o4WYunIj6g=uTVd+U-d zE-17MM#Kngr4@tRi14*4I7;`pP${!eMGoNEioPa2FXLj)Ea(-m%SiT52mFKL8$ox@ z{dO`h(~UFEE@ToVo_sDQb^zUhhnVi*z(CH?Fy0^@UUXFS$ooN>&wd`ir7^2@y=I)x zY=35E$`TQ8?aYxe%~1}+kF28hgxdFB>rF!OgC>@(xzvY8st=o8WH^m~%|Dlm5S+nW z-wdwq6omv$RHA$B%qVqDE}*Aiya_c{1(>gy)Ftthky$$cXi%%ql6$39D9{o}0+E&w z8QY-PeJqdPn>-V~?)Uukv&+P5K8PnNoQ^%@tv|KDqy~ zjHb?$H=%)XQ?A4!IZ!1&Y;$w7{-Q$rYD-^K)Fqb5Q9^>b?JwEGC!7AzbL7C4b}i*x zoy6eq@TTz=iJf0`#%B0^8gRWM z-6Jj2Bby-jMx8cry3S^BSpky#gB6!r_RH&{?Y+57>tMCB#T7*+)qK@HLD2Rh-|{(T z%o(9Jyy%qy1oszq)~LR*i3$$`qjb}-HF-q;M}H@ROWvWGo^Ge{j)=vS$V{xhID!%b zP4?MrwQ^U9cl}Y`-rp_8t}9V}BqR)c0DX(mMKhsa032-aYyE(TNF$VV24!#t56}8( zA>X5j%W0)9UwYDplc@vQpHUi@X3F;o*z63{msTYF6SG*3} zmvkGg8{?_HSARhZF&EPtoc1cR-aKw_H*gWH_VF1YYb?|}x%ePwqPkX6sErcS(2!g& zt6o_#LJ8K)(Jc*_;{t`<(xNQp2@&U@7sITJ8aC<4@2Mc;CFp^eqva zx3EF9*6#VjLL)LmI`IjM?NfNi%*<9(b4y_XbTK{S1z{s2x=E=;!Q%GNl&FqSQ;CU@ zk(!2@%cB!f2$*Bb!TafG8Yb|ru5Qrq#lQ&O7dk*iMLhXsSvy?bZ(v36^mP_hyd ztng3W*+}dDfPhL{+tT_vAwg6PP+}laLD@qez4j;&e$Kc8+zT&swKqdZtU#|0qp75} z)}{67Y@_2TV;On4}hrmit@bBG&Bjo$5I^6+S` zcdKjw(#gW!%iGDR26LT}#=~?v(d!}gplSIOOH8>OTaNT<^G&Jx@7X)vZzrD1V2lh8 z2MU%UL9YsQYBu=t90WtN9jFnnQ$G{o)mb<#AL0d_x+lCa2}QBEM}>!XULdm}$z77+ zH;uZ?wv`+_e0<1VS83{_T*@Q?r;_jFLgw9`J}(JiX{o7FVNkFmfqFrqb>C$&Jv|+? z+H|is$0~_GH!pDNWv}3kR&Epeon5iGhGq7(ecJEQ9Q_in-sKjWFR?XU1BYN99XJ*3 zDa(iHYG{<0E*x$o+hN1~{Mv5&?6yjo(9@YGKBn|qrMm^1syG45+ts*-u)|8P?5Zkx zVPV|R`|zx+D)|O~ki=k!{ls|pF2J{YMjxlZ6x-A_97Q17RnUtSsm;x=Kr<%V$`MM_ zb&e8_l+iSI@%_!{W@JPK7gtv1;O4#yi#k3x5~j|2r(r_;0eWuG3g_T(5Nr2@z%cYC zCe%T9(oZjWXq8Fl&~80=K3VuO4X1O3{{8N*j;ByC^_QsH__69L#VIGtu3ue6Wkr&* zvXWK5k=OOlH~BM+EiAlvPsV3Grt!oi zh(Ms1V)M_R%1TO z-7}Uc;d8J?>@`eAyMC2+ikf-{|CjFh;#s76>GLZ*FSD-D7b|<_Ki_ycOz>$;@OvHD z3yU}pZpGTIF_aWt5Jv`&25N2MIby+e?cPi}xCP4Os}| zDIF`-KBqN3Is^HMYQmTOXR5`Sw6WeJxtwV>DZNC4@!(M#wS!feh*Se1Ow67#B(2R{ z11bM_>~;!~`RcaB-Yk_D&va+!*_dujxPrpeh&k?Hs=p%Ws&*=}f(7U5`qxt1^{2fv zWk^u7ib~Mdcm(x0<3kPcT$xWnrSUq04*8B!B;Lli{%Fen zJK@o%56?uX701lVpLdkK>S6$`xWP)SNX1~p{&D}*bBY_FM9r5jogfyiI2Slsm-clu1m-}x%11(? z9{KI8ATLEp+L7n`K+DKr-J_IT|G`P8?9I(QLT!e7wSpoaLqDs(l&bsUaP)X^*m&8f z?B_S}v^PYbGgx$!+QL2=og05ipf?^=2#=8q$N2z#v!Gva{Uuijv;$eB^@x7>5x+2Q z`pRJk3UDBdy8II8;*s`&N+cvbMZ8fM^LdO44Qo33xsiFa#zG$pTF*@n3{d=xSA^vK zG&e##nVz&$yPQd)8^xW*SJ15bBhQnR^yNeVE#a`fo*sr3CH_e>KP90w`%c!wThzJBo)KiH%dWKQ6OOi zU-~0q{O{F=e+vz{pW^)^u}m*3`}U8B(udLEkDAdkTk)ptX7Au)7>i~O^o>#~F=79F z1{-qIm;(=NXd(<6dSI+R&$s3Oh#Md9DK(SVqIpB%Gqo2m1Pex_WO_rx3nR8CetLi? z#%Dx_)s|IYO+qD$D)$gh1*Ysoq<9`43GbKOI|?3xnbq4?BXe*lxRi^N9<3})2oERC z{-Y=i6!fUkR3q%Je2IZ^7Zl8q+K*p`LD3&zJWWYXK0#rMJKec+YXa|&`0(bV8eiuz zJF*l$cK$6s6n{GpRaREno1MYHOum&ISW#Yy+%;m@Ne@mI>2_hJu8rZHI7f~*)1>WP zDoZ+2IlQ%F;k`4&mCEBR4h#cbdOZ~p&;YfQHL{M;Xp<>j3B@0Xc7Bdqe#sF(H}&;K z!pp0hGhjMX<3pn=I3*^<_WgG$F|DX4d}%ltZz)RsVBcvGs1z76LWneaY}rUmwm5d= zmv+1_LR%aN=Kp}Pt7DFmrpp3@LX@31HSnT$-nbp=)= zwcnw$yL(xsjc(r;j)UJ@v6GXxfp|=~&fhDxJoha*%t5X8I<%}Ts^846ySw|^!=5a( z{aVP0R0vyOn(IeP+SLV;RxFf692Hjau4&Jn_X;St#4qD9wjD6UKS*1RxX>$NuKTl#BK6CoYP7%d5cKMZ?JYbN^moGP;Q>*3O`vCcx- zZ@~kDiy-l3H=rjZ%sB2&+HNInH8fvh!wHxZWU3r?+9igfqodQ`Z1f-+XJ+c4@JFPx z+WTA5PF&1$dX!RuL9&Uw{3bdbAyxwn_zz)Gm(*rysPS7@0)>}*mwulYC=^;DjjVGP zB=i{K;v^*{Yz&Nfb@wkQ5c_+sIy$?_U+VRORJf7E-l5G)W;VfUJOcKKM5fJ(rP0Pj_^$AStB473E*S$6XJ0!?fPtpN$j?mI z6H`cZqCKa*>zxJ{O&5Aoil2c z(E`-BprD0A1f0WY@Pkwl{`rtc&FxK<^oO=~!&T=0D>grKCIB2c0Nx$Hy2PcV;($)4 zcNbSI`XjOj3I_|+nJd`4v~&uHC8eaKaILRCLv4xYU9K<9DsLqMrv)Cwp3kAIv#U#5 z#sdk!6Hvzrc^Jsbo|r_Y3~dMsU#jG*e@$X;skM5Eh4aQ=0#_J-FtAGMoj10Z$xP0% zjtKr!QFnfOV~hCztfM=`9%rbyMETx3gCzH5$Uo;zV8ZuWnRGQVsRTtlsMNKpoZCQk z>bAd6mKiK7B~|_Y1?iqX-eU+h94L;ETT`eZE_1Vk#&1NSg`c50)(;61xp08{>VJpR zKwf$()z+4g^x6Gny7R+_%Phu=d$+X_VId*AnGepQa_H{EqICQD`wrRrxys6lcwN|5 zS8~@%jW6|$jR%TTdp66vm$e!GNcr*}d#E490uKum&Okl=V#BVevJwj{U=+C8*WNNQidrkYt5`H zP+74H?&t$Oe6onVJj1OE@`bXov*SbTnRR&SnUxvvRaM7kB()knqq~)d`uiiex%>`RP#wg=itw-(a~%B z?P9>9sB1daySxx&J%|ClA38cE)v;5{5<)+j>-VdH`$hd5O?U2NB@dXYUOUZ>wa?AW z&Rk{Pv%clqjm*sd{H-y7&ACP9fkS z&CCwe7#tU8G%Ic9zVR3|xC=u%Ucoo}2f9{)ri_?t>C8Q$+75qW++GyoD;uAhO3CeU zz7EPb7kgq z_-GO|`>`2B1EufjC5=Zk`q!w%maa$)y2>|t>}aVDrnK@-8Qkn6mXci4_xVCF2y95U zOkWQVBwMvtO8@9Svo1Whl3T+D>40xNI+k7-Dz%*^M&^i&jtcs#=pE}6zo7_KcgLvvm_m@?TPdJ9bk@V59& zs?q=8H(FX+%G$f~HI@*$_+^Q^_>y)y8uzPwblF!{4|KZkqG|_ zE^pj8C;zx{^lX@KEI8@fd>-RdQa4!H1<7;=oc`A{Q(VN7JOH$7u$xm?yvlUGI{5zq zba8P7@))OmCEx$>f5n)c9mf_IwM}>9H2DK!Ma9H|{uwTisXhNQ+&@^weBqr^$KNiX zn888Ye-P~dFXok&J_h6Y1M6ja5PvJyT3bXOr7lxWPBH*Uo0ehRWeCK3H_EDYd!n)% zUrsU*Mx*}${{K2R5E=l5{xd(_{*NYZegz(0w3j!UZL;OsB6s(UC6v?q_iByT1SL`y zTL3U4ut+954cRc7Suj$80Tvj{%;dhXeA});dxju-9dZm>=Sf%_ljR@F%kR11`49oi z4I$^Ro(^%j>&yZsA(nTL>_10yAdr2~!-vAz;_^V3Dukq`qX0rK4)Mhx=0uwt$<53Z zaNL$J>AuB54ttD4$c4j8`IT!nX{shT0O*U*BDgYEkBP0Xr-+j&^X*LRcvvjmseY zPTNcV1}=Y}$#h@I&QQ&%B_9n<`0SA5A?fvLwmRT7UhrdD+G8DXL>Un;Pt1rwg~ zd#D)Qdxn_olA1T>O-p`JbeT8m=woOh1wre1_&*z||K0Te;jI2Y9AF=qs_Yc@(89IW zZbmUMGRnAKMQo>-8yXoI8ymUv8jftH!xY;iRKst7M6hE6{o1=K4A&mfumAdNQlRj4 z;n(NuKavY|1W@+h19Nf$1L<5uX^T{x@vNnqZOF_mEs5;b^W5JmE8}I0dOX7pYIJ|y z_iIBaQYqx8#nywfm9aSR`u(xm7QJ4RO-}U==Q-J!T=oHa99R#e6WH9w-&Kaumg+oj ztZ~@->gyW~c-0Y$7#L!-AfRM#_Ql&Wr~k#vvR|3RxL3>_q#P+rr%1fFD)&#w5+k7D7z1Ph9897ne#l8)B>vf0Eyc9@3bDSwd4pe z{;#ZdKXdkb_?&E9j;JNb`{NInq|b6pODA2KyIUt1DJidJYPn96VwsYHYTpr|P~g>= znb9&`P45e(rXttTkAX*pl-J6dOVhtos5eZ`a3g|4t3U&!J3zWHYnS{*R)y*VrclxMYXH#I8sFc0MFrsTOgUeP6hSRdDZwWCF5CD-y6^7 zV(j4X8!+hm=6f@ZM?=%JRJXHL(*D%~P_+UHx7_vn3?+*2Pp$ln*D1c>3QHe_ImNcI zo(kN8u{+6r@pZX>7Cf4->v=r>YOkpw2%l+eWCUe(P=~s{An?it1l489~ zV6<{M1vcED#yU%Z8Tpm4dbe5@PQSVHHP>24<4`i8OTaiAuljFqGx)qjo`24Lsw|7# z6#%se6b_W=iR`RO*KGyb+smlRf7~gTH+C1;e|5|t+J|Ndi*11;iK9RxAsQ@fa)i6b zWqy8bRju2?o&V(0KQ_IDRM)!P3n9jLS~w2wcL`V60zR%v%h}w#ykIEN>WkZ6LFvAD z@Q(3uC7s}p9)(Nosq`XvkYrFJb*nD6eNK4%l>HuTdD`aXJog-c<>N)3e1eC;8$w8@ z-!CCDU)eoCS-FHFJ=vOSyr)w4({$^B!cxwJgI$%wmd5;-2L#MXX`HingDV>cIMbf}89c0zn)LR@rsObi?7Y>|fgxn0f)Bjl3=3r>>={dmTa z=W`{Pv6~OcS&sc zA9GAzrqgh7DyE+vK;X-Eb&+4*TFv;%W^%ogm?By?g4LxupkAdQ=f7U`Vv%e*Z5;BP z>E!vXPghUr9>juBwq`q$$LC-+H|zd?vA7u9C+O6?D3x3yfB!#Fyp)*O_d=6*+al~iJxokYM7zZM8%)M`Jeqw- zX8|z*3l~5BjR|at2}#Uar(0xm71tUs#DVPV_r{Lxn(T)H>mJ4Lz}YtCU}vgCV9~W4 zcXsA-Kh?=1%R)AuGt+0!s&D2f_@P~Mw;grGV zMiTfQ(5v{3oV{6I^#JU8gPZ6)JvnSH8dLziB&-e%BGdQp82TXGRT=Q50VO) znAQ3Jv51saAl|Yg$}5cC5EA-DMK7pV4QKoekH%90@Sj*&MJc~Ur4H9Epu7UHk16lM zs%*VGI{Nrwu7gY`Q%W^B`o$p`&rliiLKKj}_%pTNw&q_sV)x{iQ{9EJyZ=?=#L7qi zduz=1<0=KV6!W$OFur4Z9}D>K@Vtb=AL^Hi z>My_YUKyQx_W%k_F3Re4yS!BbFt=|HwQDf(=`~=b-zX?ZI6n9e_}H8OOJ;bm!K40B z_U$y_xV}O3|A5qokrVxyIzGT@0%wele&_QH0QbN7W)Bame=sx^W$LcU_-%0czt0E@ zlW;Q!-4NdqiHUdC{u9aacE~M|{U42ebwHHe`t5);5=u!sD4=wsfS@2Dpfo7m z-CYAnh=>SC2}n0cGt%AN&CuQ5@!R-)=bm%!J@=mLU(WEx?EUU{?`N&`JZsC!ndE0@ z0}a6guFvl%!G!CCCpEG#0+%E^&K~|p6(3Eeet?l!s@JpOO#U6iE(*O67W6I9S z`450IUF9P3&))eD);s?pK$Hy(2*4bt^-{fUdQ6PuKhGaZPEQy6>m!_yWYjIt;3XCS z4ZF?)&XVM}!hq-#!iF$G;4ARQ+~>A#AJo>|cHwQ+{vS61U)3^o=R=Y|z$rD=_b-g| z56JTmD+2&|pnoGYdk2TVO%Ecyy^;Sx*cTOjzdiE-MMAQ-C8DC=nCrHbUp|YlkqvOK zdU|%Vbx42X4io|cg1e2o2X6t_{z)e$CI%S2=#Qd;-!!|mb+wkj5#2R41+=GtsxL2b zBrs^dl5tCFX=z<&R8*Xo*(^xpV%%2mdjokqI5_xH1{*tYy4o#F^5Sh)TR3k#3J+WG8PA!-kY`UTwF1~nsB3=|`yB(z8ON;5$=t>?8n zNFNqN&>_7|Y)H#Fh?w}0GeA1y`h234E>-ez5aRrmXk1cudXt=y1QhCpZ2X`@GWSVG zt}SJjo_5Q1GY3Jd?Xa1{oGgSba%ZX4YyK3p{!2W%=Doz250u^ zd7mpJdtt5kKv0qUZ0oIzglJZH(p~;Qq+#~)lA`2wx?0Mb`TxJc^e;|Nneic@$|`k~ zb;+~loyQVi-SzQSY%1|^SDr@gI$xb2GjkgBk&x$JiHQ@-3s2~XgIBfek5S(fo)M!2 zGU3&&$;BkWJj~KOn14AhYc{?9z@k?mNiQf`Qc|yQXR&uJkWhT(2-VtK%4pGMdq%lPBSvmCkGC=9`73JKx96BdBn{4T`Lo7#q|0B8$0^5H7aH zKc{kAE_<2i_LvSgp{FvOSNt2L^U->FQqtZp(pvj%^FX}bu8y$^hejX&06>^ZVdr2H zqA z-n2Kub^+-M@{j=6PA5aUFvorha^=Uzqd|^9ig=I{`RoTHB#``43lbS(dEbCLAJH16GV|;6WfgzecVJB|V0d`St_Ksm7-G>7EOqVX@a>!6mODnc&BL0v z_-Qn&U3+v3<0vlP#+`?eso+4lnpzExe`yiizpe@Uk(hn%xrK0gNw7kWzDw^D65?B3 z@5M2Ng+;tUocSfu>+=_k35Ih8xsA*bIbO>&xp&0x-6JJ6$I!pL zenx(e9U1E+xp#%t_W5?a`hNGtbekKp(2U2N&ZA5s(R@w!r4Cex@o9 zjJ!tBzRps7m%#ptA?z!)&|3x~t0_VdKaLb6-GNNiddfU5IypWLZBGTcF^o(M>LqMs zkmWAT^TweJhESnbH>DQvyE5MSc)e4EY8#2D8D zenSqQ`ezR7W2cynqq*a6a+3=Rv=2tO30hYuV}Ra6NFzy>Lhy33Av4;XrxM%;3w26b^5^lw zGb`2v$PLn&OL;Bz7{ASa=8>5bY&cKE`le6I0|7VI;YwgljL~Aw=i}yHx(K+3b`BKY zV1vRjen-p8S+^4N=F6d>aP|~>koq!R0rUCN3I@8zm4%9S-An#TF;>mg%^L&piO!+5 z!DTtC)#_R+bhNwn_SWxQt~W&|?1%%8wkwy{=c(9fY#Uo1m@+?o+DrGA%D`%8w=sOY z{CQFC ze-f$Jb%U+k$W4WpA+P1E+L7Iivz=f*vcy5wzUdqxEOc59*N_nrl;RFKvJbMG7p*r z!=)G#hczPA$rKXDpUS2|2C z)T5gz3$@-j4tg49bauF|<^kQQF(W39%vFQ;cNLhO7}AB6+rMEtx}qUm7iYd+>xoMR zwN4N(nfw|bkxxioDLw~<3!aas%fO@AQxf9~Zp9|$3hy>F<1s{JR{H827$HNBsP1^> z=1Qc=6OoYEJo^b?rxa$LFW#vzKEGlX8ON(txOw(?)95C3Obv!pWbVyrI4~m!B zD2tJS(fO(4Ko_uKi;RnSbJe|MEA#*lQWkChHcRpN2s@kYswXZMx#s2I>3dbxfY-;t z-*?OmXRYnEZju`N!9SGgp)LX5((gJbKcv%RV_WNeE;>7=&p#TAwdAby-oVcGoBQ?9 zXC_>4szb1*PqIO!Z=P1|M1~AL5#D;viVy3|WT#Q@v_+0yBrYbUrM~`M2(Oiks>aHwf}dL*knL{(HuTX}1!RZT@pccws0lrfJ4+1}nlnw8s!F+kO~H;z9x z>jEnysitwr?9%u~e(k$=am3FJ4Bvau=V3=bMf zJBPaq2{0vpC{_j)YvZM!{*t-b9UNJfUh-KekSu8sO=oFZaSokSAQ0_Ifv%?{~N|pH}qE}UxutTRuXvfEU32Yv! zO2{{FhBSo9_<0?i4#o;>91NFpeyWczF|HW(yvKkzZ9hhU$%-9@1fM+TV1_pcDcf}M zC&>!&@`^t$y7@taiVQs}H+HO_7zg)1N_d!5fPmH{DGZI@@|<#SRtKK zB~>B&>Oy)jMA9_ES&EJ_5J6LUvhI-!MrAN zCuQtMt|Ey5FDp7E^ONX-Y?>?L({5Q6L;RxYU^-&PbH*k7@+D_lS=q!2PG+3%LhDVR zwA>RyNMDsp4)A9{$Gc9a69h>DOPwS{$KSGPYaiasH*+$_l&13;LSDj*%`HU1TS@

    StS-Dedlpo~wOwkdVvMq?{anuoj)R zUSoBlLsV2w=I7^Ydz#GRlYbAR&%l?v)etpy#UR3Wb)IX`f+WK9CDe5dRpEVR=c4KxE(u#i5arH zQ%uh@@_R^dPs`}t_ITynCqq`#zak=+{QX5AKUR;~Z~QTPog^mK5)hyg&amX|!kDe3 z?}O%8Q+?+f#fy-rsEzGuL0;aKw$K@sJoTNWakG>7T)XABl=u05w(D?8_Osjf-Smh25<99 z+XdX8XcyJe;>GVn{Q_>Z`br659`27*e%_SKui{YFz1P_Hh(h}17{q~Qv;PO zs&IQsPS^WzRyAhikJi2Mx%acm0oioW3i0mb5tu~ zbKUsuU}L7dd*-dotf6-0^sudE!f~L9G?u5exrG_{+6=VjCq$}jZ4KuD6QL+A$Gn-{ z;%bc)!*10Y=RI{dm$$JcpM@L_8=5)6n;AyeW89z24f#s?qJL^?ezyJ!gihS-?6(VI zbk=jo>iT-#`x?uImLG$jR6?$sUTJ9#rFkN&|FB))6As76V*C+ zaJ)~-ZR>q(S2i;8+aN8?Y&3WN0QKX?JLo8QS=*8Mg)zbAunqHXOlot}Df=(lB%ek| znzp{nRnDk&eeT{=2L|)d$*NH>pWXwwwh}0=&Sv93*DsQnvP`&lLT;s2?3rK5^O_|p zn2H=!DvehtkyBFk0682K2vbn}-1r3v3X0Xmqdwb0tep${ASwk9%<90~g@%)NdAiy5 zd2c8hUr`f5>sznwzJR#siarHT7%ZWma43y#O3E78yDRQd2sDlI?jqKt(bh(7Jb1#O6n~JKi z+5ioO>sF?9Jpy4D*NlgZZ#6@5yclro{H8Y&E^eOjJ(EwFPbMRHP=wQMPX=G!u+4e# zQw24F4!WwacXARO5Ma`mh=lM<9A$&*gG3k(9-h2nB-DO%o!ch&W`!95BT!OOQhSw; z({;WfwA|cEobyyPNXJFs&y#<8BgEizcMh0s{(Xoj6R`sCU18t4EBqp3v>P$sKV5oU z)W%`lmsnE5Rgv@3N?W*1US0ibE}vXMlxSo_gD_RFIAVb$6mr|gry8P>iHQ%V@Ss?9 zzD!tt$Z8JB+N^<0&Vs&NfBIjO_=C5PkByM;lOOm+;gONLIqwKJ4MJ&IS!EvL&z|Ai z{&`%rIc>oFgyIs0AqSdP4!hvF7W92G)=AUpgN{eWZR)GX^8L>3{cgYdLc|FCdk{*A zytl_X@9E0P-TV6!DX-XQDux%-RlVFLs5e?Eb>lu-+l{TW!`{|XuJny>JaL7(ie_CLC9 z_HohYzG5R8T>nOW4k9enz-IZJ;k#%)y(>H8#s1p^HTKR%ZZsGu-T)HqD?Fp<0Tzv` zIT+QSs+nkjZ&P{0DIE?O*Y43#T0YGo8J#Z%NO~)U!-oYHt)C?&C6y@qYBu`f*LQSe zOGUAO>$(~P6V{a#aVI$pHYiFz*CPsC`p1ubw{82(e&EZ$S5x!6IvQn&00q)ifEGD1sxmh}aqobEm}BTwQxSaj6v z5IRFju~=nU#PL2Bp?bVr?XK*-cl!N##VW69ny32)V+FR0Pq=TU(z;vJLCrpUN+tg5 zVligFFY;~9n$k|t{Lt?q^TG!4sb6#aOLs*Qb6USw(Jbx1mA@|^lEU@7VeaQWh}Xxb z8EjGEK3pi2TM;CoMRNGn;aJ=S63@L}OM=w@=>PX5@Ne@(EGs)SFsh66s1_RIe?oo9 z9Mvgg8_f`5dJxI|s{X~rsgsOES*q+4;YQ?=Y|=!koDnMJ;-Om!=U1TO;iyx zt3SB=Nj9?=ME;`H@NENERSDw+;9nD!&^EWaf98#~Fa`UFQSrMJS9W3U8#%uy=hA{2 zp5*)yqlG%+0v%^W`U83CRTxt_?h~RfPM%-4mc|D=i|OAI-Ak?U5cyfVtHLW&`s>#y z@BPz@9e%J@nZ|1nRU2!6W`$f@>-<*#(5q7jAguK=t6A@S;%Sv z$sI4x#M=vm?TKf4-qtFt7K*qXp1W#)-ua$pM|;2USXZEWQlrNG`aH-NoCwGT&xp|J z?!nyHj7g9Vm`Qqh?s&B!eL3Ya=z6sMfN;w0;#<(rAzo$Q@2=OFpGT1^*; zW&N6#f!x2(0^@goDU*OJQoH3jJL_Sp3*1@Fr@nZ3Ojlp=1)tA^a2m;&EcbSGy)QG4 z^h_M(eKC1sC4D+>m!V~l>W_zw>{Wh{?r>nQR%MS1VbbLDcg4i=^FD7?$oS@mhrPc& z$IjN|w364&5P=1u5^@Rib>)2eb7v>C|Mtma319mQnAIwIfz6cb-IO? z(^Vta8$CDVM=w3m%=;3*^?pF_O ztVv))^cU`3q7DWpJVSxp4{(-V>?7yB4}gB`;ni0$9LnZ-CIY=d(m@hPT|rWcvbYr% z&KWTT`3{<^+l8?*A+@J{|Mzj=pkmnlb$z1Qt#brkd19Ux|Gjg`Zbb%ro#cMCtq&+D z^v)zLFMkdx0PKpq)V(;->|P8@zdr>tkE6X8YAXgBezSXLZe1 zT#c-!l>GL?ciAQP)`p#|+t?u1_SS-qm%&ZsDlY{1ir)X?wwdI%ORtrzwBEZ*G?5*$ zB(Y@clJO2vUF~W@r&;E_yj`v4=0pupZ3>?hsQdwNYGW{G=UW`Povpqa&lX9*CQ_n! z@aQ8WetvEwNN8J)ffPwMpofZTFuxf8_?_{Z`TW#H|p}V1a z8TLEJIFP&4f{u%sef&;gd`|1}|71QCuZZqt*3h9qD(v?rDl9Z!y{b>^G|CEYDAYr* z7I2Ffy2k_t!MZ_RAdS(ViC;se6M7siGP3=!@% z5J(=aCSd;FHK(kDC9-*b6RF_n#`!(`QsCdi>7f_!cU+yEH6fLk19 zm-6?;<>lbW8zBpjuO%q5Q7~_SOSQ^S>>uy%ZBahc)t4|e?eGgUs;Zto#CH~`&SnLU zF_4$e?_aOHLv+gU^6*^wMu0dzO%f9#pY>RGNSjJ#XHw^;`1fx_kwJH0$%>K|u221Y z3edyLV-6cBo>vy5!3ryION1C**pDAS^3-ecE`Kud#7gQ-@@OfQP5?Rq!p7x(@RS7W z2$9^%B_@Hioy(Csx=NwT65n1vM1ho7rB-N_aNC`>?QleMu^7hs`O!jt z#HuMXf=vcofN>`=g@FEuOqgL$Oqu754WI<*#ev0QT8^5H{@uS2yjp5`G%7?EF#gff z+3oEYz!5mn16u8RtU*Kx=QGSo@hMKnR;e8ty`qe0cc#>#m0mnUX3|*pQfSTBv}2 z1b(mq0a4rvJqb5qolAVzPIV$uI{(D)bLd*w(d#zk$!o?(N7%_!j$7-LrH*OKPn4Co ztomb`b(`}(Oy`JGyJS@bYt;f$_KM4`<40Q(?9y9N(%>>HWqMlh)s^XzBPt7vCHl>& z0hoWfV@m<9>Vlg^DKRJkr-_drL1E>>0zGbX?gvtK8hP=I{k{0kj*bTZqm7B?%B1SX zB#~OIQ)Dx{IfYA#quX^)22Efx4aQW`Owm)v6=CNQ~E+Xu&Rqm<-kaL~a$Ceg4 zUdzY3&aS|qm5aBu;CIp460pOeVg=kbo3^&!v61m?;GV*9{f)--DIdw>2RaqflJBd$jHu>9P!sJ{LP`KcUW+{_&{sksk zH3fo$)BsIv0$Cj@oJ{uX=QEEh<7RZ+oY2?N8O~OURn4xo%h=Wt`E+BmI_igQ6#leh zUbhr1Bj`6{GDi#i4x5hqi#p6JT!B(wG&H7eE;n8ut~Z-g#%22TtFx$xiyMu9e>O9X zWYp7?uTB(U&sZ3&h3~NX;c-#$nM)vm`FV&~u#-SU=vhZ|bI|&|?w_P-8LhWJisKq% zIDjEv9B&45Ap=n!Tf%0#e6*qRNbL0|f7mQlAi%d}H?unt{c`QW)bC=RrHBTOBpAmI zbIXhU?s0|Qm}x~?y}na{{pzsN?Ny6zmVmqTDhtXwj5Vi+kdW{$nfe)wS6u9C#<%m) z?_jyZCh7ilm^C)6wzWw%GM#;oE;So?Ci48szZ_Wx2vb!99vC=zp=@Djs+p4`_9G-) zmm^v^a-`%}Oy|-7+*EPrWawVD#XnHszXgE*Cs2egWfT0y715TEWdE$m#It+KRFUHx z6wcE$_C4&pKsN31yuADCe3jlWINc04{qGTvXrYk4lDO+mdOv`rjMJYeL_|Izf2N{$ z8Kf#ID_nucR!ha&(|*m>=$q`i-W9JBcjs+1lNix*E_Ui=ONcz3%9h%7LwT;COdAi! zC~X#(V&xZX!Xl=#t>S??cAjM#Z!r{lMre2?iQPa##1o=*fj1U79KyeTQbgCtCbZKS z2*Gi=f7tq_k)6&wSo``(*#)L|x zhy|^(AXTFh2uFgd{eBH(SM0x#w|*fU{x>B09vS(mwdnS6?7-#*Uwm*dahiNUH50%~ zx0qa*7>MJW2KR2i4|whUFW-ksymrD|Wd;xtC=LVva&a4L>%>ZwLt<%+5AlL9$-fS| zh4mq*;&*6p0Oou~hHvs$=nuvI79|Rh+6U_@CeZFST}#V%e_s_O<^9`FE-r37_|w9r zvx60YRd2y5FyYrB7bXPfQwe!|wfKWX>oEH5!;RaMn93b5XU zNIVu!k`)*Cuh)J+O1d=x54=Tu<`vj4ArSCp1BQHPh9mkbSsm{!MytRFP;+*DJzGAj zL*`z<>yIBl9xDC8cTg^_p^gs8gOFxjutCC}vw&)#uoos&G>qN zPzP@({o?qLIfMf{F?`#0wSHI+>n`T``|q}bkB@ZZ2F8DW4cBEv*G!Ur<@EeB4?|i- zs>FCjanc2Ei~T5a>f^<=IB&HTJASku2S*O8d8qyQsS;K5e%BZwY4L>SY*tmLGlSY2d$Z6QuILsnxdceBwY=m{80W9p+Q-a) zzkH})EI7WbN?Cl^gGj|B@K`)R19g6o2>Zmz)C<+tXjGa+_H3M z%=)JO1;iLpu>~UZpK&pex-5lO#~nmWe7vEq?!+X$HVA0x!p=_HQ!r#X;wjgX}E5582E4cS>=7j&7I77=*_+1;3gzgbuY0kurL00$e}=3bE1Sjatd zo(uiUP+F-KH6vExw?9otxJ3Ug$x#o_sQ^04GuB{%TE{Z zuzMNbmY0{u#SgecgjNkmF53 za|08Nl+rk=BP*dr5c2NO2X*!Kl5^V%gXoR{aKf^Xm+qJSqEd$Z&$*+yIkU}3c9_*h zx2KCE&Z7XjS{)7)cjt9%pMUTzZ+pPt0K~!ZpoiyAIKKni){(p+CV?YSNdR`1q(+@x zjwh;(HUQm>R5#rGLihFJ#d>*k_$koej~#>PWMYUY2whJs2b$Vb0DC&kTicd5H}zZ! zscv%82;e`EZYcJt$LoQRUinx_3Wp#VgY4r@HezvEDg!z;$ zIP1r-8kqMWrzcx7@a5RwO>pxy4!d(?$m&!Uu!x)nrF$2sxXIUMLv5H;RFp^$%RE;B zbx=!vqu2NempSpM8$=egnj=hApmXyysvwCVLI4t#_%|vHJZcji9l$Ebo(vQGUxZ?{ zv;*>0a(6@xCuD4rJGsUQDG@w+B0hot2%Rh~42mr}^M`(d4K#38R~*jv%hTV+sF>X; z8r%H9nat&bY?fa;fb%`}QJ|0mmwDT6(%d7YL@A?Q@YVx~K3~1?o2rD@jThAm_n9jF zJ0d`yEJ~9ySPIFDr+rOxFC#tu?v3SLBD|oI5=9_Lzud*|CUiPE2B1dLEm{&5Z~wev z$exlC7qWVJvI7v;(C9%WD78IbE%X7Y=f6sfMBCe(k1o$@S+#0_T(5n$_TfV`^@;Gu zR@fEh?}7qkAalpMo-Uf~gOmvB7Zoy;kG@ON4r|x~!Jca`Q~4?eXMcTI3bJ}SqXO}% zw{|*scC}*D{nKKiB*#*zDccCMy|?!c#Evr%YY-a?8&cP|+xA+j6Ld9 zK6}%9At=aD2q6iH$(hr?X{e`zN3`~1e2_%>r$`UiMvk%uX+SjT@q;5azxBqXhcKdI zLTF(`KfmJ0j7LI82MO{ZVsZiuL{U*uKR>@Akr&e|oX0O;NMQtLphM(8G_$^A;9OF> za|>nObc0_2soRX>iWG+#FYez5Ai0GxyCROC_k-G`81vV3N7W9`5wmnvaP80PEa+ZU zRq6@~oa;}s6dMeS;S{;)u=l^oa*W*8mzRk_*MixAfiPd3MJZ<(P@hdrLjw&Ag;P|` zX9u3PqRD48 zzJCT(^|0^vSv3SnsEFkdu|q#!IDrNzsziZ=&V*9SnFplo+Xz}N^TjrO4wb7a^mvoDp^PsriLq3LzT_E#uvdoyvoQ1Nm#1 zWSpI~y}A2kQxX6ZAYxx%RdldcLGwwfzLw3zw^TE0Y)FWVV7cNOV==t-e z23}%0yxi2@$fzO;H=hHU6P{NND>5Z7)`h{Ng@B1}(YBo5(OkXKG6LYQVmk$LwR@1m z)j2g9*0ZBh1SmS^mZqjup6jb1kFL9$Ni2v$%VSilv17n>J$#(s z(Lz$9Y!M_eW$BfQVyBc^s*#i$q9(8!4-deHFYw%9saF2iG7R@xn^*|2j$m%+Xy|@< zU%yp2T!@jYoXpJxf#J^YvTqGIgJA+>$i#-{3|cgs2U!B^t!xa0=}*VFa@fjn8wbxB zhN@~9743|W!}-{-Z{Q2J)8TyIJ^@QBRa+a~sv-WaPc39zfXqBvTe_*0BUokeB`mQD z@ee2$bxG4WkM37`S#2(|?&t08{mKW%2s71hat443Ub_D8sD)u$8M?&=g$v8bST&x% zh!9L;<;omZ0((7FTMZkgAI_L3PS^WZ*(~;z%fO$|g}oUa7FTCEQOG?funalRiKpBW z&HVO9+xhPi&ZlKxwjU30}%E~8ss=<^J$gAtk- zPd$nDdr{X#`=S{TY?#8g8uo5Al>VGliKj5yo0aTeHflO{!7+GB2U+-`_>dmsIN18x*#>Rabf1_CHzlg2K7K8dCeIlS=YROT{qpKc4`e`b8NYOb zu5zEBh%lUfU-*deT3r~Q=2lk%+bSmi0|mTr6Z<=;2Lb!=aonIR{8o)Q-P0TR#^Nyt zpp1>9V++rpkp3|}z209Ty|lD6(1LESbw2=Q@>an43y{#$)6)^m5PHwXzKU!OrXC%< zxPs;V4JQK2pHHLsGXZGuzrlyBk)U`vN}#Z%fx4Z)@zdPFnSGSk^et4<(SiPjr2e{v zz#sYO!>&8VKXD4$>pwV!>*4>xXB4b~Krkzsb^efgt6AP68*?6Vw-2{4Of*RrXll0@ zWi~Sv{25>$TH4zqK%Lazw+G&hX@B<+phXL#L~n5nAl|KI;{Kdh_y-Yrw-!k>)}8pE z00EaM9q>s71qE$w5-|+I`2hN$I60kQX(`c!K6&!wbIq4qL5arK-=F-!gYbj_xS0wg zBjfq)k;0#6=jQs$Z1lQCLL~?(D5T9qSGlDzo;EZ#`UyjioWQmqz#@M&4+DwL_aPM( z71*snA!nE-6u3TL+ei7_>RFGOn7;O|=-m+}6qryWkn}sBsm0b3U}<;PlvhuD@>G$5 zsL4X?5w@j`4ry>N54;~x>qU~cEY zT)_|Xc~w2QYHkQ4k!@PC^oNHm4EQ9ZKWxXPV{@IXKbv zK4J!M+nOe)zGQKU&k2umIOhFr#gbV0^Bf6;=_h%_QzwyXA6wM1j z-H5F>bjBP5s;eN=T-jlJ_5DfZgyHGpMVCBD%_+|+9E4v8F2O$13CjwiQdu7{^mSWU zu)6A(Fubb33MIaL^i$cDRPt1L$U*N>FDDfw(W=L2bsw)OZBPTFpAEWmv~SWM7kCyK zB-HCHd)R$eH&AUY3q-QT246)W*SGF;6`2^kae~5o#(H*}^<2)2X)3UxgmkI4yze?u z(%p$&jGmkNB<0Te`2Y>`;Sk9lW+n?<6}II&G*$m&TX-E~=5vlL+w~Q>RihfFv;^(8 zdKWu z^=n|*utA9;I+VWKm)DhPYO3tB-(@~5NI7ERL7?KVEDh2q+^dZwW{F6n7Q7dC$tCBX zYo>=NiyF;DmSZ@kO;<{VA&qMWxMaIXO+Ogn(+duSyJ^^E^TB z5oh79$Cpb8`48u%NsN~;y6LNn{MsIiv~+qzz|2vnH0)zQZEtGJJE}F(V6zk+i1s=g zZN8+L$=ngi?bVl`J}_6dU^+WIV@9soYtt!YA|{7S{N9E?tC)e4`(L`rpg=eYx8K0L&53V>3OUp5I+U|2?)jHUh#q<*g;pEE8?;zLJ_U;f8FWH~FJ_Piv zHisE)HKcC2z`<qmQn9?6De+k01|9Rr<{}JW@H}SIX6%$S)oXVZxN1mNx8uApC2ZtH_VP}yc%~pnr?Pvr zcOZLjJ4+=sqgOu1bjdkX;n7L>5I?Y>>5~qPA~w4rF_WT4^NF@!(YF{<_?WEtl&i&8 z(N`^%MvC9^x&O)Zc1 za(aKxpxynHs}zPyMNLK0S$p!BC)RGm@-l^flz0E1qwgf-<1?8l0DqObO}NxyCr=9%;!3QxD*i8a@X0Z zLNymnmh(ZxCgSY)^P%;@jUthtv%SlSm5|bTcvfNV`s&?DS0x#l`qAuE541r1c(U2fDF+j`|Nqz@07 zPx;h&=JZTKC;6$%J!I)2A$huFfSc>5fxI{H-L%ek=(VzH`$B3%7fD)lZ{QHa?CyIe z%6ZoV0VI0*;`dpy(cFEUY&yI)3xQ-C>_3`@-#n9UYh_?mB3gRdx!4qx(y0G2KBRkT vbj0d@E&eSR46VQm!Us{=f65rTK@a@=y?Ek`%noc^4tXQ`?scB9w)g)4EPaK& literal 0 HcmV?d00001 diff --git a/backend/app.py b/backend/app.py index 08d38442..325f4c7d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2459,6 +2459,99 @@ def generate_summary(): except Exception as e: logging.error(f"Failed to clean up: {e}") +from datetime import datetime +from pathlib import Path + +from curation_report_generator import graph +from financial_doc_processor import markdown_to_html, BlobStorageManager +from financial_agent_utils.curation_report_utils import ( + REPORT_TOPIC_PROMPT_DICT, + InvalidReportTypeError, + ReportGenerationError, + StorageError +) +from financial_agent_utils.curation_report_config import ( + WEEKLY_CURATION_REPORT, + ALLOWED_CURATION_REPORTS, + NUM_OF_QUERIES +) + +from prompts.curation_reports.ecommerce import report_structure as ecommerce_report_structure +from prompts.curation_reports.monthly_economics import report_structure as monthly_economics_report_structure +from prompts.curation_reports.weekly_economics import report_structure as weekly_economics_report_structure + +report_structure_dict = { + "Ecommerce": ecommerce_report_structure, + "Monthly_Economics": monthly_economics_report_structure, + "Weekly_Economics": weekly_economics_report_structure +} + +@app.route('/api/reports/generate/curation', methods=['POST']) +def generate_report(): + try: + data = request.get_json() + user_report_type_rqst = data['report_type'] # Will raise KeyError if missing + + # Validate report type + if user_report_type_rqst not in ALLOWED_CURATION_REPORTS: + raise InvalidReportTypeError(f"Invalid report type. Please choose from: {ALLOWED_CURATION_REPORTS}") + + # Get report configuration + report_type = REPORT_TOPIC_PROMPT_DICT[user_report_type_rqst] + search_days = 7 if user_report_type_rqst in WEEKLY_CURATION_REPORT else 30 + + # Generate report + logger.info(f"Generating report for {user_report_type_rqst}") + report = graph.invoke({ + "topic": report_type, + "report_structure": report_structure_dict[user_report_type_rqst], + "number_of_queries": NUM_OF_QUERIES, + "search_mode": "news", + "search_days": search_days + }) + + # Generate file path + current_date = datetime.now() + file_path = Path(f"Reports/Curation_Reports/{user_report_type_rqst}/{current_date.strftime('%B_%Y')}.html") + file_path.parent.mkdir(parents=True, exist_ok=True) + + # Convert and save report + logger.info("Converting markdown to html") + markdown_to_html(report['final_report'], str(file_path)) + + # Upload to blob storage + logger.info("Uploading to blob storage") + blob_storage_manager = BlobStorageManager() + upload_result = blob_storage_manager.upload_to_blob( + file_path=str(file_path), + blob_folder=f"Reports/Curation_Reports/{user_report_type_rqst}" + ) + + # Cleanup files + logger.info("Cleaning up local files") + file_path.unlink(missing_ok=True) # Delete file + try: + file_path.parent.rmdir() # Try to remove directory if empty + except OSError: + pass # Directory not empty or already removed, that's okay + + return jsonify({ + 'status': 'success', + 'message': f'Report generated for {user_report_type_rqst}', + 'report_url': upload_result['blob_url'] + }) + + except KeyError: + logger.error("Missing report_type in request") + return jsonify({'error': 'report_type is required'}), 400 + + except InvalidReportTypeError as e: + logger.error(f"Invalid report type: {str(e)}") + return jsonify({'error': str(e)}), 400 + + except Exception as e: + logger.error(f"Unexpected error during report generation: {str(e)}", exc_info=True) + return jsonify({'error': 'An unexpected error occurred while generating the report'}), 500 if __name__ == "__main__": diff --git a/backend/curation_report_generator.py b/backend/curation_report_generator.py new file mode 100644 index 00000000..53489f7f --- /dev/null +++ b/backend/curation_report_generator.py @@ -0,0 +1,458 @@ +# library +import os +from dotenv import load_dotenv +import requests +import markdown2 +import json +import operator +from datetime import datetime +from typing import Annotated, List, Optional, Literal +from typing_extensions import TypedDict +from pydantic import BaseModel, Field +from pathlib import Path +from IPython.display import Image, display, Markdown + +from langgraph.constants import Send +from langgraph.graph import START, END, StateGraph +from langchain_core.messages import HumanMessage, SystemMessage +from datetime import datetime +from financial_doc_processor import BlobStorageManager + +from llm_config import LLMManager, LLMConfig + +from prompts.curation_reports.ecommerce import ( + report_structure, + final_section_writer_instructions, + query_writer_instructions, + section_writer_instructions +) + +from prompts.curation_reports.general import ( + report_planner_query_writer_instructions, + report_planner_instructions +) + +from financial_agent_utils.curation_report_tools.web_search import CustomSearchClient + +load_dotenv() + +import logging + + +logging.basicConfig( + level = logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +#################################### +# LLM and Tools +#################################### + +llm_manager = LLMManager() +llm_writing = llm_manager.get_client(client_type='gpt4o', use_langchain=True) + +web_search_tool = CustomSearchClient() +# query (str): Search query +# max_results (int): Maximum number of results to return +# search_mode (str): Search topic (e.g., "news") +# search_days (int): Number of days to search for news +# **kwargs: Additional parameters to pass to the search endpoint (e.g., include_domains) + +MAX_RESULTS = 3 # for web search query results + +#################################### +# State Definitions +#################################### + +class Section(BaseModel): + name: str = Field( + description="Name for this section of the report.", + ) + description: str = Field( + description="Brief overview of the main topics and concepts to be covered in this section.", + ) + research: bool = Field( + description="Whether to perform web research for this section of the report." + ) + content: str = Field( + description="The content of the section." + ) + +class Sections(BaseModel): + sections: List[Section] = Field( + description="Sections of the report.", + ) + +class SearchQuery(BaseModel): + search_query: str = Field(None, description="Query for web search.") + +class Queries(BaseModel): + queries: List[SearchQuery] = Field( + description="List of search queries.", + ) + +class ReportState(TypedDict): + topic: str # Report topic + report_structure: str # Report structure + search_mode: Literal["general", "news"] # Search topic type + number_of_queries: int # Number web search queries to perform per section + sections: list[Section] # List of report sections + completed_sections: Annotated[list, operator.add] # Send() API key + search_days: Optional[int] # Only applicable for news topic + report_sections_from_research: str # String of any completed sections from research to write final sections + final_report: str # Final report + +class ReportStateOutput(TypedDict): + final_report: str # Final report + +class SectionState(TypedDict): + search_mode: Literal["general", "news"] # Search topic type + search_days: Optional[int] # Only applicable for news topic + number_of_queries: int # Number web search queries to perform per section + section: Section # Report section + search_queries: list[SearchQuery] # List of search queries + source_str: str # String of formatted source content from web search + report_sections_from_research: str # String of any completed sections from research to write final sections + completed_sections: list[Section] # Final key we duplicate in outer state for Send() API + +class SectionOutputState(TypedDict): + completed_sections: list[Section] # Final key we duplicate in outer state for Send() API + + +#################################### +# Research Planning +#################################### + +def generate_report_plan(state: ReportState): + logger.info(f"Starting report plan generation for topic: {state['topic']}") + + # Inputs + topic = state["topic"] + report_structure = state["report_structure"] + number_of_queries = state["number_of_queries"] + search_mode = state["search_mode"] + search_days = state["search_days"] + + # Convert JSON object to string if necessary + if isinstance(report_structure, dict): + report_structure = str(report_structure) + + # Generate search query + structured_llm = llm_writing.with_structured_output(Queries) + + # Format system instructions + system_instructions_query = report_planner_query_writer_instructions.format(topic=topic, + report_organization=report_structure, + number_of_queries=number_of_queries, + today_date = datetime.now().strftime("%B %Y")) + + # Generate queries + results = structured_llm.invoke([SystemMessage(content=system_instructions_query)]+[HumanMessage(content="Generate search queries that will help with planning the sections of the report.")]) + logger.info(f"Generated {len(results.queries)} search queries to conduct web search") + + # Web search + query_list = [query.search_query for query in results.queries] + + ################################################## + # At this point, we have successfully generated a + # list of search queries ready for web search execution. + ################################################## + + search_tasks = [] + logger.info(f"Conducting web search to design sections") + for query in query_list: + try: + result = web_search_tool.search(query=query, + search_mode=search_mode, + max_results=MAX_RESULTS, + search_days=search_days) + search_tasks.append(result) + except Exception as e: + logger.warning(f"Search failed for query '{query}': {str(e)}") + # Add empty/default search result + search_tasks.append({ + "query": query, + "results": [] + }) + + # Only proceed with formatting if we have any results + if search_tasks: + search_tasks_str = web_search_tool.format_results_for_llm(results=search_tasks) + else: + search_tasks_str = "No search results found. Proceeding with report generation based on general knowledge." + + ################################################## + # At this point, we have successfully conducted X web searches (max_results) on X queries (number of queries). + ################################################## + + # Format system instructions + system_instructions_sections = report_planner_instructions.format(topic=topic, + report_organization=report_structure, + context=search_tasks_str) + + # Generate sections + structured_llm = llm_writing.with_structured_output(Sections) + + logger.info(f"Generating section plan for the report") + report_sections = structured_llm.invoke([SystemMessage(content=system_instructions_sections)]+[HumanMessage(content="Generate the sections of the report. Your response must include a 'sections' field containing a list of sections. Each section must have: name, description, plan, research, and content fields.")]) + + ################################################## + # Now, we have parsed web search results and topic subject matter to sections (intro, subject 1-2 in body, conclusion). + ################################################## + + logger.info(f"Generated a report plan with {len(report_sections.sections)} sections") + return {"sections": report_sections.sections} + +#################################### +# Section writing +#################################### + +def generate_queries(state: SectionState): + """ Generate search queries for a section """ + + # Get state + number_of_queries = state["number_of_queries"] + section = state["section"] + + # Generate queries + structured_llm = llm_writing.with_structured_output(Queries) + + # Format system instructions + system_instructions = query_writer_instructions.format(section_topic=section.description, number_of_queries=number_of_queries) + + # Generate queries + logger.info(f"Generating {number_of_queries} search queries for section {section.name}") + queries = structured_llm.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate search queries on the provided topic.")]) + + ################################################## + # At this point, we have successfully generated queries ready for web search execution. + ################################################## + + logger.info(f"Search queries generated for section: {state['section'].name}") + return {"search_queries": queries.queries} + +def search_web(state: SectionState): + logger.info(f"Starting web search for section: {state['section'].name}") + + """ Search the web for each query, then return a list of raw sources and a formatted string of sources.""" + + # Get state + search_queries = state["search_queries"] + search_mode = state["search_mode"] + search_days = state["search_days"] + + # Web search + query_list = [query.search_query for query in search_queries] + + search_tasks = [] + + ################################################## + # Here, for each search query, we conduct X web searches (max_results) and return X sources. + ################################################## + + + for query in query_list: + search_tasks.append(web_search_tool.search(query=query, + search_mode = search_mode, + max_results= MAX_RESULTS, + search_days= search_days)) + logger.info(f"Returning {MAX_RESULTS} sources for query: {query}") + + # convert search_tasks to a string + search_tasks_str = web_search_tool.format_results_for_llm(results=search_tasks) + + ################################################## + # total searches conducted = number of search queries * max_results + # all converted to a string before saved to state + ################################################## + + logger.info(f"Completed web search for section {state['section'].name}") + return {"source_str": search_tasks_str} + +def write_section(state: SectionState): + logger.info(f"Writing content for section: {state['section'].name}") + + """ Write a section of the report """ + + # Get state + section = state["section"] + source_str = state["source_str"] + + # Format system instructions + system_instructions = section_writer_instructions.format(section_title=section.name, + section_topic=section.description, + context=source_str) + + # Generate section + logger.info(f"Generating section content for section {state['section'].name}") + section_content = llm_writing.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate a report section based on the provided sources.")]) + + ################################################## + # Here, we have successfully generated a section of the report. + ################################################## + + + ################################################## + # IMPORTANT: here, it is saving section content directly to state['section'] object + ################################################## + logger.info(f"Saving section content to: {section.name} section object") + section.content = section_content.content + + ################################################## + # content was empty when report plan was generated, now it is added + # research-required sections have content generated and added to completed_sections state + ################################################## + + # Write the updated section to completed sections + logger.info(f"Completed writing content for section: {state['section'].name}") + return {"completed_sections": [section]} + +# Add nodes and edges +section_builder = StateGraph(SectionState, output=SectionOutputState) +section_builder.add_node("generate_queries", generate_queries) +section_builder.add_node("search_web", search_web) +section_builder.add_node("write_section", write_section) + +section_builder.add_edge(START, "generate_queries") +section_builder.add_edge("generate_queries", "search_web") +section_builder.add_edge("search_web", "write_section") +section_builder.add_edge("write_section", END) + +# Compile +logger.info(f"Compiling section builder graph") +section_builder_graph = section_builder.compile() + +# View +# display(Image(section_builder_graph.get_graph(xray=1).draw_mermaid_png())) + +#################################### +# End to end report generation +#################################### + + +def initiate_section_writing(state: ReportState): + """ This is the "map" step when we kick off web research for some sections of the report """ + + # Kick off section writing in parallel via Send() API for any sections that require research + logger.info(f"Kicking off section writing for {len(state['sections'])} research-required sections") + return [ + Send("build_section_with_web_research", {"section": s, + "number_of_queries": state["number_of_queries"], + "search_mode": state["search_mode"], + "search_days": state["search_days"]}) + for s in state["sections"] + if s.research + ] + +def write_final_sections(state: SectionState): + """ Write final sections of the report, which do not require web search and use the completed sections as context """ + + logger.info(f"Writing final/non-research section: {state['section'].name}") + + # Get state + section = state["section"] + completed_report_sections = state["report_sections_from_research"] + + # Format system instructions + system_instructions = final_section_writer_instructions.format(section_title=section.name, section_topic=section.description, context=completed_report_sections) + + # Generate section + logger.info(f"Generating final section content for section {state['section'].name}") + section_content = llm_writing.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate a report section based on the provided sources.")]) + + # Write content to section + logger.info(f"Saving final section content to: {section.name} section object") + section.content = section_content.content + + ################################################## + # here, we have generated content for non-research sections + # content is added to completed_sections state to generate final report + ################################################## + + logger.info(f"Completed writing final section: {state['section'].name}") + return {"completed_sections": [section]} + +def format_sections(sections: list[Section]) -> str: + """ Format a list of sections into a string """ + formatted_str = "" + for idx, section in enumerate(sections, 1): + formatted_str += f""" + {'='*60} + Section {idx}: {section.name} + {'='*60} + Description: + {section.description} + Requires Research: + {section.research} + + Content: + {section.content if section.content else '[Not yet written]'} + + """ + return formatted_str + +def gather_completed_sections(state: ReportState): + """ Gather completed sections from research """ + + # List of completed sections + completed_sections = state["completed_sections"] + + # Format completed section to str to use as context for final sections + logger.info(f"Combining completed sections to one single string") + completed_report_sections = format_sections(completed_sections) + + return {"report_sections_from_research": completed_report_sections} + +def initiate_final_section_writing(state: ReportState): + """ This is the "map" step when we kick off research on any sections that require it using the Send API """ + + # Kick off section writing in parallel via Send() API for any sections that do not require research (intro and conclusion) + logger.info(f"Kicking off final section writing for non-research sections") + return [ + Send("write_final_sections", {"section": s, "report_sections_from_research": state["report_sections_from_research"]}) + for s in state["sections"] + if not s.research + ] + +def compile_final_report(state: ReportState): + logger.info("Starting final report compilation") + + """ Compile the final report """ + + # Get sections + sections = state["sections"] + completed_sections = {s.name: s.content for s in state["completed_sections"]} + + # Update sections with completed content while maintaining original order + # actually, this is not necessary, since we have already added content to section in previous steps + for section in sections: + section.content = completed_sections[section.name] + + # Compile final report + logger.info(f"Compiling final report with {len(sections)} sections") + all_sections = "\n\n".join([s.content for s in sections]) + + logger.info("Completed final report compilation") + return {"final_report": all_sections} + +# Add nodes +builder = StateGraph(ReportState, output=ReportStateOutput) +builder.add_node("generate_report_plan", generate_report_plan) +builder.add_node("build_section_with_web_research", section_builder.compile()) +builder.add_node("gather_completed_sections", gather_completed_sections) +builder.add_node("write_final_sections", write_final_sections) +builder.add_node("compile_final_report", compile_final_report) + +# Add edges +builder.add_edge(START, "generate_report_plan") +builder.add_conditional_edges("generate_report_plan", initiate_section_writing, ["build_section_with_web_research"]) +builder.add_edge("build_section_with_web_research", "gather_completed_sections") +builder.add_conditional_edges("gather_completed_sections", initiate_final_section_writing, ["write_final_sections"]) +builder.add_edge("write_final_sections", "compile_final_report") +builder.add_edge("compile_final_report", END) + +# Compile +logger.info(f"Compiling report builder graph") +graph = builder.compile() +# display(Image(graph.get_graph(xray=1).draw_mermaid_png())) diff --git a/backend/financial_agent_utils/curation_report_config.py b/backend/financial_agent_utils/curation_report_config.py new file mode 100644 index 00000000..0231ebf2 --- /dev/null +++ b/backend/financial_agent_utils/curation_report_config.py @@ -0,0 +1,10 @@ +#################################### +# Curation report config +#################################### + +ALLOWED_CURATION_REPORTS = ["Ecommerce", "Monthly_Economics", "Weekly_Economics"] + +WEEKLY_CURATION_REPORT = ['Weekly_Economics'] +MONTHLY_CURATION_REPORT = ['Monthly_Economics', 'Ecommerce'] + +NUM_OF_QUERIES = 2 \ No newline at end of file diff --git a/backend/financial_agent_utils/curation_report_tools/__init__.py b/backend/financial_agent_utils/curation_report_tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/financial_agent_utils/curation_report_tools/web_search.py b/backend/financial_agent_utils/curation_report_tools/web_search.py new file mode 100644 index 00000000..4e4bba65 --- /dev/null +++ b/backend/financial_agent_utils/curation_report_tools/web_search.py @@ -0,0 +1,150 @@ +from typing import Literal, Optional, List +import requests +from pydantic import BaseModel, HttpUrl +import logging +from pathlib import Path +from pydantic_settings import BaseSettings + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# search default setting +class SearchSettings(BaseSettings): + SEARCH_API_ENDPOINT: HttpUrl = "https://webgpt0-vm2b2htvuuclm.azurewebsites.net/api/web-search" + class Config: + env_prefix = "SEARCH_" # Allow override with env vars like SEARCH_MAX_RESULTS + +search_settings = SearchSettings() + +class SearchResult(BaseModel): + title: str + date: Optional[str] # date is optional because it is not always available + url: str + content: str + +class SearchResponse(BaseModel): + query: str + results: List[SearchResult] + +class CustomSearchClient: + """Client for performing web searches using a custom search endpoint. + + Attributes: + endpoint (str): The search API endpoint + """ + + def __init__(self, endpoint: str = search_settings.SEARCH_API_ENDPOINT): + self.endpoint = endpoint + + def search(self, + query: str, + max_results: int = 2, + search_days: int = 15, + search_mode: Literal["news", "web"] = "news", + include_domains: Optional[List[str]] = None, + **kwargs) -> SearchResponse: + """ + Perform a web search using the custom endpoint. + + Args: + query (str): Search query + max_results (int): Maximum number of results to return + search_days (int): Number of days to search back + search_mode (Literal["news", "web"]): The type of search to perform + include_domains (Optional[List[str]]): List of domains to include in the search + **kwargs: Additional parameters to pass to the search endpoint + + Returns: + SearchResponse: Search results in a format similar to Tavily's response + """ + if not query.strip(): + raise ValueError("The search query must be non-empty.") + if search_days < 0: + search_days = 15 + + payload = { + "query": query, + "mode": search_mode, + "max_results": max_results, + "search_days": search_days, + "include_domains": include_domains, + } + + try: + response = requests.post(self.endpoint, json=payload) + response.raise_for_status() + return SearchResponse(**response.json()) + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP error occurred: {e}") + raise + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error occurred: {e}") + raise + except requests.exceptions.Timeout as e: + logger.error(f"Timeout error occurred: {e}") + raise + except requests.exceptions.RequestException as e: + logger.error(f"An error occurred: {e}") + raise + except ValueError as e: + logger.error(f"Error parsing search response: {e}") + raise + + def format_results_for_llm(self, results: List[SearchResponse]) -> str: + """Format search results for LLM consumption. + + Args: + results: List of search responses to format + + Returns: + Formatted string containing all search results + """ + formatted = [] + + for query_result in results: + formatted.extend([ + f"\nSearch Query: {query_result.query}\n", + "-" * 80, + self._format_individual_results(query_result.results) + ]) + + return "\n".join(formatted) + + def _format_individual_results(self, results: List[SearchResult]) -> str: + """Helper method to format individual search results. + + Args: + results: List of individual search results to format + + Returns: + Formatted string containing the results + """ + formatted = [] + + for idx, result in enumerate(results, 1): + formatted.extend([ + f"Result {idx}:", + f"Title: {result.title}", + f"Date: {result.date}", + f"URL: {result.url}", + "\nContent:", + f"{result.content}\n", + "-" * 40 + ]) + + return "\n".join(formatted) + +if __name__ == "__main__": + # Test the client + try: + client = CustomSearchClient() + query = "Who won the 2024 presidential election?" + + results = client.search(query = query, max_results=4, search_mode="news", search_days=-1) + print(client.format_results_for_llm([results])) + except Exception as e: + logger.error(f"Test failed: {str(e)}") \ No newline at end of file diff --git a/backend/financial_agent_utils/curation_report_utils.py b/backend/financial_agent_utils/curation_report_utils.py new file mode 100644 index 00000000..fcc2f154 --- /dev/null +++ b/backend/financial_agent_utils/curation_report_utils.py @@ -0,0 +1,27 @@ +######################### +# Curation Report Generator +######################### +# get the current month and year to format Month_Year.html +from datetime import datetime +current_month = datetime.now().strftime("%B") +current_year = datetime.now().strftime("%Y") + + +REPORT_TOPIC_PROMPT_DICT = { + "Ecommerce": f"Please provide an ecommerce report for {current_month} {current_year}", + "Monthly_Economics": f"Please provide an economics report for {current_month} {current_year}", + "Weekly_Economics": f"Please provide an economics report for this week" +} + + +class ReportGenerationError(Exception): + """Base exception for report generation errors""" + pass + +class InvalidReportTypeError(ReportGenerationError): + """Raised when report type is invalid""" + pass + +class StorageError(ReportGenerationError): + """Raised when storage operations fail""" + pass \ No newline at end of file diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index dd440ec5..c2ef6470 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -7,6 +7,7 @@ import shutil from pathlib import Path from collections import defaultdict +import markdown2 from typing import Dict, List import pandas as pd @@ -23,12 +24,13 @@ from utils import convert_html_to_pdf from app_config import BLOB_CONTAINER_NAME, PDF_PATH +# Load environment variables +load_dotenv() + BLOB_CONNECTION_STRING = os.getenv('BLOB_CONNECTION_STRING') BLOB_CONTAINER_NAME = os.getenv('BLOB_CONTAINER_NAME') -# Load environment variables -load_dotenv() # configure logging logging.basicConfig( @@ -282,6 +284,61 @@ def create_document_paths(output_path: str, equity_name: str, financial_type: st } } +def markdown_to_html(markdown_text: str, output_file: str): + """Convert markdown to HTML using markdown2""" + # Define CSS styles + css_styles = """ + + """ + + html_content = markdown2.markdown(markdown_text, extras=["tables"]) + + # Combine CSS with HTML content + final_html = f""" + + + + + {css_styles} + + + {html_content} + + + """ + + # Create output directory if it doesn't exist + Path(output_file).parent.mkdir(parents=True, exist_ok=True) + with open(output_file, 'w', encoding='utf-8') as f: + f.write(final_html) + class BlobStorageError(Exception): """Base exception for blob storage operations""" pass @@ -306,6 +363,17 @@ class BlobDownloadError(BlobStorageError): """Failed to download blob""" pass +class ReportGenerationError(Exception): + """Base exception for report generation errors""" + pass + +class InvalidReportTypeError(ReportGenerationError): + """Raised when report type is invalid""" + pass + +class StorageError(ReportGenerationError): + """Raised when storage operations fail""" + pass class BlobStorageManager: def __init__(self): try: @@ -399,18 +467,71 @@ def download_documents(self, equity_name: str, return downloaded_files # make sure the document_paths is a dict with the structure of create_document_paths - def upload_to_blob(self, document_paths: dict) -> Dict: + def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: """ - Upload files to Azure Blob Storage with organized folder structure based on filing type. + Upload files to Azure Blob Storage. Can handle either a document_paths dictionary + or a single file path. Args: - document_paths (dict): Nested dictionary with equity IDs and their filing types - container_client: Azure blob container client - base_folder (str): Base folder name in blob storage + document_paths (dict, optional): Nested dictionary with equity IDs and their filing types + file_path (str, optional): Direct path to a file to upload + blob_folder (str, optional): Custom folder path in blob storage (defaults to self.blob_base_folder) Returns: - dict: Dictionary of upload results with equity IDs and filing types as keys + dict: Dictionary of upload results """ + if not document_paths and not file_path: + raise ValueError("Either document_paths or file_path must be provided") + + if document_paths and file_path: + raise ValueError("Cannot provide both document_paths and file_path") + + # Handle single file upload + if file_path: + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + try: + # Use provided blob folder or default to base folder + base_folder = blob_folder if blob_folder else self.blob_base_folder + blob_path = f"{base_folder}/{os.path.basename(file_path)}" + # set the content type based on the file extension + if blob_path.endswith('.pdf'): + content_type = 'application/pdf' + elif blob_path.endswith('.html'): + content_type = 'text/html' + elif blob_path.endswith('.txt'): + content_type = 'text/plain' + else: + content_type = 'application/octet-stream' + with open(file_path, "rb") as data: + try: + self.container_client.upload_blob( + name=blob_path, + data=data, + overwrite=True, + content_settings=ContentSettings(content_type=content_type) + ) + except Exception as e: + raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") + + # get the blob url for the uploaded file + blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" + + result = { + "status": "success", + "blob_path": blob_path, + "blob_url": blob_url + } + logger.info(f'Document has been uploaded to {blob_path}') + return result + + except Exception as e: + result = {"status": "failed", "error": str(e)} + logger.error(f"Failed to upload file {file_path}: {str(e)}") + return result + + # Handle document_paths dictionary upload (original functionality) if not isinstance(document_paths, dict): raise ValueError("document_paths must be a dictionary") @@ -424,22 +545,34 @@ def upload_to_blob(self, document_paths: dict) -> Dict: blob_path = f"{self.blob_base_folder}/{filing_type}/{equity}_summary.pdf" if "summary" in document_path else f"{self.blob_base_folder}/{filing_type}/{equity}.pdf" + # set the content type based on the file extension + if blob_path.endswith('.pdf'): + content_type = 'application/pdf' + elif blob_path.endswith('.html'): + content_type = 'text/html' + elif blob_path.endswith('.txt'): + content_type = 'text/plain' + else: + content_type = 'application/octet-stream' + with open(document_path, "rb") as data: try: self.container_client.upload_blob( name=blob_path, data=data, overwrite=True, - content_settings=ContentSettings(content_type='application/pdf') + content_settings=ContentSettings(content_type=content_type) ) except Exception as e: raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") # get the blob url for the uploaded file blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" - upload_results[equity][filing_type] = {"status": "success", - "blob_path": blob_path, - "blob_url": blob_url} + upload_results[equity][filing_type] = { + "status": "success", + "blob_path": blob_path, + "blob_url": blob_url + } logger.info(f'Document has been uploaded to {blob_path}') except Exception as e: upload_results[equity][filing_type] = {"status": "failed", "error": str(e)} diff --git a/backend/llm_config.py b/backend/llm_config.py index 8ad3f9f6..18689240 100644 --- a/backend/llm_config.py +++ b/backend/llm_config.py @@ -3,6 +3,7 @@ from typing import Dict, Optional import json from openai import AzureOpenAI +from langchain_openai import AzureChatOpenAI import os from dotenv import load_dotenv @@ -60,7 +61,7 @@ class Config: class LLMManager: def __init__(self): self.prompts = PromptTemplate() - self._clients: Dict[str, AzureOpenAI] = {} + self._clients: Dict[str, AzureOpenAI | AzureChatOpenAI] = {} self.config: Dict[str, LLMConfig] = { "gpt4o": LLMConfig( api_base=os.getenv('OPENAI_API_BASE'), @@ -76,16 +77,31 @@ def __init__(self): ) } - def get_client(self, client_type: str = "gpt4o") -> AzureOpenAI: - """Get or create an Azure OpenAI client""" - if client_type not in self._clients: + def get_client(self, client_type: str = "gpt4o", use_langchain: bool = False) -> AzureOpenAI | AzureChatOpenAI: + """Get or create an Azure OpenAI client + + Args: + client_type: Type of client to create ("gpt4o" or "embedding") + use_langchain: If True, returns a LangChain AzureChatOpenAI client instead of regular AzureOpenAI + """ + client_key = f"{client_type}_langchain" if use_langchain else client_type + + if client_key not in self._clients: config = self.config[client_type] - self._clients[client_type] = AzureOpenAI( - api_key=config.api_key, - api_version=config.api_version, - base_url=f"{config.api_base}/openai/deployments/{config.model_name}" - ) - return self._clients[client_type] + if use_langchain: + self._clients[client_key] = AzureChatOpenAI( + openai_api_key=config.api_key, + openai_api_version=config.api_version, + azure_endpoint=config.api_base, + deployment_name=config.model_name + ) + else: + self._clients[client_key] = AzureOpenAI( + api_key=config.api_key, + api_version=config.api_version, + base_url=f"{config.api_base}/openai/deployments/{config.model_name}" + ) + return self._clients[client_key] def get_prompt(self, prompt_type: str) -> str: """Get a prompt template by type""" diff --git a/backend/prompts/curation_reports/__init__.py b/backend/prompts/curation_reports/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/prompts/curation_reports/ecommerce.py b/backend/prompts/curation_reports/ecommerce.py new file mode 100644 index 00000000..9ce6a011 --- /dev/null +++ b/backend/prompts/curation_reports/ecommerce.py @@ -0,0 +1,204 @@ +# Structure +report_structure = """ +This report type is focused on ecommerce trends and the industry news this month. + +The report shouild adhere to the following structure: + +1. **Introduction** (no research needed) + - Provide a brief overview of the ecommerce landscape + - Offer context for analyzing recent business trends + +2. **Main Body**: + - One dedicated section for each major ecommerce platform/company in this list: + * Overall industry trends, Amazon, Shopify, Walmart, Target, Home Depot, Lowe's + - Each section should examine the news and highlight any of the following: + * Tracking significant business events (funding, acquisitions, partnerships) + * Analyzing product launches and feature updates + * Shifts in market strategy and positioning + * Identifying emerging patterns across the industry + * Considering competitive responses and market dynamics + +3. No Main Body Sections other than the ones dedicated to each platform/company in the provided list + +4. Conclusion +- A timeline of key events across companies +- Analysis of emerging industry patterns +- Implications for the broader market""" + +query_writer_instructions=""" + +Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. + +Topic for this section: + +``` +{section_topic} +``` + +When generating {number_of_queries} search queries, ensure they: +1. Cover key aspects of the eCommerce topic, such as: + - Recent business events (e.g, funding, mergers, acquisitions) + - Product launches and feature updates + - Shifts in market strategies and competitive positioning + - Emerging industry patterns or trends + - Customer behavior and technological adoption + +2. Include eCommerce-specific terms, company names, or platform features to refine the search + +3. Target recent information by including relevant time markers (e.g.,"Q4 2024", "December 2024") + +4. Seek insights on: + - Comparisions on differentiators between eCommerce plaforms or companies + - Implications of new strategies or technologies in the industry + +5. Focus on credible sources, such as: + - Official announcements, press releases + - Market research reports + - Blogs, forums, and articles on practical implementation or customer feedbacks + +Your queries should be: +- Specific enough to avoid generic results +- Targeted enough to the eCommerce industry and the topic +- Diverse enough to cover all aspects of the section plan +""" + +# Section writer instructions +section_writer_instructions = """ +You are an expert technical writer responsible for crafting one section of an eCommerce report. + +### Title of the section: +``` +{section_title} +``` + +### Topic for this section: +``` +{section_topic} +``` + +### Guidelines for Writing: + +1. **Technical Accuracy:** + - Include specific metrics, dates, and version numbers where applicable. + - Reference concrete business events (e.g., funding, partnerships, product launches). + - Cite official sources like press releases, financial reports, or industry studies. + - Use precise eCommerce terminology (e.g., platform names, market strategies). + +2. **Length and Style:** + - Limit the section to **150-200 words**. + - Avoid any marketing language; maintain a technical and analytical focus. + - Write in clear, simple language suitable for professional readers. + - Start with your **most important insight in bold**. + - Use short paragraphs (2-3 sentences) for better readability. + +3. **Structure:** + - Use `##` for the section title (Markdown format). + - Include only ONE of the following structural elements, if it clarifies your point: + * A **focused Markdown table** comparing 2-3 key metrics, features, or trends: + - Example: | Platform | Key Feature | Date | + * A **short Markdown list** (3-5 items): + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Properly format and indent all structural elements. + +4. **Writing Approach:** + - Include at least one **specific example or case study** related to the eCommerce topic. + - Focus on concrete insights (e.g., measurable impacts of a strategy or feature). + - Prioritize clarity and conciseness—avoid generalizations or unnecessary details. + - Begin directly with the content; no preamble or introductions. + - Emphasize the single most important insight in your analysis. + - Don't include any sources in the content section. Save sources for the sources section. + +5. **Sources:** + - Use the provided source material to support your analysis: + ``` + {context} + ``` + - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT + ``` + - : [Source](url) + ``` + + Here is a good example example: + ``` + Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) + ``` + + This is a bad example: + ``` + Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) + ``` + + - Include title, date, and URL for each source. + +6. **Quality Checks:** + - Strictly adhere to the **150-200 word count** (excluding title and sources). + - Use only one structural element (table or list) where necessary. + - Start with **bold insight** to capture attention. + - Ensure your writing is concise, specific, and actionable. +""" + + +final_section_writer_instructions="""You are an expert technical writer crafting a section that synthesizes information from the rest of the report. + +Title of the section: +``` +{section_title} +``` + +Section description: +``` +{section_topic} +``` + +Available report content: +``` +{context} +``` + +1. Section-Specific Approach: + +For Introduction: +- Use # for report title (Markdown format) +- The title should mention the month and year of the report along with the main ecommerce theme of the month. Here is an example" + +``` +January 2024: eCommerce Trends to Kickstart the New Year +``` + +- 50-100 word limit +- Write in simple and clear language +- Focus on the core motivation for the report in 1-2 paragraphs +- Use a clear narrative arc to introduce the report +- Include NO structural elements (no lists or tables) +- No sources section needed + +For Conclusion/Summary: +- Use ## for section title (Markdown format) +- 100-150 word limit +- Leverage the insights in this report by aligning strategies with market trends, mitigating identified risks, and implementing recommended actions to drive immediate business impact. +- Highlight (bold) the actionable, insightful suggestions +- For comparative reports: + * Must include a focused comparison table using Markdown table syntax + * Table should distill insights from the report + * Keep table entries clear and concise +- For non-comparative reports: + * Only use ONE structural element IF it helps distill the points made in the report: + * Either a focused table comparing items present in the report (using Markdown table syntax) + * Or a short list using proper Markdown list syntax: + - Use `*` or `-` for unordered lists + - Use `1.` for ordered lists + - Ensure proper indentation and spacing +- End with specific next steps or implications +- No sources section needed + +3. Writing Approach: +- Use concrete details over general statements +- Make every word count +- Focus on your single most important point + +4. Quality Checks: +- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section +- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section +- Markdown format +- Do not include word count or any preamble in your response""" \ No newline at end of file diff --git a/backend/prompts/curation_reports/general.py b/backend/prompts/curation_reports/general.py new file mode 100644 index 00000000..55f9f709 --- /dev/null +++ b/backend/prompts/curation_reports/general.py @@ -0,0 +1,65 @@ + +# Prompt to generate a search query to help with planning the report outline +## general prompt: +report_planner_query_writer_instructions=""" +You are an expert technical writer, helping to plan a report. + +Current month and year: {today_date} + +The report will be focused on the following topic: + +``` +{topic} +``` + +The report structure will follow these guidelines: + +``` +{report_organization} +``` + +Your goal is to generate {number_of_queries} search queries that will help gather comprehensive information for planning the report sections. + +The query should: + +1. Be related to the topic +2. Help satisfy the requirements specified in the report organization + +Make the query specific enough to find high-quality, relevant sources while covering the breadth needed for the report structure.""" + +# Prompt generating the report outline +## general prompt: +report_planner_instructions=""" + +You are an expert technical writer, helping to plan a report. + +Your goal is to generate the outline of the sections of the report. + +The overall topic of the report is: + +``` +{topic} +``` + +The report should follow this organization: + +``` +{report_organization} +``` + +You should reflect on this information to plan the sections of the report: + +``` +{context} +``` + +Now, generate the sections of the report. Each section should have the following fields: + +- Name - Name for this section of the report. +- Description - Brief overview of the main topics and concepts to be covered in this section. +- Research - Whether to perform web research for this section of the report. +- Content - The content of the section, which you will leave blank for now. + +Consider which sections require web research. For example, introduction and conclusion will not require research because they will distill information from other parts of the report.""" + + diff --git a/backend/prompts/curation_reports/monthly_economics.py b/backend/prompts/curation_reports/monthly_economics.py new file mode 100644 index 00000000..07f820e9 --- /dev/null +++ b/backend/prompts/curation_reports/monthly_economics.py @@ -0,0 +1,3 @@ +report_structure = """ + +""" \ No newline at end of file diff --git a/backend/prompts/curation_reports/weekly_economics.py b/backend/prompts/curation_reports/weekly_economics.py new file mode 100644 index 00000000..07eed614 --- /dev/null +++ b/backend/prompts/curation_reports/weekly_economics.py @@ -0,0 +1,3 @@ +report_structure = """ + +""" diff --git a/backend/requirements.txt b/backend/requirements.txt index db99eeac..28c929e8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,7 @@ azure-identity==1.15.0 flask==2.2.2 flask-cors==3.0.10 werkzeug==2.2.2 -requests +requests==2.28.2 python-dotenv==1.0.0 azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 @@ -21,9 +21,13 @@ sec-edgar-downloader==5.0.2 pdfkit==1.0.0 pydantic<=2.10.3 tavily-python==0.5.0 -openai<=1.57.4 +openai tiktoken<=0.8.0 pandas<=2.2.3 pymupdf==1.23.7 reportlab==4.2.5 -Flask-Session==0.8.0 \ No newline at end of file +Flask-Session==0.8.0 +langgraph==0.2.60 +markdown2==2.5.2 +langchain-openai==0.2.14 +pydantic-settings==2.7.0 \ No newline at end of file From d5248fe4a287c76239f82d78b35080a317575e54 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 23 Dec 2024 12:23:13 -0500 Subject: [PATCH 253/820] added weekly and monthly economics --- backend/app.py | 53 ++--- backend/curation_report_generator.py | 78 +++++-- backend/prompts/curation_reports/ecommerce.py | 2 +- .../curation_reports/monthly_economics.py | 209 +++++++++++++++++ .../curation_reports/weekly_economics.py | 212 ++++++++++++++++++ 5 files changed, 508 insertions(+), 46 deletions(-) diff --git a/backend/app.py b/backend/app.py index 325f4c7d..115c02c9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2476,35 +2476,26 @@ def generate_summary(): NUM_OF_QUERIES ) -from prompts.curation_reports.ecommerce import report_structure as ecommerce_report_structure -from prompts.curation_reports.monthly_economics import report_structure as monthly_economics_report_structure -from prompts.curation_reports.weekly_economics import report_structure as weekly_economics_report_structure - -report_structure_dict = { - "Ecommerce": ecommerce_report_structure, - "Monthly_Economics": monthly_economics_report_structure, - "Weekly_Economics": weekly_economics_report_structure -} @app.route('/api/reports/generate/curation', methods=['POST']) def generate_report(): try: data = request.get_json() - user_report_type_rqst = data['report_type'] # Will raise KeyError if missing + report_topic_rqst = data['report_topic'] # Will raise KeyError if missing # Validate report type - if user_report_type_rqst not in ALLOWED_CURATION_REPORTS: + if report_topic_rqst not in ALLOWED_CURATION_REPORTS: raise InvalidReportTypeError(f"Invalid report type. Please choose from: {ALLOWED_CURATION_REPORTS}") # Get report configuration - report_type = REPORT_TOPIC_PROMPT_DICT[user_report_type_rqst] - search_days = 7 if user_report_type_rqst in WEEKLY_CURATION_REPORT else 30 + report_topic_prompt = REPORT_TOPIC_PROMPT_DICT[report_topic_rqst] + search_days = 10 if report_topic_rqst in WEEKLY_CURATION_REPORT else 30 # Generate report - logger.info(f"Generating report for {user_report_type_rqst}") + logger.info(f"Generating report for {report_topic_rqst}") report = graph.invoke({ - "topic": report_type, - "report_structure": report_structure_dict[user_report_type_rqst], + "topic": report_topic_prompt, # this is the prompt to to trigger the agent + "report_type": report_topic_rqst, # this is user request "number_of_queries": NUM_OF_QUERIES, "search_mode": "news", "search_days": search_days @@ -2512,7 +2503,12 @@ def generate_report(): # Generate file path current_date = datetime.now() - file_path = Path(f"Reports/Curation_Reports/{user_report_type_rqst}/{current_date.strftime('%B_%Y')}.html") + week_of_month = (current_date.day - 1) // 7 + 1 + if report_topic_rqst in WEEKLY_CURATION_REPORT: + file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}/Week_{week_of_month}.html") + else: + file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}.html") + file_path.parent.mkdir(parents=True, exist_ok=True) # Convert and save report @@ -2524,29 +2520,34 @@ def generate_report(): blob_storage_manager = BlobStorageManager() upload_result = blob_storage_manager.upload_to_blob( file_path=str(file_path), - blob_folder=f"Reports/Curation_Reports/{user_report_type_rqst}" + blob_folder=f"Reports/Curation_Reports/{report_topic_rqst}" ) # Cleanup files logger.info("Cleaning up local files") - file_path.unlink(missing_ok=True) # Delete file try: - file_path.parent.rmdir() # Try to remove directory if empty - except OSError: - pass # Directory not empty or already removed, that's okay + # Use shutil.rmtree to recursively remove directory and all contents + import shutil + if file_path.exists(): + shutil.rmtree(file_path.parent, ignore_errors=True) + logger.info(f"Successfully removed directory: {file_path.parent}") + except Exception as e: + logger.warning(f"Error while cleaning up directory {file_path.parent}: {str(e)}") + # Continue execution even if cleanup fails + pass return jsonify({ 'status': 'success', - 'message': f'Report generated for {user_report_type_rqst}', + 'message': f'Report generated for {report_topic_rqst}', 'report_url': upload_result['blob_url'] }) except KeyError: - logger.error("Missing report_type in request") - return jsonify({'error': 'report_type is required'}), 400 + logger.error("Missing report_topic in request") + return jsonify({'error': 'report_topic is required'}), 400 except InvalidReportTypeError as e: - logger.error(f"Invalid report type: {str(e)}") + logger.error(f"Invalid report topic: {str(e)}") return jsonify({'error': str(e)}), 400 except Exception as e: diff --git a/backend/curation_report_generator.py b/backend/curation_report_generator.py index 53489f7f..39e0761a 100644 --- a/backend/curation_report_generator.py +++ b/backend/curation_report_generator.py @@ -17,15 +17,9 @@ from langchain_core.messages import HumanMessage, SystemMessage from datetime import datetime from financial_doc_processor import BlobStorageManager - +from importlib import import_module from llm_config import LLMManager, LLMConfig - -from prompts.curation_reports.ecommerce import ( - report_structure, - final_section_writer_instructions, - query_writer_instructions, - section_writer_instructions -) +from financial_agent_utils.curation_report_config import WEEKLY_CURATION_REPORT from prompts.curation_reports.general import ( report_planner_query_writer_instructions, @@ -60,6 +54,25 @@ # **kwargs: Additional parameters to pass to the search endpoint (e.g., include_domains) MAX_RESULTS = 3 # for web search query results +REPORT_TYPES = Literal["Ecommerce", "Monthly_Economics", "Weekly_Economics"] + +# get the right system prompt for the report + +class ReportPrompts: + def __init__(self, report_type: str): + try: + # Dynamically import the prompt module based on report type + module_name = report_type.lower() + prompt_module = import_module(f"prompts.curation_reports.{module_name}") + + # Get all prompts from the module + self.report_structure = getattr(prompt_module, 'report_structure') + self.final_section_writer_instructions = getattr(prompt_module, 'final_section_writer_instructions') + self.query_writer_instructions = getattr(prompt_module, 'query_writer_instructions') + self.section_writer_instructions = getattr(prompt_module, 'section_writer_instructions') + except (ImportError, AttributeError) as e: + logger.error(f"Failed to load prompts for report type {report_type}: {str(e)}") + raise ValueError(f"Invalid report type or missing prompts: {report_type}") #################################### # State Definitions @@ -94,8 +107,8 @@ class Queries(BaseModel): class ReportState(TypedDict): topic: str # Report topic - report_structure: str # Report structure search_mode: Literal["general", "news"] # Search topic type + report_type: REPORT_TYPES # Report type number_of_queries: int # Number web search queries to perform per section sections: list[Section] # List of report sections completed_sections: Annotated[list, operator.add] # Send() API key @@ -108,6 +121,7 @@ class ReportStateOutput(TypedDict): class SectionState(TypedDict): search_mode: Literal["general", "news"] # Search topic type + report_type: REPORT_TYPES # Report type search_days: Optional[int] # Only applicable for news topic number_of_queries: int # Number web search queries to perform per section section: Section # Report section @@ -129,14 +143,14 @@ def generate_report_plan(state: ReportState): # Inputs topic = state["topic"] - report_structure = state["report_structure"] + report_type = state["report_type"] number_of_queries = state["number_of_queries"] search_mode = state["search_mode"] search_days = state["search_days"] - # Convert JSON object to string if necessary - if isinstance(report_structure, dict): - report_structure = str(report_structure) + # get the right system prompt for the report + report_prompts = ReportPrompts(report_type) + report_structure = report_prompts.report_structure # Generate search query structured_llm = llm_writing.with_structured_output(Queries) @@ -214,12 +228,14 @@ def generate_queries(state: SectionState): # Get state number_of_queries = state["number_of_queries"] section = state["section"] + report_type = state["report_type"] # Generate queries structured_llm = llm_writing.with_structured_output(Queries) # Format system instructions - system_instructions = query_writer_instructions.format(section_topic=section.description, number_of_queries=number_of_queries) + report_prompts = ReportPrompts(report_type) + system_instructions = report_prompts.query_writer_instructions.format(section_topic=section.description, number_of_queries=number_of_queries) # Generate queries logger.info(f"Generating {number_of_queries} search queries for section {section.name}") @@ -278,9 +294,11 @@ def write_section(state: SectionState): # Get state section = state["section"] source_str = state["source_str"] + report_type = state["report_type"] # Format system instructions - system_instructions = section_writer_instructions.format(section_title=section.name, + report_prompts = ReportPrompts(report_type) + system_instructions = report_prompts.section_writer_instructions.format(section_title=section.name, section_topic=section.description, context=source_str) @@ -340,7 +358,8 @@ def initiate_section_writing(state: ReportState): Send("build_section_with_web_research", {"section": s, "number_of_queries": state["number_of_queries"], "search_mode": state["search_mode"], - "search_days": state["search_days"]}) + "search_days": state["search_days"], + "report_type": state["report_type"]}) for s in state["sections"] if s.research ] @@ -353,9 +372,28 @@ def write_final_sections(state: SectionState): # Get state section = state["section"] completed_report_sections = state["report_sections_from_research"] - + report_type = state["report_type"] + # Format system instructions - system_instructions = final_section_writer_instructions.format(section_title=section.name, section_topic=section.description, context=completed_report_sections) + report_prompts = ReportPrompts(report_type) + + current_date = datetime.now() + week_of_month = (current_date.day - 1) // 7 + 1 + year = current_date.year + current_week_and_month_and_year = f"Current week: {week_of_month}, Current month: {current_date.strftime('%B')}, Current year: {year}" + + if report_type in WEEKLY_CURATION_REPORT: + system_instructions = report_prompts.final_section_writer_instructions.format(section_title=section.name, + section_topic=section.description, + context=completed_report_sections, + current_week_and_month=current_week_and_month_and_year) + ################################################## + # Here we need to include include week so that it can write proper report title + ################################################## + else: + system_instructions = report_prompts.final_section_writer_instructions.format(section_title=section.name, + section_topic=section.description, + context=completed_report_sections) # Generate section logger.info(f"Generating final section content for section {state['section'].name}") @@ -410,7 +448,9 @@ def initiate_final_section_writing(state: ReportState): # Kick off section writing in parallel via Send() API for any sections that do not require research (intro and conclusion) logger.info(f"Kicking off final section writing for non-research sections") return [ - Send("write_final_sections", {"section": s, "report_sections_from_research": state["report_sections_from_research"]}) + Send("write_final_sections", {"section": s, + "report_sections_from_research": state["report_sections_from_research"], + "report_type": state["report_type"]}) for s in state["sections"] if not s.research ] diff --git a/backend/prompts/curation_reports/ecommerce.py b/backend/prompts/curation_reports/ecommerce.py index 9ce6a011..8964c694 100644 --- a/backend/prompts/curation_reports/ecommerce.py +++ b/backend/prompts/curation_reports/ecommerce.py @@ -159,7 +159,7 @@ 1. Section-Specific Approach: For Introduction: -- Use # for report title (Markdown format) +- Use # for report title (Markdown format). You must include a title for the report - The title should mention the month and year of the report along with the main ecommerce theme of the month. Here is an example" ``` diff --git a/backend/prompts/curation_reports/monthly_economics.py b/backend/prompts/curation_reports/monthly_economics.py index 07f820e9..2939754a 100644 --- a/backend/prompts/curation_reports/monthly_economics.py +++ b/backend/prompts/curation_reports/monthly_economics.py @@ -1,3 +1,212 @@ +# Structure report_structure = """ +This report type is focused on analyzing key economic trends and significant events of the past month. +The report shouild adhere to the following structure: + +1. **Introduction** (no research needed) + - Provide a brief overview of the domestic and global economic landscape + - Offer context for understanding the key economic events and trends analyzed in the report + + +2. **Main Body**: + - Organize sections based on the following categories: + * **Global Economic Trends**: + - Overview of major global economic indicators (e.g., GDP growth, inflation, unemployment rates) + - Analysis of significant developments (e.g., central bank policies, trade agreements, geopolitical events) + * **Regional Highlights**: + - Focus on major regions: North America, Europe, Asia-Pacific, and Emerging Markets + - Key trends, policy changes, and regional challenges + * **Industry-Specific Analysis**: + - Highlight significant trends in major industries such as technology, energy, finance, and healthcare + - Include macroeconomic influences and sectoral performance metrics + * **Financial Market Insights**: + - Overview of stock market performance, bond yields, and currency movements + - Analysis of investor sentiment and market outlook + +3. **Conclusion** + - Recap of key economic events and trends for the month + - Emerging global and regional patterns + - Implications for businesses, policymakers, and investors + +""" + +query_writer_instructions=""" + +Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. + +Topic for this section: + +``` +{section_topic} +``` + +When generating {number_of_queries} search queries, ensure they: +1. Cover key aspects of the eCommerce topic, such as: + - Recent global and regional economic events (e.g., GDP growth, inflation, unemployment) + - Central bank policies and monetary decisions + - Trade agreements, geopolitical developments, and regulatory changes + - Industry-specific trends and performance metrics + - Financial market movements (e.g., stock indices, bond yields, currencies) + +2. Include economics-specific terms, key metrics, and relevant regions or countries to refine the search. + +3. Target recent information by including relevant time markers (e.g., "Q4 2024," "December 2024"). + +4. Seek insights on: + - Comparisons of economic indicators across regions or industries + - Implications of policy changes, global events, or economic shifts for businesses and investors + +5. Focus on credible sources, such as: + - Reports from international economic organizations (e.g., IMF, World Bank, OECD) + - Official government or central bank statements + - Market research, industry reports, and financial analyst commentary + - News articles, blogs, and expert opinion pieces on key economic topics + +Your queries should be: +- Specific enough to avoid generic results +- Targeted to the economics topic and region of interest +- Diverse enough to cover all aspects of the section plan +""" + +# Section writer instructions +section_writer_instructions = """ +You are an expert technical writer responsible for crafting one section of a Monthly Economics Report. + +### Title of the section: +``` +{section_title} +``` + +### Topic for this section: +``` +{section_topic} +``` + +### Guidelines for Writing: + +1. **Technical Accuracy:** + - Include specific metrics, dates, and key economic indicators (e.g., GDP growth, inflation rates, unemployment figures). + - Reference concrete events (e.g., central bank decisions, trade agreements, geopolitical developments). + - Cite official sources such as government reports, financial analyses, or statements from international organizations. + - Use precise economic terminology and maintain clarity. + +2. **Length and Style:** + - Limit the section to **150-200 words**. + - Maintain an analytical and professional tone; avoid opinionated or speculative language. + - Write in clear, concise language suitable for policymakers, analysts, and professionals. + - Start with your **most important insight in bold**. + - Use short paragraphs (2-3 sentences) for better readability. + +3. **Structure:** + - Use `##` for the section title (Markdown format). + - Include only ONE of the following structural elements if relevant: + * A **focused Markdown table** summarizing key metrics or comparisons: + - Example: | Region | GDP Growth (%) | Inflation (%) | + * A **short Markdown list** (3-5 items): + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Properly format and indent all structural elements. + +4. **Writing Approach:** + - Include at least one **specific example or case study** related to the economic topic. + - Focus on actionable insights (e.g., implications of a policy change or economic trend). + - Avoid generalizations or excessive detail; prioritize clarity and conciseness. + - Begin directly with the content; avoid introductions or background that restates the title or topic. + - Emphasize the single most critical insight in your analysis. + - Do not include sources in the main content; list them in the sources section. + +5. **Sources:** + - Use the provided source material to support your analysis: + ``` + {context} + ``` + - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT + ``` + - : [Source](url) + ``` + + Here is a good example example: + ``` + Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) + ``` + + This is a bad example: + ``` + Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) + ``` + + - Include title, date, and URL for each source. + +6. **Quality Checks:** + - Strictly adhere to the **150-200 word count** (excluding title and sources). + - Use only one structural element (table or list) where necessary. + - Start with **bold insight** to capture attention. + - Ensure your writing is concise, specific, and actionable. +""" + + +final_section_writer_instructions="""You are an expert technical writer crafting a section that synthesizes information from the rest of the report. + +Title of the section: +``` +{section_title} +``` + +Section description: +``` +{section_topic} +``` + +Available report content: +``` +{context} +``` + +1. Section-Specific Approach: + +For Introduction: +- Use # for report title (Markdown format). You must include a title for the report +- The title should mention the month and year of the report along with the main economic theme of the month. Example: + +``` +January 2024: Key Economic Trends Shaping the Global Landscape +``` + +- 50-100 word limit +- Write in simple and clear language +- Focus on the purpose and scope of the report in 1-2 paragraphs +- Use a concise narrative arc to introduce the report +- Include NO structural elements (no lists or tables) +- No sources section needed + +For Conclusion/Summary: +- Use ## for section title (Markdown format) +- 100-150 word limit +- Leverage the insights from this report by identifying actionable strategies for policymakers, businesses, or investors to address risks and capitalize on trends. +- Highlight (bold) key takeaways and actionable insights. +- For comparative reports: + * Must include a focused comparison table using Markdown table syntax. + * Table should distill insights from the report. + * Keep table entries clear and concise. +- For non-comparative reports: + * Use ONLY ONE structural element IF it helps clarify points made in the report: + * Either a focused table summarizing key metrics or findings (using Markdown table syntax). + * Or a short list using proper Markdown list syntax: + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Ensure proper indentation and spacing. +- End with actionable implications or recommendations. +- No sources section needed. + +3. Writing Approach: +- Prioritize concrete details over generalizations. +- Ensure every word contributes to clarity and precision. +- Focus on the single most critical insight for each section. + +4. Quality Checks: +- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section. +- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section. +- Use Markdown format. +- Do not include word count or any preamble in your response. """ \ No newline at end of file diff --git a/backend/prompts/curation_reports/weekly_economics.py b/backend/prompts/curation_reports/weekly_economics.py index 07eed614..7aa07dd7 100644 --- a/backend/prompts/curation_reports/weekly_economics.py +++ b/backend/prompts/curation_reports/weekly_economics.py @@ -1,3 +1,215 @@ +# Structure report_structure = """ +This report type is focused on analyzing key economic trends and significant events of the past week. +The report should adhere to the following structure: + +1. **Introduction** (no research needed) + - Provide a brief overview of the domestic and global economic landscape for the week. + - Offer context for understanding the key economic events and trends analyzed in the report. + +2. **Main Body**: + - Organize sections based on the following categories: + * **Global Economic Trends**: + - Overview of major global economic indicators for the week (e.g., GDP updates, inflation snapshots, unemployment figures). + - Analysis of significant developments (e.g., central bank announcements, trade disputes, geopolitical updates). + * **Regional Highlights**: + - Focus on major regions: North America, Europe, Asia-Pacific, and Emerging Markets. + - Key events, policy changes, and notable economic challenges. + * **Industry-Specific Updates**: + - Highlight weekly developments in key industries such as technology, energy, finance, and healthcare. + - Brief analysis of macroeconomic influences on sectoral performance. + * **Financial Market Movements**: + - Weekly performance of stock markets, bond yields, and currency movements. + - Analysis of investor sentiment and short-term market trends. + +3. **Conclusion** + - Recap of key economic events and trends for the week. + - Emerging global and regional patterns. + - Implications for businesses, policymakers, and investors over the near term. +""" + +query_writer_instructions=""" + +Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. + +Topic for this section: + +``` +{section_topic} +``` + +When generating {number_of_queries} search queries, ensure they: +1. Cover key aspects of the weekly economic topic, such as: + - Major global and regional economic events of the week (e.g., GDP updates, inflation reports, unemployment rates) + - Central bank statements or decisions announced during the week + - Significant trade agreements, geopolitical developments, or regulatory changes + - Weekly trends in specific industries and performance metrics + - Financial market movements (e.g., weekly stock index changes, bond yields, currency fluctuations) + +2. Include economics-specific terms, key metrics, and relevant regions or countries to refine the search. + +3. Target recent information by including weekly time markers (e.g., "week of December 18, 2024," "last week December 2024"). + +4. Seek insights on: + - Week-to-week comparisons of economic indicators across regions or industries + - Immediate implications of new policies, global events, or economic shifts for businesses, policymakers, and investors + +5. Focus on credible sources, such as: + - Reports and updates from international economic organizations (e.g., IMF, World Bank, OECD) + - Central bank announcements and government statements + - Market research, weekly financial analyses, and expert commentary + - News articles or blogs providing real-time insights on economic events + +Your queries should be: +- Specific enough to avoid generic results +- Focused on recent events relevant to the week in review +- Diverse enough to cover all aspects of the section plan """ + +# Section writer instructions +section_writer_instructions = """ +You are an expert technical writer responsible for crafting one section of a Weekly Economics Report. + +### Title of the section: +``` +{section_title} +``` + +### Topic for this section: +``` +{section_topic} +``` + +### Guidelines for Writing: + +1. **Technical Accuracy:** + - Include specific metrics, dates, and key economic indicators relevant to the week (e.g., GDP updates, weekly inflation rates, unemployment figures). + - Reference concrete events (e.g., central bank announcements, trade negotiations, geopolitical updates). + - Cite official sources such as government releases, financial analyses, or reports from international organizations. + - Use precise economic terminology while maintaining clarity. + +2. **Length and Style:** + - Limit the section to **150-200 words**. + - Maintain an analytical and professional tone; avoid subjective or speculative language. + - Write in clear, concise language suitable for policymakers, investors, and professionals. + - Start with your **most important insight in bold**. + - Use short paragraphs (2-3 sentences) for better readability. + +3. **Structure:** + - Use `##` for the section title (Markdown format). + - Include only ONE of the following structural elements if relevant: + * A **focused Markdown table** summarizing key weekly metrics or comparisons: + - Example: | Indicator | Value | Date | + * A **short Markdown list** (3-5 items): + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Properly format and indent all structural elements. + +4. **Writing Approach:** + - Include at least one **specific example or case study** relevant to the economic topic of the week. + - Focus on actionable insights (e.g., immediate implications of a policy change or trend). + - Avoid generalizations or excessive background information; prioritize clarity and conciseness. + - Begin directly with the content; avoid introductions or redundant restatements of the title or topic. + - Highlight the single most important takeaway in your analysis. + - Do not include sources in the main content; list them in the sources section. + +5. **Sources:** + - Use the provided source material to support your analysis: + ``` + {context} + ``` + - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT + ``` + - : [Source](url) + ``` + + Here is a good example example: + ``` + Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) + ``` + + This is a bad example: + ``` + Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) + ``` + + - Include title, date, and URL for each source. + +6. **Quality Checks:** + - Strictly adhere to the **150-200 word count** (excluding title and sources). + - Use only one structural element (table or list) where necessary. + - Start with **bold insight** to capture attention. + - Ensure your writing is concise, specific, and actionable. +""" + +final_section_writer_instructions=""" +You are an expert technical writer crafting a section that synthesizes information from the rest of the report. + +Current week and month: +``` +{current_week_and_month} +``` + +Title of the section: +``` +{section_title} +``` + +Section description: +``` +{section_topic} +``` + +Available report content: +``` +{context} +``` + +1. Section-Specific Approach: + +For Introduction: +- Use # for report title (Markdown format). You must include a title for the report +- The title should mention the week and month of the report along with the main economic theme of the week. Example: + +``` +Week 1 of January 2024: Key Economic Trends Shaping the Global Landscape +``` + +- 50-100 word limit +- Write in simple and clear language +- Focus on the purpose and scope of the report in 1-2 paragraphs +- Use a concise narrative arc to introduce the report +- Include NO structural elements (no lists or tables) +- No sources section needed + +For Conclusion/Summary: +- Use ## for section title (Markdown format). +- 100-150 word limit. +- Leverage the insights from this report by identifying actionable strategies for policymakers, businesses, or investors to address risks and capitalize on trends. +- Highlight (bold) key takeaways and actionable insights. +- For comparative reports: + * Must include a focused comparison table using Markdown table syntax. + * Table should distill insights from the report. + * Keep table entries clear and concise. +- For non-comparative reports: + * Use ONLY ONE structural element IF it helps clarify points made in the report: + * Either a focused table summarizing key metrics or findings (using Markdown table syntax). + * Or a short list using proper Markdown list syntax: + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Ensure proper indentation and spacing. +- End with actionable implications or recommendations. +- No sources section needed. + +3. Writing Approach: +- Prioritize concrete details over generalizations. +- Ensure every word contributes to clarity and precision. +- Focus on the single most critical insight for each section. + +4. Quality Checks: +- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section. +- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section. +- Use Markdown format. +- Do not include word count or any preamble in your response. +""" \ No newline at end of file From 81b9b7cf484dd05b5ee1f5f0d26ff1e9fa605c57 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Mon, 23 Dec 2024 14:17:10 -0400 Subject: [PATCH 254/820] FA-313 create report managemant landing page (#229) --- .../src/pages/reports/ReportManagement.tsx | 85 ++++++++++++++++++- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/reports/ReportManagement.tsx b/frontend/src/pages/reports/ReportManagement.tsx index cdc0dede..58dff77a 100644 --- a/frontend/src/pages/reports/ReportManagement.tsx +++ b/frontend/src/pages/reports/ReportManagement.tsx @@ -1,11 +1,88 @@ import React from "react"; -import styles from "./ReportManagement.module.css"; +import { useNavigate } from "react-router-dom"; +import { DocumentData24Regular, DocumentText24Regular, DocumentTable24Regular } from "@fluentui/react-icons"; + +interface ReportCardProps { + title: string; + description: string; + icon: React.ElementType; + onClick: () => void; +} + +interface CardData { + title: string; + description: string; + icon: React.ElementType; + route: string; +} + +const ReportCard: React.FC = ({ title, description, icon: Icon, onClick }) => { + return ( +

    (e.currentTarget.style.transform = "translateY(-4px)")} + onMouseOut={e => (e.currentTarget.style.transform = "translateY(0)")} + > +
    +
    +
    + +
    +
    +

    {title}

    +

    {description}

    +
    +
    + ); +}; const ReportManagement: React.FC = () => { + const navigate = useNavigate(); + + const cards: CardData[] = [ + { + title: "Curation Reports", + description: "Manage and create custom curation reports", + icon: DocumentData24Regular, + route: "/curation-reports" + }, + { + title: "Summarization Reports", + description: "Create and manage custom summarization reports", + icon: DocumentText24Regular, + route: "/summarization-reports" + }, + { + title: "Summarization Report Templates", + description: "Manage report templates", + icon: DocumentTable24Regular, + route: "/report-templates" + } + ]; + + const handleNavigation = (route: string): void => { + navigate(route); + }; + return ( -
    -

    Report Management

    -

    Welcome to the Report Management page!

    +
    +
    +
    +

    Report Management

    +
    +
    + +
    +
    + {cards.map((card, index) => ( +
    + handleNavigation(card.route)} /> +
    + ))} +
    +
    ); }; From 0ab8df6e06cb959979189dfc8dc76bcf01599c30 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:12:30 -0400 Subject: [PATCH 255/820] Fa 280 build UI for create curation report (#228) * FA-280 Adding the Curation Reports page ver0.1 * FA-280-Finishing the Curation Report UI Page receiving the info from the endpoints * FA-280 Various changes to the Curation Report page and adding the delete report function * FA-280 Separating the creation page and adding the Curation page to the userflow in Report Management --- frontend/src/App.tsx | 16 ++ frontend/src/api/api.ts | 17 ++ frontend/src/components/Sidebar/Sidebar.tsx | 3 +- .../pages/reports/CurationReports.module.css | 269 ++++++++++++++++++ .../src/pages/reports/CurationReports.tsx | 164 +++++++++++ .../CurationCreation.module.css | 216 ++++++++++++++ .../ReportCreation/CurationCreation.tsx | 126 ++++++++ 7 files changed, 810 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/reports/CurationReports.module.css create mode 100644 frontend/src/pages/reports/CurationReports.tsx create mode 100644 frontend/src/pages/reports/ReportCreation/CurationCreation.module.css create mode 100644 frontend/src/pages/reports/ReportCreation/CurationCreation.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5dc71b8a..ca8f0b4f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,8 @@ import SubscriptionManagement from "./pages/subscriptionmanagement/SubscriptionM import UserManagement from "./pages/usermanagement/UserManagement"; import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; +import CurationReports from "./pages/reports/CurationReports"; +import CurationCreation from "./pages/reports/ReportCreation/CurationCreation"; export default function App() { return ( @@ -112,6 +114,20 @@ export default function App() { + + } + > + }> + } /> + }/> + + + {/* Catch-All Route for Undefined Paths */} } /> diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index fa0f9693..8374ad18 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -619,4 +619,21 @@ export async function getFilteredReports(type?: string) { const reports = await response.json(); return reports; +} + +export async function deleteReport(reportId: string) { + const response = await fetch(`/api/reports/${encodeURIComponent(reportId)}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.status === 404) { + throw Error(`Report with ID ${reportId} not found`); + } + + if (response.status > 299 || !response.ok) { + throw Error(`Error deleting report with ID ${reportId}`); + } } \ No newline at end of file diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index a17634a6..ebf07195 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -15,7 +15,8 @@ import { IconHeadset, IconDots, IconSubtask, - IconReportMoney + IconReportMoney, + IconClipboardText } from "@tabler/icons-react"; import salesLogo from "../../img/logo.png"; import styles from "./Sidebar.module.css"; diff --git a/frontend/src/pages/reports/CurationReports.module.css b/frontend/src/pages/reports/CurationReports.module.css new file mode 100644 index 00000000..91c0c89d --- /dev/null +++ b/frontend/src/pages/reports/CurationReports.module.css @@ -0,0 +1,269 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.elementrow{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + justify-items: center; + margin-bottom: 10px; + height: 40px; + align-items: center; + margin-top: 30px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 20px; + margin-top: 15px; + flex-grow: 2; +} + +.iconColor{ + color: #a7c444; +} + +.iconLarge{ + color: #a7c444; + width: 3em; + height: 3em; +} + +.container { + display: flex; + justify-content: center; + gap: 300px; + width: 100%; +} + +.labelContainer{ + display: flex; + justify-content: flex-start; + width: 100%; +} + +.text{ + text-align: center; +} + +.textButton{ + text-align: center; + cursor: pointer; + margin-left: 10px; +} + +.textPanel{ + text-align: center; + margin-bottom: 10px; +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.button:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonPanel{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: 40px; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.buttonPanel:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.buttonPanel:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +@media (max-width: 900px){ + .buttonContainer{ + gap: 10px; + } +} + +.table { + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +@media (max-width: 900px){ + .table{ + font-size: 0.7em; + } +} + + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +@media (max-width: 900px) { + .modal{ + width: 20em; + } + .info{ + width: 10em; + } +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.thead{ + background-color: #a7c444; + color: white; +} + +.tableName{ + padding: 10px; +} + +.tableText{ + text-align: justify; +} + +.tableBackground{ + background-color: white; +} + +.tableBackgroundAlt{ + background-color: #e9e9e9; +} + +.tableStatusContainer{ + width: 100%; + justify-content: flex-start; + justify-items: center; + text-align: center; + display: flex; + text-transform: capitalize; +} + +.tableStatusActive{ + width: 100px; + background-color: #d2ffd2; + padding: 5px; + color: #a0df2c; + border-radius: 15px; +} + +.tableStatusArchived{ + width: 100px; + background-color: #c6f0ee; + padding: 5px; + color: #2ca6df; + border-radius: 15px; +} diff --git a/frontend/src/pages/reports/CurationReports.tsx b/frontend/src/pages/reports/CurationReports.tsx new file mode 100644 index 00000000..7ae8c54a --- /dev/null +++ b/frontend/src/pages/reports/CurationReports.tsx @@ -0,0 +1,164 @@ +import React, { useEffect, useState } from "react"; +import styles from "./CurationReports.module.css" +import { Label, Spinner } from "@fluentui/react"; +import { IconArrowBack, IconFilePlus, IconTrash, IconX } from "@tabler/icons-react"; +import { deleteReport, getFilteredReports } from "../../api"; +import { useNavigate } from "react-router-dom"; + +const CurationReports = () => { + + const navigate = useNavigate(); + const [dataLoad, setDataLoad] = useState(false) + const [filteredReports, setFilteredReports] = useState([]); + const [loading, setLoading] = useState(false); + const [isDeleteActive, setIsDeleteActive] = useState(false) + const [deletedReportID, setdeletedReportID] = useState(''); + const [deletedReportName, setdeletedReportName] = useState(''); + + useEffect(() => { + const getReportList = async () => { + + setLoading(true); + + try { + let reportList = await getFilteredReports('curation') + + if (!Array.isArray(reportList)) { + reportList = []; + } + + setFilteredReports(reportList); + } catch (error) { + console.error("Error fetching user list:", error); + setFilteredReports([]); + } finally { + setLoading(false); + } + }; + + getReportList(); + + + }, [dataLoad]); + + const handleDeleteButton = (reportID: string, reportName: string) => { + setIsDeleteActive(!isDeleteActive); + setdeletedReportID(reportID) + setdeletedReportName(reportName) + } + + const handleConfirmDelete = async () => { + setIsDeleteActive(!isDeleteActive) + + try{ + await deleteReport(deletedReportID) + setDataLoad(!dataLoad) + } catch (error){ + console.error("Error trying to delete the report: ", error) + }finally{ + setLoading(false) + } + } + + const handleCancelDelete = () => { + setIsDeleteActive(!isDeleteActive) + setdeletedReportID('') + setdeletedReportName('') + } + + return ( + +
    +
    + +
    +
    +

    Curation Reports

    +
    +
    +
    + +
    + {loading ? ( + + ) : ( + + + + + + + + + + + + {filteredReports.length > 0 ? ( + filteredReports.map((report, index) => ( + + + + + + + + )) + ) : ( + + + + )} + +
    NameCategoryCreated AtStatusActions
    + {report.name} + + {report.category} + +
    +
    + {new Date(report.createAt).toLocaleDateString()} +
    +
    +
    +
    +
    + {report.status} +
    +
    +
    +
    + +
    +
    + +
    + )} +
    + {isDeleteActive && ( +
    + + +
    + + +
    +
    + )} +
    + ); +} + + +export default CurationReports; diff --git a/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css b/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css new file mode 100644 index 00000000..50e0527c --- /dev/null +++ b/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css @@ -0,0 +1,216 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.labelContainer{ + display: flex; + justify-content: flex-start; + width: 100%; +} + +.card { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 50px; + margin-top: 15px; + flex-grow: 2; +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.textButton{ + text-align: center; + cursor: pointer; + margin-left: 10px; +} + +.button:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: flex-start; + display: flex; + gap: 40px; +} + +.iconColor{ + color: #a7c444; +} + +.input{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: 25em; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.input:focus{ + border: 2px solid #8eb11a; + outline: none; + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +@media (max-width: 900px) { + .input{ + width: 16em; + } +} + +.error { + text-align: center; + font-weight: 400; + font-style: italic; + color: #ff3333; +} + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +.modalPopup{ + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + padding: 100px; + background-color: #cef1a5; + border: 1px solid #87f083; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +@media (max-width: 900px){ + .modal{ + width: 20em; + height: 10em; + padding: 20px; + } + .modalPopup{ + width: 20em; + } +} + +.buttonModalContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +.text{ + text-align: center; +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx b/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx new file mode 100644 index 00000000..31070516 --- /dev/null +++ b/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx @@ -0,0 +1,126 @@ +import React, { useState } from "react"; +import styles from "./CurationCreation.module.css" +import { IconArrowBack, IconX } from "@tabler/icons-react"; +import { Dropdown, Label, ResponsiveMode } from "@fluentui/react"; +import { useNavigate } from "react-router-dom"; +import { createReport } from "../../../api"; + +const CurationCreation: React.FC = () => { + const navigate = useNavigate(); + const [inputReportName, setinputReportName] = useState('') + const curationReportOptions = [ + { key: "1", text: "Ecommerce" }, + { key: "2", text: "Weekly Economic" }, + { key: "3", text: "Monthly Economic" }, + ]; + const [categorySelection, setCategorySelection] = useState('') + const [errorMessage, setErrorMessage] = useState(""); + const [isConfirm, setIsConfirm] = useState(false); + const [isPopupActive, setIsPopupActive] = useState(false) + + const handleTypeDropdownChange = (event: any, selectedOption: any) => { + setCategorySelection(selectedOption.text) + } + + const handleInputName = (event: React.ChangeEvent) => { + setinputReportName(event.target.value) + } + + const handleConfirmButton = () => { + + if(inputReportName == ('')){ + setErrorMessage('Please type the Name of the Report') + return; + } + if(categorySelection == ('')){ + setErrorMessage('Please select the Report Category') + return; + } + setIsConfirm(!isConfirm); + } + + const handleCancelButton = () => { + setIsConfirm(false) + setinputReportName('') + setErrorMessage(null) + } + + const handleCreateReport = async () => { + setIsConfirm(false) + let timer: NodeJS.Timeout; + + try{ + await createReport({ + type: "curation", + name: inputReportName, + category: categorySelection, + status: "archived" + }); + + setIsPopupActive(true) + timer = setTimeout(() => { + setIsPopupActive(false); + }, 3000); + + } catch (error){ + console.error("Error trying to create the report: ", error); + } + } + + return ( +
    +
    + +
    +
    +

    Curation Report Creation

    +
    +
    +
    +
    + + + + + +
    +
    + {errorMessage !== null &&

    {errorMessage}

    } +
    + + +
    +
    +
    + {isConfirm && ( +
    + + +
    + + +
    +
    + )} + {isPopupActive && ( +
    + +
    + )} +
    + ); +}; + +export default CurationCreation; From 36872646365e43fc8a30a45e08d95b29c4e5ec3f Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 23 Dec 2024 21:21:51 -0400 Subject: [PATCH 256/820] FA-313-Create Summarization Reports page (#231) --- frontend/src/App.tsx | 4 + .../SummarizationCreation.module.css | 216 +++++++++++++++++ .../ReportCreation/SummarizationCreation.tsx | 130 +++++++++++ .../reports/SummarizationReports.module.css | 219 ++++++++++++++++++ .../pages/reports/SummarizationReports.tsx | 163 +++++++++++++ frontend/vite.config.ts | 8 +- 6 files changed, 739 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/reports/ReportCreation/SummarizationCreation.module.css create mode 100644 frontend/src/pages/reports/ReportCreation/SummarizationCreation.tsx create mode 100644 frontend/src/pages/reports/SummarizationReports.module.css create mode 100644 frontend/src/pages/reports/SummarizationReports.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ca8f0b4f..106d723b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -22,6 +22,8 @@ import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; import CurationReports from "./pages/reports/CurationReports"; import CurationCreation from "./pages/reports/ReportCreation/CurationCreation"; +import SummarizationReports from "./pages/reports/SummarizationReports"; +import SummarizationCreation from "./pages/reports/ReportCreation/SummarizationCreation"; export default function App() { return ( @@ -125,6 +127,8 @@ export default function App() { }> } /> }/> + }/> + }/> diff --git a/frontend/src/pages/reports/ReportCreation/SummarizationCreation.module.css b/frontend/src/pages/reports/ReportCreation/SummarizationCreation.module.css new file mode 100644 index 00000000..50e0527c --- /dev/null +++ b/frontend/src/pages/reports/ReportCreation/SummarizationCreation.module.css @@ -0,0 +1,216 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.labelContainer{ + display: flex; + justify-content: flex-start; + width: 100%; +} + +.card { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 50px; + margin-top: 15px; + flex-grow: 2; +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.textButton{ + text-align: center; + cursor: pointer; + margin-left: 10px; +} + +.button:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: flex-start; + display: flex; + gap: 40px; +} + +.iconColor{ + color: #a7c444; +} + +.input{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: 25em; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.input:focus{ + border: 2px solid #8eb11a; + outline: none; + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +@media (max-width: 900px) { + .input{ + width: 16em; + } +} + +.error { + text-align: center; + font-weight: 400; + font-style: italic; + color: #ff3333; +} + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +.modalPopup{ + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + padding: 100px; + background-color: #cef1a5; + border: 1px solid #87f083; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +@media (max-width: 900px){ + .modal{ + width: 20em; + height: 10em; + padding: 20px; + } + .modalPopup{ + width: 20em; + } +} + +.buttonModalContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +.text{ + text-align: center; +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportCreation/SummarizationCreation.tsx b/frontend/src/pages/reports/ReportCreation/SummarizationCreation.tsx new file mode 100644 index 00000000..ce2bbef1 --- /dev/null +++ b/frontend/src/pages/reports/ReportCreation/SummarizationCreation.tsx @@ -0,0 +1,130 @@ +import React, { useState } from "react"; +import styles from "./SummarizationCreation.module.css" +import { useNavigate } from "react-router-dom"; +import { createReport } from "../../../api"; +import { IconArrowBack, IconX } from "@tabler/icons-react"; +import { Dropdown, Label, ResponsiveMode } from "@fluentui/react"; + +const SummarizationCreation: React.FC = () => { + const navigate = useNavigate(); + const [inputReportTicker, setinputReportTicker] = useState('') + const summarizationReportOptions = [ + { key: "1", text: "10-K" }, + { key: "2", text: "10-Q" }, + { key: "3", text: "8-K" }, + { key: "4", text: "DEF 14A"} + ]; + const [typeSelection, setTypeSelection] = useState('') + const [errorMessage, setErrorMessage] = useState(""); + const [isConfirm, setIsConfirm] = useState(false); + const [isPopupActive, setIsPopupActive] = useState(false) + + const handleTypeDropdownChange = (event: any, selectedOption: any) => { + setTypeSelection(selectedOption.text) + } + + const handleInputName = (event: React.ChangeEvent) => { + setinputReportTicker(event.target.value) + } + + const handleConfirmButton = () => { + + if(inputReportTicker == ('')){ + setErrorMessage('Please type the Stock Ticker of the Report') + return; + } + if(typeSelection == ('')){ + setErrorMessage('Please select the Report Type') + return; + } + setIsConfirm(!isConfirm); + } + + const handleCancelButton = () => { + setIsConfirm(false) + setinputReportTicker('') + setErrorMessage(null) + } + + const handleCreateReport = async () => { + setIsConfirm(false) + let timer: NodeJS.Timeout; + + try{ + await createReport({ + type: "companySummarization", + name: inputReportTicker + ' + ' + typeSelection, + reportTemplate: typeSelection, + companyTickers: inputReportTicker, + status: "archived" + }); + + setIsPopupActive(true) + timer = setTimeout(() => { + setIsPopupActive(false); + }, 3000); + + } catch (error){ + console.error("Error trying to create the report: ", error); + }finally{ + setinputReportTicker('') + } + } + + return ( +
    +
    + +
    +
    +

    Summarization Report Creation

    +
    +
    +
    +
    + + + + + +
    +
    + {errorMessage !== null &&

    {errorMessage}

    } +
    + + +
    +
    +
    + {isConfirm && ( +
    + + +
    + + +
    +
    + )} + {isPopupActive && ( +
    + +
    + )} +
    + ); +}; + +export default SummarizationCreation; \ No newline at end of file diff --git a/frontend/src/pages/reports/SummarizationReports.module.css b/frontend/src/pages/reports/SummarizationReports.module.css new file mode 100644 index 00000000..ebcfce1d --- /dev/null +++ b/frontend/src/pages/reports/SummarizationReports.module.css @@ -0,0 +1,219 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 20px; + margin-top: 15px; + flex-grow: 2; +} + +.iconColor{ + color: #a7c444; +} + +.labelContainer{ + display: flex; + justify-content: flex-start; + width: 100%; +} + +.text{ + text-align: center; +} + +.textButton{ + text-align: center; + cursor: pointer; + margin-left: 10px; +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.button:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +@media (max-width: 900px){ + .buttonContainer{ + gap: 10px; + } +} + +.table { + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +@media (max-width: 900px){ + .table{ + font-size: 0.7em; + } +} + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +@media (max-width: 900px) { + .modal{ + width: 20em; + } + .info{ + width: 10em; + } +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.thead{ + background-color: #a7c444; + color: white; +} + +.tableName{ + padding: 10px; +} + +.tableText{ + text-align: justify; +} + +.tableBackground{ + background-color: white; +} + +.tableBackgroundAlt{ + background-color: #e9e9e9; +} + +.tableStatusContainer{ + width: 100%; + justify-content: flex-start; + justify-items: center; + text-align: center; + display: flex; + text-transform: capitalize; +} + +.tableStatusActive{ + width: 100px; + background-color: #d2ffd2; + padding: 5px; + color: #a0df2c; + border-radius: 15px; +} + +.tableStatusArchived{ + width: 100px; + background-color: #c6f0ee; + padding: 5px; + color: #2ca6df; + border-radius: 15px; +} diff --git a/frontend/src/pages/reports/SummarizationReports.tsx b/frontend/src/pages/reports/SummarizationReports.tsx new file mode 100644 index 00000000..aeeebc54 --- /dev/null +++ b/frontend/src/pages/reports/SummarizationReports.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useState } from "react"; +import styles from "./SummarizationReports.module.css" +import { useNavigate } from "react-router-dom"; +import { deleteReport, getFilteredReports } from "../../api"; +import { IconArrowBack, IconFilePlus, IconTrash, IconX } from "@tabler/icons-react"; +import { Label, Spinner } from "@fluentui/react"; + +const SummarizationReports: React.FC = () => { + + const navigate = useNavigate(); + const [dataLoad, setDataLoad] = useState(false) + const [filteredReports, setFilteredReports] = useState([]); + const [loading, setLoading] = useState(false); + const [isDeleteActive, setIsDeleteActive] = useState(false) + const [deletedReportID, setdeletedReportID] = useState(''); + const [deletedReportName, setdeletedReportName] = useState(''); + + useEffect(() => { + const getReportList = async () => { + + setLoading(true); + + try { + let reportList = await getFilteredReports('companySummarization') + + if (!Array.isArray(reportList)) { + reportList = []; + } + + setFilteredReports(reportList); + } catch (error) { + console.error("Error fetching user list:", error); + setFilteredReports([]); + } finally { + setLoading(false); + } + }; + + getReportList(); + + + }, [dataLoad]); + + const handleDeleteButton = (reportID: string, reportName: string) => { + setIsDeleteActive(!isDeleteActive); + setdeletedReportID(reportID) + setdeletedReportName(reportName) + } + + const handleConfirmDelete = async () => { + setIsDeleteActive(!isDeleteActive) + + try{ + await deleteReport(deletedReportID) + setDataLoad(!dataLoad) + } catch (error){ + console.error("Error trying to delete the report: ", error) + }finally{ + setLoading(false) + } + } + + const handleCancelDelete = () => { + setIsDeleteActive(!isDeleteActive) + setdeletedReportID('') + setdeletedReportName('') + } + + return ( + +
    +
    + +
    +
    +

    Summarization Reports

    +
    +
    +
    + +
    + {loading ? ( + + ) : ( + + + + + + + + + + + + {filteredReports.length > 0 ? ( + filteredReports.map((report, index) => ( + + + + + + + + )) + ) : ( + + + + )} + +
    TickerReport TypeCreated AtStatusActions
    + {report.companyTickers} + + {report.reportTemplate} + +
    +
    + {new Date(report.createAt).toLocaleDateString()} +
    +
    +
    +
    +
    + {report.status} +
    +
    +
    +
    + +
    +
    + +
    + )} +
    + {isDeleteActive && ( +
    + + +
    + + +
    +
    + )} +
    + ); +} + +export default SummarizationReports; \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index c18c6b92..c57fc62a 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -41,7 +41,13 @@ export default defineConfig({ "^/api/subscriptions/.*?/tiers": { target: "http://localhost:8000", changeOrigin: true - } + }, + "^/api/subscription/.*?/financialAssistant": { + target: "http://localhost:8000/", + changeOrigin: true + }, + "/api/reports":"http://localhost:8000/", + "/api/reports/":"http://localhost:8000/" }, host: true } From 2da25c6386b4de1586538c68f6e01a3e5ae04bfc Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 26 Dec 2024 09:11:31 -0500 Subject: [PATCH 257/820] FA 294 curation report (#230) * FA-294 Curation Report * added weekly and monthly economics --- .../Curation_Report_Agent.png | Bin 0 -> 31974 bytes backend/app.py | 94 ++++ backend/curation_report_generator.py | 498 ++++++++++++++++++ .../curation_report_config.py | 10 + .../curation_report_tools/__init__.py | 0 .../curation_report_tools/web_search.py | 150 ++++++ .../curation_report_utils.py | 27 + backend/financial_doc_processor.py | 157 +++++- backend/llm_config.py | 36 +- backend/prompts/curation_reports/__init__.py | 0 backend/prompts/curation_reports/ecommerce.py | 204 +++++++ backend/prompts/curation_reports/general.py | 65 +++ .../curation_reports/monthly_economics.py | 212 ++++++++ .../curation_reports/weekly_economics.py | 215 ++++++++ backend/requirements.txt | 10 +- 15 files changed, 1653 insertions(+), 25 deletions(-) create mode 100644 backend/Agent_Graph_Images/Curation_Report_Agent.png create mode 100644 backend/curation_report_generator.py create mode 100644 backend/financial_agent_utils/curation_report_config.py create mode 100644 backend/financial_agent_utils/curation_report_tools/__init__.py create mode 100644 backend/financial_agent_utils/curation_report_tools/web_search.py create mode 100644 backend/financial_agent_utils/curation_report_utils.py create mode 100644 backend/prompts/curation_reports/__init__.py create mode 100644 backend/prompts/curation_reports/ecommerce.py create mode 100644 backend/prompts/curation_reports/general.py create mode 100644 backend/prompts/curation_reports/monthly_economics.py create mode 100644 backend/prompts/curation_reports/weekly_economics.py diff --git a/backend/Agent_Graph_Images/Curation_Report_Agent.png b/backend/Agent_Graph_Images/Curation_Report_Agent.png new file mode 100644 index 0000000000000000000000000000000000000000..6843d7defda528c33e39cafc7df4433ed3fe671f GIT binary patch literal 31974 zcma%jWk6J2)GpTRh=2n~2oeK=bV;WUAYIZe-Jo=b4j>^NA_CIg-69Rr-Q68B^xTc_ z`+nd3ZhelhIZSN1+ zI1-0~O3|_6T~aML#d$T7tS8a*v<7rqet|>@xt;y}D%7=4ML#qhg!$1h<28E9pP!hi zd#a@Sg_NMj7W_2IIwDl2XjO9?85u00c<)jy5)gp5s#00GP_~ys!D1Y#9edjw;XWg@ zcE~oTY)V`_Ih94Ekrk%hTiMl@<9bn6#*(R_#IzxRD5UkK_Juq{bOhD5dXem#d`xx_J44F72^`P)R+319ig z&(f^D$od9`6)GG?$!o**U|jWw<9LhYmUZ*4)SJ{2R|gzOY-gJ zq~}a!9br*-9-K|@a3|wDEOi>90AH2)qTs^A>vrtEZas2a48uagQ;Q;6a_TKV6wdEq zEGL(bc;RA50A~yf)3E;btJJ~Fw>F=sBk$pwu&rpX8#WjX?>&QM8B(?#_AIB%9_B1;?-lpFi%28V|Hu}ID@&+OjJUMufdDa{Eo zS|kV1KNT1MeEE&8bT1lOtTtViuzgzl_44GeH_9gKaU~@~Z{0}1DQ)6%D=y)=kuW2(=qpYD}uCLGL zdFd=CC+B|G%ggJ<%}*(%SjZ=l6pxFO&qN%XrIlQ66S0qs%eT_ZClaQ%6;-&cTpK@Y zxdf>BPEq9Q&oy|wfBzl^gSiU|F)@Yo80ThXG3Yk%GcYhbd)6{Lt0N`#&;~zIz-@JH zEi*Y8n~aPMAK%!}(9p{2v)uB@&J;zs=~CP0m6a80YHHnv`iblu)~>)-Mph%=I}ct# zk`MI*0*UIWGQNd}HyM|H{P^(+Mq80y^S+OYiV6g#sHmu^spt6LHYsVZap{(uTdAUQW-OkhhUsG9y$WHdrwUHw3>ba?_gCQlZNX1E zJ3CtpsHv!W;$E~54GoQrsZxYPe90b3j*pLbEFE~TM6n=VA&FnX&9mM;5p@{><9q%* zQzoACf$vXmZ|{}v*q~MiC+*#tT7t)qm6Vms4Lc(^EJm|(bEVSXoaN5(kV29pZnJ9Q zofAM@)j5=^jt?VZHkG(}cz$n=Z0#>J6LVP)4-Lh0zh8`9+McbS>hEWvp!m@AHVh}A zFN#2l-C=%G_^y>WW(=M7ls9cb4CmYFw(Oi7IAp-IjMbpc)WV|C{R{`zqF)S!S65db z4xm4L$jHce;0y*69UJR4>F2JqLtta$XwRFGaj!8EG8q;+{`7q6xlxQ`Po39MM>yqJ zp)T_9u&d|hXJrLRXd#$Z~)t@Zf+S+O} zTNmDGcv^8`(%{Y)*=gDKHJ2`3;*pe=*e3fDJ03w9jAL>ltHHwOPN$!rpR%$tc+%j{ zpD`g2LOw?ZIHb8~fB&7b@`%IMXab)TlaP?t=5TgMNQlK)fp)2}OkbjaYN1X-Y^)RN z`ieL6Y-K9Ga@9$$dvd}(^JC`leXyaGlqLK2I*${KUpm5;r9SlxS@f}O{e;y*CB1c2 zFpa2X-nO{!tKMp|lF`!A+I(7&ovkh}zrXU%#AMB=JBGs|&f!yVF#r3-me}TMEo%vh z&e&$K$VPHix8lFDl(Ow8yCmu5UtZ&3-nSBG3>Tup)ymbL+bA0u8IhEf)WtF>o%s5d z(NjQ0K|w)Xo$}nD&~Ia7V+6Tbo$w^#A}ikU0~m$4IHR03zKUx1_Ic$%H%93j5jhmC zEiof`Efn2{@wWyqa->5Rx4b8pvPF4tNK@z~zu-+A`~J3`j{q@YhG$4{Sl^l_OZ>YO zdqP$qf-JhD^PT5@lr^+k!&Y3(s5LWF{^jBUgwM(<(5d|p3@u7NY>P17+3OKBh~ zGSVYIuAllHD;#}Zz#nt?fPhxpy*fRSh#P|HR>@fN=L^ zDLqWGgHy*FziqNM$)DsshwUZC7-a@TkVbK1uEaa}Qho&a2lH@2rZv}%YA-R`3_Yss zZdcAG1|A2mY646pEH}7?!py9=Do>3ex+C$~<9Ll+HTxy|^@7)q+URIFgWT%0c-3S% z`)fP{>uROCIp@b4igtE(m)kSOErvtmPiu0Bgtg=KCMC*yaVcIUki&-AEXRu&H7f|8 zH0h}3crX1&I)pPC8~1qiG3_&g4;`-d5SK`ymA`~%iOOJ}{CM;58A2m4D5#U^*6{5I z$@nBBRbIDm&Qd*p?!S%mk}(WwUR+j&xHB6CPRFFBJ>%~7dGmL!m6v)b@aQi46L5vE zSAPZ0g9wl%3PM`NFzr1YIOEybSy8$yZ5R~o6S!V$OUqqX^*86H9*aZD z)zsAbP4|=KA?}33hv&a7vXvwq@#ZPPnU})CbW#!)>P#MOKj+hm2%x31-^!a@SIJ1MjtlexMi~U5uyHR{LF6l`9*3p zoMlS1jE^JL9(;f7$EFgc%9tIa;kb4}D84!**C{PNnS0n0bY|^yjzP7)?Nc+;7$Iiz z-Lt{H1f6^*H!hl@#(}fXt`6362U>)j?=~D4dJn%jHFZYb@Z)`!cV6|Kd-`17u?H0? zWnaF@a+^Jna8oE~Ql+rieOcUZs^)2o$5TpeRAAJK!zf_Hd*@vB`>Qg0_13eIv|nGp z?a}xp58fWPyGN8$^CnQd#{4hO)?4I%zuG_R=w^|j{l5-`*+fJ~$Hhgza>SES(9rmL ze7?dZYj`PwnnRtI$I+$Gh>H(-bcmCYa37E4t{D;xK8~cmuT|&lw@vrhJB2zeE32p~ zt(7cK+C88|gu(j6;!9(*=)fsMPEJSlG<-pRQRg|P{h94_a`T0)ZOLTZS3UNx1utz5 zZ0ND!aq3SrE3KXPSrpXc%*^yS^nSAEVkJP~dO>SMPrkAc23H+rwvLR{yZ(;ju)F2o z+?46Kx*cH9o_cg>Ywz&;{xS0DhEPnWAwC`+7V$1Vo6+djvTCKZc4<+OMy2tV&&S%u zCFZqd<0EJHN64n8SBmTQuEr{_`<;al{Uvh*48o@KFZb>p#o|0xOk!TE;?Z~VSgDuw zq5#qMW?_1x(~^M!*&Wq~%*?bR#K?YO04laPmZYsB;yA#_g{m-I)PYT!!Y-!3J! zwGF=eBddyq*a(xn>~eB)HnkLVugGa+^>8BIZB6EINY&rJIN2V!7!O!~E=MYp#7O{q z_IS1D3s2LJAEvJB{20vzT0BA)CuuvHk18xo{Ghy!{56faVyA(h%7&e`I2qOs%7in1 z!67>+yR`${fo6|8($bTSUP-pc<37ia%pMY4hV;?%x`u}KefqssF|=XNn{Yk z$U~rcd0BP9|HQW1{F?H%i^iqs!`MQ|BVw|rgFT}`_ubGFeY8}ZIxL!-BpXJgE?Razzhb5iz?@T#alLk(xYG|<8wO#hLW!s1@ z6pkWa$tV4Mw)`t@DI;TNtydiZEh#A>Wwq^3XhFy(@R9}3=4U6Dd+u;vs`kwE(7&55 zs(P4dg6I{7JRu@tzWn_NqnTGjeNOySqZ7)pBTC)HrP^Qr#E(&9VcG^w@I9%Vn?Xz0 zM)J|V5TC;)znOlP`>1B>b(ByNC*_N)wjO5`wog}AR}4M?lLiJuy$MzassTZ;{f(!M z8am#nD(i|9Nb;_uK%3mLkh^nZP^so|vH&}gRnMk4lYbC&(RSpgDx>G+soq$DwEEh zQR-{=2g1}NZ#<^>qN=*KYPf_lVO2lu zlP2$XJ3nTHuQz=ZDORha;I|ZYCcMU(<>>3~SvWX_!1!uOlX`2ulcRn#X(M;%n$|vf z-P;n;!tjw>6PKSg*-IHPJvphDO%l`qP;UL;qxH68Hzy}5I(n`_20uV#b(I7PKf4DP zYj22r-g9bis64`ZUL}b^m96vvinF=~L;O?~SvMW4nFwrX&{J2Zg@0<*@4mmf_VBrX zuwJP$b#PKnp-q+3-Y9Y-`#0XizP{km#?uE~Zn1}}rChc=T5-}UupAnC`VslDOYU#E zeq3UwNtP8|surf?Sm0&kWYM53otssoEgalyLp6(?&>w&ufuZ^+S0E63V0wjMQ$Nu&=32Oo!1 zwu@R$`yzkuY`gM%oS!NWK0Nz^hlke^xjEW%tGQ^XSd92EW^oaYi79ibCW zlcs(Mdv=MM+sME>VqA%iikYr^Vq-9a2Q##qSs7Tm4@wfRjkeE?Q25>WCzkk{G`}n? zt=QtW7t>3%u*O=dNZ{h?64lEnWHoLcjc*2vRmTCY|Me9zu@R+)(M*W(+ST=IT--f4 zjp_6#oLZFRhB zU*UM;v#=~1N=}V-cX9hv%0~GzK08H@ukh!s!aPdl5R(3S$zN2GldF9SzeM7bV z?(|OKsT8X}(yrQkG@+)<#zt0taBi#sY$xhShpY1uFS3~=HW%vnp4X7Lc&Dm&2hzi% zn2}!JUKOPfm7gl5vOL&$JF#&ZAbj{)710mfTd?!gF#T>rSnaY`L4pm!gFd0Lp56+d z<+5rfKCcxhHs+5vQjxnes|TkZ7ggp*4x3L)b?2^^+GKf2+!#1dzN9KPW_4WAt1=3b zxUqAaqWkIT=`C1{G6e-|WTvKWZ7vR!=@V&^`Zs?Ln0WrH&tk6Ez4h>O0T{0%CoyLK2|h1^qgD7yzf z_4$}A=ur|oYgt;_>0o;4t0ijOye#UxTbtx-y`w8s5zQ8Zmuuw~!?n+F9(Y!L&&e@w zFX$;QZfDZHTGOKG*vpl7&!4a9da1vDEm}TZ z9a*Grq?W)eYCVgb=}yLPJ@tbjq@|~vDvImR$66N`DM&--raF1v{Sqk&GS6z&qt9)1 zm{y9s>Xg;NnV6j1o5ogLlltu-IE5v;Y2?Eh)S?sfb9?9B4MP5JK@RvtSR|AP(`F_F&Lqj$}}W3!GD)d!w-1xkk6qLP3f=Kh{XEy;H*K7%%5L zCQh`}c#eI;EuI9dL7YK12Wg!XrAQ7Z+g{IhiDxCvxkC&)Biuw? z8m`jALfQE=NGK;jrsbVtX;0LJMQum{5t-@aK|TINlAXU=->-MQfcL9HPr zB7%^oc~*HU_M$54`sMh1$em7S@|k&$<)0_Z+E z_~k!bZVDMtvmBRX8TEYSc&CG({&ztp_6#TdpIam*CK3@5{h5`Wn`J=^XS_h}F#}@h zpNG^pFp!dz{CAE3J;A}jadW#SM?j5!$4tft({fWby_HHZzbPJ__un$bO5GGc<5=L{ zWw2q}-fj)ujHlHu{6Q>q7rAAUo0cZ#2(!ZaoY7)z;l)$8t20_^!l%<1 zqVVR;8B@7WD5(&>!}|AM`md(0wI$MVs1>`LmpWpZM`rxU5ey7c8 zzRS=tNLRZ#xg{_++2ED8v|TH|xxHqja8rMt?1eHiFi`(9pVKoWJWt0jQ^WkITUx1T zC>CEDWO}Yj*Tqk@#?hTXm_0%vKZyYf&~x8BvnMrd)h=l)!N;+HSnZWq`qD|mBOA&fhkFe~yGMTzXY?%A^* z>ODV)N((bh=I|3l4FXytn4R85YG!6eGt$%RyuMnHihbSMsx1te zskZyllx6T*wP1R(`p6kP?8S-Sn~gC!PYH47A|okjSJ#+^hLEJB_Kxt6P&kExk&@D{ zbg2i?(IeENV`g-MoECjSV()8^_0%g1XIPV^){Wxob4-3SPE3IEuG2Fc_$J%QWh)pg^Rgcy3LUO6ln-NJyL= zEX9VBHRk1ZhXzggg7>?bkr`?tuZv*}AtfG7gCTk=#lGhVdfnnZEmv65UQ1cxXCIHiyx|vuu&O zxT*!0k&%&wneAl!<1Uw{Uu}%t-50N~iYF%Enwq(Do(~5G7OuLg{rGzisR8fW7gw557G)axPIy(S zX!9Kp&ly!UpbfY*I!sJr~9Jf z9cf!HSBe>@D5UP-Vr#gr_ERm&$*`u63GJVcUO_O)b~)cK z+VD9(B^I2Pk;tBmhVx!AQ{Q8NpRg@p7&+ku7VH4pdmy-A0Z@83_?2YzQ{{N-#1D@5NL z@9^>y%2%Y06cr_@C@5~>*vR|(_ZgR}Lz1VbFYDdp3e{MpbTTU{R=c8W0Xv!$9^PzR`r#ue@PqZ5 z|C%oZaKYyEUFF%)^F=QZZ$|oTKxZaYOWTb}i(7rxL!X7v4VefQr#@Ts0gO`2wM%H4 zUe$W05p@h+byw`7;#4Jb6ES_U+S4ab6xG!7k`I0lWyP0}z%z%gF8O1aKD%CD%8V6| zd}Z+!3)R8L#}6eHP*1esy`s;L^YQsE6^rF|^3AX`uGznqmY$vxfrCRD*t(c3EWDB` z@$8Me%|^cFBa)|<`_AlI8XCsd*4N|3!pp)>Nv!(51;|Ntw+KDEToQvUZ1Hl%u4`a% zaD;sP*i&xd^jwSPEn5wKzOgy=|=xg{2B__5zJC~5Ylw-j6_q9TQIE8rBL_q&s zjN4RxPAV41_?H^1lXX#b^@9edgPGc6EmOAev% z^K)KIuZMnm@mwgPCr@y(-?Q+ZPwzGM6_sv1dyId0_7%KrGx#YlH|(|-yHv2)moi#XIf z*)G-%5i$-&hLDIop#HS?E>b*gj$8MGGLnjfL`PWA!i9fd2jq8(#j(E8_~hgt)6<4S z8M>4dSbyF9Ma*gW&VfX4YeHUK)RGaNGo^l z_qx}N1ajGPCl|?UYD!&~Y9^s{{dLc_-^oaezj}oY|I|KYIyrksMur0+|LWBe7O5R~ z&O`i5ZpZEZYP+#tai!-!eRs^wzkT?iFCbv$5>Ma0lC!u~udp9lTC~69(sOnudiagq zCn!kR`Lvfxib7dQt)M`Fj*gC!(g4K&MePLUb~0A|`2y|s#;cTs!rs`oBObIujs5-d zl?wN=vPUl8j!gvf@i=UaFO5MFD8T; znwh~<2bgr~CbHrdYhPhv+`V;6Cp~@T$rBy{SNj<8XU}D|!b24m6ppftKrF9yN^*BK zgeGwWr3WJ;w!YmG?|+Ai|uBU%9G#ATpkpy%>UGVQ~`rkqEdRbZ@9CD=!Ew z8?-rFSXk7%pN)>I$*IPAf!%cYzD57NgG2e{82X&62xOu5b=qJ}ygV3NAo{>c_k%;& zssA-2(STn|tM=Lf`?%$5z9wq6?(FULWx#2hcWUa++0hu-%+CCu*VpZ6X`iU5IJhVA z+M7PdQNBLx_Dk|4GBM5Kv(K`y+}4ebR+g2maB=Z{@ZgaPe@f;v5G_&r%?wUX&tJSq zU212xBg8OrzwrcFg*6V90gB6!dsTn zxmASyK#wiDJ%ogaz=@HAqxr|n{f*rZ)pj32)&$^W@tqR5k4Q>RN(yg5!{PCEWK@*x z#+#q6PvN-i(VWBjZajM38Mo?S3 z!C%bAJzleSYD)B34!=Fk?&TTrrs<7m-U-;Rhm|3v7 zwdL&0?P2iZ%^sc-1n=i>*xcu&IZGp>zdkmFbsCP0=o4WYunIj6g=uTVd+U-d zE-17MM#Kngr4@tRi14*4I7;`pP${!eMGoNEioPa2FXLj)Ea(-m%SiT52mFKL8$ox@ z{dO`h(~UFEE@ToVo_sDQb^zUhhnVi*z(CH?Fy0^@UUXFS$ooN>&wd`ir7^2@y=I)x zY=35E$`TQ8?aYxe%~1}+kF28hgxdFB>rF!OgC>@(xzvY8st=o8WH^m~%|Dlm5S+nW z-wdwq6omv$RHA$B%qVqDE}*Aiya_c{1(>gy)Ftthky$$cXi%%ql6$39D9{o}0+E&w z8QY-PeJqdPn>-V~?)Uukv&+P5K8PnNoQ^%@tv|KDqy~ zjHb?$H=%)XQ?A4!IZ!1&Y;$w7{-Q$rYD-^K)Fqb5Q9^>b?JwEGC!7AzbL7C4b}i*x zoy6eq@TTz=iJf0`#%B0^8gRWM z-6Jj2Bby-jMx8cry3S^BSpky#gB6!r_RH&{?Y+57>tMCB#T7*+)qK@HLD2Rh-|{(T z%o(9Jyy%qy1oszq)~LR*i3$$`qjb}-HF-q;M}H@ROWvWGo^Ge{j)=vS$V{xhID!%b zP4?MrwQ^U9cl}Y`-rp_8t}9V}BqR)c0DX(mMKhsa032-aYyE(TNF$VV24!#t56}8( zA>X5j%W0)9UwYDplc@vQpHUi@X3F;o*z63{msTYF6SG*3} zmvkGg8{?_HSARhZF&EPtoc1cR-aKw_H*gWH_VF1YYb?|}x%ePwqPkX6sErcS(2!g& zt6o_#LJ8K)(Jc*_;{t`<(xNQp2@&U@7sITJ8aC<4@2Mc;CFp^eqva zx3EF9*6#VjLL)LmI`IjM?NfNi%*<9(b4y_XbTK{S1z{s2x=E=;!Q%GNl&FqSQ;CU@ zk(!2@%cB!f2$*Bb!TafG8Yb|ru5Qrq#lQ&O7dk*iMLhXsSvy?bZ(v36^mP_hyd ztng3W*+}dDfPhL{+tT_vAwg6PP+}laLD@qez4j;&e$Kc8+zT&swKqdZtU#|0qp75} z)}{67Y@_2TV;On4}hrmit@bBG&Bjo$5I^6+S` zcdKjw(#gW!%iGDR26LT}#=~?v(d!}gplSIOOH8>OTaNT<^G&Jx@7X)vZzrD1V2lh8 z2MU%UL9YsQYBu=t90WtN9jFnnQ$G{o)mb<#AL0d_x+lCa2}QBEM}>!XULdm}$z77+ zH;uZ?wv`+_e0<1VS83{_T*@Q?r;_jFLgw9`J}(JiX{o7FVNkFmfqFrqb>C$&Jv|+? z+H|is$0~_GH!pDNWv}3kR&Epeon5iGhGq7(ecJEQ9Q_in-sKjWFR?XU1BYN99XJ*3 zDa(iHYG{<0E*x$o+hN1~{Mv5&?6yjo(9@YGKBn|qrMm^1syG45+ts*-u)|8P?5Zkx zVPV|R`|zx+D)|O~ki=k!{ls|pF2J{YMjxlZ6x-A_97Q17RnUtSsm;x=Kr<%V$`MM_ zb&e8_l+iSI@%_!{W@JPK7gtv1;O4#yi#k3x5~j|2r(r_;0eWuG3g_T(5Nr2@z%cYC zCe%T9(oZjWXq8Fl&~80=K3VuO4X1O3{{8N*j;ByC^_QsH__69L#VIGtu3ue6Wkr&* zvXWK5k=OOlH~BM+EiAlvPsV3Grt!oi zh(Ms1V)M_R%1TO z-7}Uc;d8J?>@`eAyMC2+ikf-{|CjFh;#s76>GLZ*FSD-D7b|<_Ki_ycOz>$;@OvHD z3yU}pZpGTIF_aWt5Jv`&25N2MIby+e?cPi}xCP4Os}| zDIF`-KBqN3Is^HMYQmTOXR5`Sw6WeJxtwV>DZNC4@!(M#wS!feh*Se1Ow67#B(2R{ z11bM_>~;!~`RcaB-Yk_D&va+!*_dujxPrpeh&k?Hs=p%Ws&*=}f(7U5`qxt1^{2fv zWk^u7ib~Mdcm(x0<3kPcT$xWnrSUq04*8B!B;Lli{%Fen zJK@o%56?uX701lVpLdkK>S6$`xWP)SNX1~p{&D}*bBY_FM9r5jogfyiI2Slsm-clu1m-}x%11(? z9{KI8ATLEp+L7n`K+DKr-J_IT|G`P8?9I(QLT!e7wSpoaLqDs(l&bsUaP)X^*m&8f z?B_S}v^PYbGgx$!+QL2=og05ipf?^=2#=8q$N2z#v!Gva{Uuijv;$eB^@x7>5x+2Q z`pRJk3UDBdy8II8;*s`&N+cvbMZ8fM^LdO44Qo33xsiFa#zG$pTF*@n3{d=xSA^vK zG&e##nVz&$yPQd)8^xW*SJ15bBhQnR^yNeVE#a`fo*sr3CH_e>KP90w`%c!wThzJBo)KiH%dWKQ6OOi zU-~0q{O{F=e+vz{pW^)^u}m*3`}U8B(udLEkDAdkTk)ptX7Au)7>i~O^o>#~F=79F z1{-qIm;(=NXd(<6dSI+R&$s3Oh#Md9DK(SVqIpB%Gqo2m1Pex_WO_rx3nR8CetLi? z#%Dx_)s|IYO+qD$D)$gh1*Ysoq<9`43GbKOI|?3xnbq4?BXe*lxRi^N9<3})2oERC z{-Y=i6!fUkR3q%Je2IZ^7Zl8q+K*p`LD3&zJWWYXK0#rMJKec+YXa|&`0(bV8eiuz zJF*l$cK$6s6n{GpRaREno1MYHOum&ISW#Yy+%;m@Ne@mI>2_hJu8rZHI7f~*)1>WP zDoZ+2IlQ%F;k`4&mCEBR4h#cbdOZ~p&;YfQHL{M;Xp<>j3B@0Xc7Bdqe#sF(H}&;K z!pp0hGhjMX<3pn=I3*^<_WgG$F|DX4d}%ltZz)RsVBcvGs1z76LWneaY}rUmwm5d= zmv+1_LR%aN=Kp}Pt7DFmrpp3@LX@31HSnT$-nbp=)= zwcnw$yL(xsjc(r;j)UJ@v6GXxfp|=~&fhDxJoha*%t5X8I<%}Ts^846ySw|^!=5a( z{aVP0R0vyOn(IeP+SLV;RxFf692Hjau4&Jn_X;St#4qD9wjD6UKS*1RxX>$NuKTl#BK6CoYP7%d5cKMZ?JYbN^moGP;Q>*3O`vCcx- zZ@~kDiy-l3H=rjZ%sB2&+HNInH8fvh!wHxZWU3r?+9igfqodQ`Z1f-+XJ+c4@JFPx z+WTA5PF&1$dX!RuL9&Uw{3bdbAyxwn_zz)Gm(*rysPS7@0)>}*mwulYC=^;DjjVGP zB=i{K;v^*{Yz&Nfb@wkQ5c_+sIy$?_U+VRORJf7E-l5G)W;VfUJOcKKM5fJ(rP0Pj_^$AStB473E*S$6XJ0!?fPtpN$j?mI z6H`cZqCKa*>zxJ{O&5Aoil2c z(E`-BprD0A1f0WY@Pkwl{`rtc&FxK<^oO=~!&T=0D>grKCIB2c0Nx$Hy2PcV;($)4 zcNbSI`XjOj3I_|+nJd`4v~&uHC8eaKaILRCLv4xYU9K<9DsLqMrv)Cwp3kAIv#U#5 z#sdk!6Hvzrc^Jsbo|r_Y3~dMsU#jG*e@$X;skM5Eh4aQ=0#_J-FtAGMoj10Z$xP0% zjtKr!QFnfOV~hCztfM=`9%rbyMETx3gCzH5$Uo;zV8ZuWnRGQVsRTtlsMNKpoZCQk z>bAd6mKiK7B~|_Y1?iqX-eU+h94L;ETT`eZE_1Vk#&1NSg`c50)(;61xp08{>VJpR zKwf$()z+4g^x6Gny7R+_%Phu=d$+X_VId*AnGepQa_H{EqICQD`wrRrxys6lcwN|5 zS8~@%jW6|$jR%TTdp66vm$e!GNcr*}d#E490uKum&Okl=V#BVevJwj{U=+C8*WNNQidrkYt5`H zP+74H?&t$Oe6onVJj1OE@`bXov*SbTnRR&SnUxvvRaM7kB()knqq~)d`uiiex%>`RP#wg=itw-(a~%B z?P9>9sB1daySxx&J%|ClA38cE)v;5{5<)+j>-VdH`$hd5O?U2NB@dXYUOUZ>wa?AW z&Rk{Pv%clqjm*sd{H-y7&ACP9fkS z&CCwe7#tU8G%Ic9zVR3|xC=u%Ucoo}2f9{)ri_?t>C8Q$+75qW++GyoD;uAhO3CeU zz7EPb7kgq z_-GO|`>`2B1EufjC5=Zk`q!w%maa$)y2>|t>}aVDrnK@-8Qkn6mXci4_xVCF2y95U zOkWQVBwMvtO8@9Svo1Whl3T+D>40xNI+k7-Dz%*^M&^i&jtcs#=pE}6zo7_KcgLvvm_m@?TPdJ9bk@V59& zs?q=8H(FX+%G$f~HI@*$_+^Q^_>y)y8uzPwblF!{4|KZkqG|_ zE^pj8C;zx{^lX@KEI8@fd>-RdQa4!H1<7;=oc`A{Q(VN7JOH$7u$xm?yvlUGI{5zq zba8P7@))OmCEx$>f5n)c9mf_IwM}>9H2DK!Ma9H|{uwTisXhNQ+&@^weBqr^$KNiX zn888Ye-P~dFXok&J_h6Y1M6ja5PvJyT3bXOr7lxWPBH*Uo0ehRWeCK3H_EDYd!n)% zUrsU*Mx*}${{K2R5E=l5{xd(_{*NYZegz(0w3j!UZL;OsB6s(UC6v?q_iByT1SL`y zTL3U4ut+954cRc7Suj$80Tvj{%;dhXeA});dxju-9dZm>=Sf%_ljR@F%kR11`49oi z4I$^Ro(^%j>&yZsA(nTL>_10yAdr2~!-vAz;_^V3Dukq`qX0rK4)Mhx=0uwt$<53Z zaNL$J>AuB54ttD4$c4j8`IT!nX{shT0O*U*BDgYEkBP0Xr-+j&^X*LRcvvjmseY zPTNcV1}=Y}$#h@I&QQ&%B_9n<`0SA5A?fvLwmRT7UhrdD+G8DXL>Un;Pt1rwg~ zd#D)Qdxn_olA1T>O-p`JbeT8m=woOh1wre1_&*z||K0Te;jI2Y9AF=qs_Yc@(89IW zZbmUMGRnAKMQo>-8yXoI8ymUv8jftH!xY;iRKst7M6hE6{o1=K4A&mfumAdNQlRj4 z;n(NuKavY|1W@+h19Nf$1L<5uX^T{x@vNnqZOF_mEs5;b^W5JmE8}I0dOX7pYIJ|y z_iIBaQYqx8#nywfm9aSR`u(xm7QJ4RO-}U==Q-J!T=oHa99R#e6WH9w-&Kaumg+oj ztZ~@->gyW~c-0Y$7#L!-AfRM#_Ql&Wr~k#vvR|3RxL3>_q#P+rr%1fFD)&#w5+k7D7z1Ph9897ne#l8)B>vf0Eyc9@3bDSwd4pe z{;#ZdKXdkb_?&E9j;JNb`{NInq|b6pODA2KyIUt1DJidJYPn96VwsYHYTpr|P~g>= znb9&`P45e(rXttTkAX*pl-J6dOVhtos5eZ`a3g|4t3U&!J3zWHYnS{*R)y*VrclxMYXH#I8sFc0MFrsTOgUeP6hSRdDZwWCF5CD-y6^7 zV(j4X8!+hm=6f@ZM?=%JRJXHL(*D%~P_+UHx7_vn3?+*2Pp$ln*D1c>3QHe_ImNcI zo(kN8u{+6r@pZX>7Cf4->v=r>YOkpw2%l+eWCUe(P=~s{An?it1l489~ zV6<{M1vcED#yU%Z8Tpm4dbe5@PQSVHHP>24<4`i8OTaiAuljFqGx)qjo`24Lsw|7# z6#%se6b_W=iR`RO*KGyb+smlRf7~gTH+C1;e|5|t+J|Ndi*11;iK9RxAsQ@fa)i6b zWqy8bRju2?o&V(0KQ_IDRM)!P3n9jLS~w2wcL`V60zR%v%h}w#ykIEN>WkZ6LFvAD z@Q(3uC7s}p9)(Nosq`XvkYrFJb*nD6eNK4%l>HuTdD`aXJog-c<>N)3e1eC;8$w8@ z-!CCDU)eoCS-FHFJ=vOSyr)w4({$^B!cxwJgI$%wmd5;-2L#MXX`HingDV>cIMbf}89c0zn)LR@rsObi?7Y>|fgxn0f)Bjl3=3r>>={dmTa z=W`{Pv6~OcS&sc zA9GAzrqgh7DyE+vK;X-Eb&+4*TFv;%W^%ogm?By?g4LxupkAdQ=f7U`Vv%e*Z5;BP z>E!vXPghUr9>juBwq`q$$LC-+H|zd?vA7u9C+O6?D3x3yfB!#Fyp)*O_d=6*+al~iJxokYM7zZM8%)M`Jeqw- zX8|z*3l~5BjR|at2}#Uar(0xm71tUs#DVPV_r{Lxn(T)H>mJ4Lz}YtCU}vgCV9~W4 zcXsA-Kh?=1%R)AuGt+0!s&D2f_@P~Mw;grGV zMiTfQ(5v{3oV{6I^#JU8gPZ6)JvnSH8dLziB&-e%BGdQp82TXGRT=Q50VO) znAQ3Jv51saAl|Yg$}5cC5EA-DMK7pV4QKoekH%90@Sj*&MJc~Ur4H9Epu7UHk16lM zs%*VGI{Nrwu7gY`Q%W^B`o$p`&rliiLKKj}_%pTNw&q_sV)x{iQ{9EJyZ=?=#L7qi zduz=1<0=KV6!W$OFur4Z9}D>K@Vtb=AL^Hi z>My_YUKyQx_W%k_F3Re4yS!BbFt=|HwQDf(=`~=b-zX?ZI6n9e_}H8OOJ;bm!K40B z_U$y_xV}O3|A5qokrVxyIzGT@0%wele&_QH0QbN7W)Bame=sx^W$LcU_-%0czt0E@ zlW;Q!-4NdqiHUdC{u9aacE~M|{U42ebwHHe`t5);5=u!sD4=wsfS@2Dpfo7m z-CYAnh=>SC2}n0cGt%AN&CuQ5@!R-)=bm%!J@=mLU(WEx?EUU{?`N&`JZsC!ndE0@ z0}a6guFvl%!G!CCCpEG#0+%E^&K~|p6(3Eeet?l!s@JpOO#U6iE(*O67W6I9S z`450IUF9P3&))eD);s?pK$Hy(2*4bt^-{fUdQ6PuKhGaZPEQy6>m!_yWYjIt;3XCS z4ZF?)&XVM}!hq-#!iF$G;4ARQ+~>A#AJo>|cHwQ+{vS61U)3^o=R=Y|z$rD=_b-g| z56JTmD+2&|pnoGYdk2TVO%Ecyy^;Sx*cTOjzdiE-MMAQ-C8DC=nCrHbUp|YlkqvOK zdU|%Vbx42X4io|cg1e2o2X6t_{z)e$CI%S2=#Qd;-!!|mb+wkj5#2R41+=GtsxL2b zBrs^dl5tCFX=z<&R8*Xo*(^xpV%%2mdjokqI5_xH1{*tYy4o#F^5Sh)TR3k#3J+WG8PA!-kY`UTwF1~nsB3=|`yB(z8ON;5$=t>?8n zNFNqN&>_7|Y)H#Fh?w}0GeA1y`h234E>-ez5aRrmXk1cudXt=y1QhCpZ2X`@GWSVG zt}SJjo_5Q1GY3Jd?Xa1{oGgSba%ZX4YyK3p{!2W%=Doz250u^ zd7mpJdtt5kKv0qUZ0oIzglJZH(p~;Qq+#~)lA`2wx?0Mb`TxJc^e;|Nneic@$|`k~ zb;+~loyQVi-SzQSY%1|^SDr@gI$xb2GjkgBk&x$JiHQ@-3s2~XgIBfek5S(fo)M!2 zGU3&&$;BkWJj~KOn14AhYc{?9z@k?mNiQf`Qc|yQXR&uJkWhT(2-VtK%4pGMdq%lPBSvmCkGC=9`73JKx96BdBn{4T`Lo7#q|0B8$0^5H7aH zKc{kAE_<2i_LvSgp{FvOSNt2L^U->FQqtZp(pvj%^FX}bu8y$^hejX&06>^ZVdr2H zqA z-n2Kub^+-M@{j=6PA5aUFvorha^=Uzqd|^9ig=I{`RoTHB#``43lbS(dEbCLAJH16GV|;6WfgzecVJB|V0d`St_Ksm7-G>7EOqVX@a>!6mODnc&BL0v z_-Qn&U3+v3<0vlP#+`?eso+4lnpzExe`yiizpe@Uk(hn%xrK0gNw7kWzDw^D65?B3 z@5M2Ng+;tUocSfu>+=_k35Ih8xsA*bIbO>&xp&0x-6JJ6$I!pL zenx(e9U1E+xp#%t_W5?a`hNGtbekKp(2U2N&ZA5s(R@w!r4Cex@o9 zjJ!tBzRps7m%#ptA?z!)&|3x~t0_VdKaLb6-GNNiddfU5IypWLZBGTcF^o(M>LqMs zkmWAT^TweJhESnbH>DQvyE5MSc)e4EY8#2D8D zenSqQ`ezR7W2cynqq*a6a+3=Rv=2tO30hYuV}Ra6NFzy>Lhy33Av4;XrxM%;3w26b^5^lw zGb`2v$PLn&OL;Bz7{ASa=8>5bY&cKE`le6I0|7VI;YwgljL~Aw=i}yHx(K+3b`BKY zV1vRjen-p8S+^4N=F6d>aP|~>koq!R0rUCN3I@8zm4%9S-An#TF;>mg%^L&piO!+5 z!DTtC)#_R+bhNwn_SWxQt~W&|?1%%8wkwy{=c(9fY#Uo1m@+?o+DrGA%D`%8w=sOY z{CQFC ze-f$Jb%U+k$W4WpA+P1E+L7Iivz=f*vcy5wzUdqxEOc59*N_nrl;RFKvJbMG7p*r z!=)G#hczPA$rKXDpUS2|2C z)T5gz3$@-j4tg49bauF|<^kQQF(W39%vFQ;cNLhO7}AB6+rMEtx}qUm7iYd+>xoMR zwN4N(nfw|bkxxioDLw~<3!aas%fO@AQxf9~Zp9|$3hy>F<1s{JR{H827$HNBsP1^> z=1Qc=6OoYEJo^b?rxa$LFW#vzKEGlX8ON(txOw(?)95C3Obv!pWbVyrI4~m!B zD2tJS(fO(4Ko_uKi;RnSbJe|MEA#*lQWkChHcRpN2s@kYswXZMx#s2I>3dbxfY-;t z-*?OmXRYnEZju`N!9SGgp)LX5((gJbKcv%RV_WNeE;>7=&p#TAwdAby-oVcGoBQ?9 zXC_>4szb1*PqIO!Z=P1|M1~AL5#D;viVy3|WT#Q@v_+0yBrYbUrM~`M2(Oiks>aHwf}dL*knL{(HuTX}1!RZT@pccws0lrfJ4+1}nlnw8s!F+kO~H;z9x z>jEnysitwr?9%u~e(k$=am3FJ4Bvau=V3=bMf zJBPaq2{0vpC{_j)YvZM!{*t-b9UNJfUh-KekSu8sO=oFZaSokSAQ0_Ifv%?{~N|pH}qE}UxutTRuXvfEU32Yv! zO2{{FhBSo9_<0?i4#o;>91NFpeyWczF|HW(yvKkzZ9hhU$%-9@1fM+TV1_pcDcf}M zC&>!&@`^t$y7@taiVQs}H+HO_7zg)1N_d!5fPmH{DGZI@@|<#SRtKK zB~>B&>Oy)jMA9_ES&EJ_5J6LUvhI-!MrAN zCuQtMt|Ey5FDp7E^ONX-Y?>?L({5Q6L;RxYU^-&PbH*k7@+D_lS=q!2PG+3%LhDVR zwA>RyNMDsp4)A9{$Gc9a69h>DOPwS{$KSGPYaiasH*+$_l&13;LSDj*%`HU1TS@

    StS-Dedlpo~wOwkdVvMq?{anuoj)R zUSoBlLsV2w=I7^Ydz#GRlYbAR&%l?v)etpy#UR3Wb)IX`f+WK9CDe5dRpEVR=c4KxE(u#i5arH zQ%uh@@_R^dPs`}t_ITynCqq`#zak=+{QX5AKUR;~Z~QTPog^mK5)hyg&amX|!kDe3 z?}O%8Q+?+f#fy-rsEzGuL0;aKw$K@sJoTNWakG>7T)XABl=u05w(D?8_Osjf-Smh25<99 z+XdX8XcyJe;>GVn{Q_>Z`br659`27*e%_SKui{YFz1P_Hh(h}17{q~Qv;PO zs&IQsPS^WzRyAhikJi2Mx%acm0oioW3i0mb5tu~ zbKUsuU}L7dd*-dotf6-0^sudE!f~L9G?u5exrG_{+6=VjCq$}jZ4KuD6QL+A$Gn-{ z;%bc)!*10Y=RI{dm$$JcpM@L_8=5)6n;AyeW89z24f#s?qJL^?ezyJ!gihS-?6(VI zbk=jo>iT-#`x?uImLG$jR6?$sUTJ9#rFkN&|FB))6As76V*C+ zaJ)~-ZR>q(S2i;8+aN8?Y&3WN0QKX?JLo8QS=*8Mg)zbAunqHXOlot}Df=(lB%ek| znzp{nRnDk&eeT{=2L|)d$*NH>pWXwwwh}0=&Sv93*DsQnvP`&lLT;s2?3rK5^O_|p zn2H=!DvehtkyBFk0682K2vbn}-1r3v3X0Xmqdwb0tep${ASwk9%<90~g@%)NdAiy5 zd2c8hUr`f5>sznwzJR#siarHT7%ZWma43y#O3E78yDRQd2sDlI?jqKt(bh(7Jb1#O6n~JKi z+5ioO>sF?9Jpy4D*NlgZZ#6@5yclro{H8Y&E^eOjJ(EwFPbMRHP=wQMPX=G!u+4e# zQw24F4!WwacXARO5Ma`mh=lM<9A$&*gG3k(9-h2nB-DO%o!ch&W`!95BT!OOQhSw; z({;WfwA|cEobyyPNXJFs&y#<8BgEizcMh0s{(Xoj6R`sCU18t4EBqp3v>P$sKV5oU z)W%`lmsnE5Rgv@3N?W*1US0ibE}vXMlxSo_gD_RFIAVb$6mr|gry8P>iHQ%V@Ss?9 zzD!tt$Z8JB+N^<0&Vs&NfBIjO_=C5PkByM;lOOm+;gONLIqwKJ4MJ&IS!EvL&z|Ai z{&`%rIc>oFgyIs0AqSdP4!hvF7W92G)=AUpgN{eWZR)GX^8L>3{cgYdLc|FCdk{*A zytl_X@9E0P-TV6!DX-XQDux%-RlVFLs5e?Eb>lu-+l{TW!`{|XuJny>JaL7(ie_CLC9 z_HohYzG5R8T>nOW4k9enz-IZJ;k#%)y(>H8#s1p^HTKR%ZZsGu-T)HqD?Fp<0Tzv` zIT+QSs+nkjZ&P{0DIE?O*Y43#T0YGo8J#Z%NO~)U!-oYHt)C?&C6y@qYBu`f*LQSe zOGUAO>$(~P6V{a#aVI$pHYiFz*CPsC`p1ubw{82(e&EZ$S5x!6IvQn&00q)ifEGD1sxmh}aqobEm}BTwQxSaj6v z5IRFju~=nU#PL2Bp?bVr?XK*-cl!N##VW69ny32)V+FR0Pq=TU(z;vJLCrpUN+tg5 zVligFFY;~9n$k|t{Lt?q^TG!4sb6#aOLs*Qb6USw(Jbx1mA@|^lEU@7VeaQWh}Xxb z8EjGEK3pi2TM;CoMRNGn;aJ=S63@L}OM=w@=>PX5@Ne@(EGs)SFsh66s1_RIe?oo9 z9Mvgg8_f`5dJxI|s{X~rsgsOES*q+4;YQ?=Y|=!koDnMJ;-Om!=U1TO;iyx zt3SB=Nj9?=ME;`H@NENERSDw+;9nD!&^EWaf98#~Fa`UFQSrMJS9W3U8#%uy=hA{2 zp5*)yqlG%+0v%^W`U83CRTxt_?h~RfPM%-4mc|D=i|OAI-Ak?U5cyfVtHLW&`s>#y z@BPz@9e%J@nZ|1nRU2!6W`$f@>-<*#(5q7jAguK=t6A@S;%Sv z$sI4x#M=vm?TKf4-qtFt7K*qXp1W#)-ua$pM|;2USXZEWQlrNG`aH-NoCwGT&xp|J z?!nyHj7g9Vm`Qqh?s&B!eL3Ya=z6sMfN;w0;#<(rAzo$Q@2=OFpGT1^*; zW&N6#f!x2(0^@goDU*OJQoH3jJL_Sp3*1@Fr@nZ3Ojlp=1)tA^a2m;&EcbSGy)QG4 z^h_M(eKC1sC4D+>m!V~l>W_zw>{Wh{?r>nQR%MS1VbbLDcg4i=^FD7?$oS@mhrPc& z$IjN|w364&5P=1u5^@Rib>)2eb7v>C|Mtma319mQnAIwIfz6cb-IO? z(^Vta8$CDVM=w3m%=;3*^?pF_O ztVv))^cU`3q7DWpJVSxp4{(-V>?7yB4}gB`;ni0$9LnZ-CIY=d(m@hPT|rWcvbYr% z&KWTT`3{<^+l8?*A+@J{|Mzj=pkmnlb$z1Qt#brkd19Ux|Gjg`Zbb%ro#cMCtq&+D z^v)zLFMkdx0PKpq)V(;->|P8@zdr>tkE6X8YAXgBezSXLZe1 zT#c-!l>GL?ciAQP)`p#|+t?u1_SS-qm%&ZsDlY{1ir)X?wwdI%ORtrzwBEZ*G?5*$ zB(Y@clJO2vUF~W@r&;E_yj`v4=0pupZ3>?hsQdwNYGW{G=UW`Povpqa&lX9*CQ_n! z@aQ8WetvEwNN8J)ffPwMpofZTFuxf8_?_{Z`TW#H|p}V1a z8TLEJIFP&4f{u%sef&;gd`|1}|71QCuZZqt*3h9qD(v?rDl9Z!y{b>^G|CEYDAYr* z7I2Ffy2k_t!MZ_RAdS(ViC;se6M7siGP3=!@% z5J(=aCSd;FHK(kDC9-*b6RF_n#`!(`QsCdi>7f_!cU+yEH6fLk19 zm-6?;<>lbW8zBpjuO%q5Q7~_SOSQ^S>>uy%ZBahc)t4|e?eGgUs;Zto#CH~`&SnLU zF_4$e?_aOHLv+gU^6*^wMu0dzO%f9#pY>RGNSjJ#XHw^;`1fx_kwJH0$%>K|u221Y z3edyLV-6cBo>vy5!3ryION1C**pDAS^3-ecE`Kud#7gQ-@@OfQP5?Rq!p7x(@RS7W z2$9^%B_@Hioy(Csx=NwT65n1vM1ho7rB-N_aNC`>?QleMu^7hs`O!jt z#HuMXf=vcofN>`=g@FEuOqgL$Oqu754WI<*#ev0QT8^5H{@uS2yjp5`G%7?EF#gff z+3oEYz!5mn16u8RtU*Kx=QGSo@hMKnR;e8ty`qe0cc#>#m0mnUX3|*pQfSTBv}2 z1b(mq0a4rvJqb5qolAVzPIV$uI{(D)bLd*w(d#zk$!o?(N7%_!j$7-LrH*OKPn4Co ztomb`b(`}(Oy`JGyJS@bYt;f$_KM4`<40Q(?9y9N(%>>HWqMlh)s^XzBPt7vCHl>& z0hoWfV@m<9>Vlg^DKRJkr-_drL1E>>0zGbX?gvtK8hP=I{k{0kj*bTZqm7B?%B1SX zB#~OIQ)Dx{IfYA#quX^)22Efx4aQW`Owm)v6=CNQ~E+Xu&Rqm<-kaL~a$Ceg4 zUdzY3&aS|qm5aBu;CIp460pOeVg=kbo3^&!v61m?;GV*9{f)--DIdw>2RaqflJBd$jHu>9P!sJ{LP`KcUW+{_&{sksk zH3fo$)BsIv0$Cj@oJ{uX=QEEh<7RZ+oY2?N8O~OURn4xo%h=Wt`E+BmI_igQ6#leh zUbhr1Bj`6{GDi#i4x5hqi#p6JT!B(wG&H7eE;n8ut~Z-g#%22TtFx$xiyMu9e>O9X zWYp7?uTB(U&sZ3&h3~NX;c-#$nM)vm`FV&~u#-SU=vhZ|bI|&|?w_P-8LhWJisKq% zIDjEv9B&45Ap=n!Tf%0#e6*qRNbL0|f7mQlAi%d}H?unt{c`QW)bC=RrHBTOBpAmI zbIXhU?s0|Qm}x~?y}na{{pzsN?Ny6zmVmqTDhtXwj5Vi+kdW{$nfe)wS6u9C#<%m) z?_jyZCh7ilm^C)6wzWw%GM#;oE;So?Ci48szZ_Wx2vb!99vC=zp=@Djs+p4`_9G-) zmm^v^a-`%}Oy|-7+*EPrWawVD#XnHszXgE*Cs2egWfT0y715TEWdE$m#It+KRFUHx z6wcE$_C4&pKsN31yuADCe3jlWINc04{qGTvXrYk4lDO+mdOv`rjMJYeL_|Izf2N{$ z8Kf#ID_nucR!ha&(|*m>=$q`i-W9JBcjs+1lNix*E_Ui=ONcz3%9h%7LwT;COdAi! zC~X#(V&xZX!Xl=#t>S??cAjM#Z!r{lMre2?iQPa##1o=*fj1U79KyeTQbgCtCbZKS z2*Gi=f7tq_k)6&wSo``(*#)L|x zhy|^(AXTFh2uFgd{eBH(SM0x#w|*fU{x>B09vS(mwdnS6?7-#*Uwm*dahiNUH50%~ zx0qa*7>MJW2KR2i4|whUFW-ksymrD|Wd;xtC=LVva&a4L>%>ZwLt<%+5AlL9$-fS| zh4mq*;&*6p0Oou~hHvs$=nuvI79|Rh+6U_@CeZFST}#V%e_s_O<^9`FE-r37_|w9r zvx60YRd2y5FyYrB7bXPfQwe!|wfKWX>oEH5!;RaMn93b5XU zNIVu!k`)*Cuh)J+O1d=x54=Tu<`vj4ArSCp1BQHPh9mkbSsm{!MytRFP;+*DJzGAj zL*`z<>yIBl9xDC8cTg^_p^gs8gOFxjutCC}vw&)#uoos&G>qN zPzP@({o?qLIfMf{F?`#0wSHI+>n`T``|q}bkB@ZZ2F8DW4cBEv*G!Ur<@EeB4?|i- zs>FCjanc2Ei~T5a>f^<=IB&HTJASku2S*O8d8qyQsS;K5e%BZwY4L>SY*tmLGlSY2d$Z6QuILsnxdceBwY=m{80W9p+Q-a) zzkH})EI7WbN?Cl^gGj|B@K`)R19g6o2>Zmz)C<+tXjGa+_H3M z%=)JO1;iLpu>~UZpK&pex-5lO#~nmWe7vEq?!+X$HVA0x!p=_HQ!r#X;wjgX}E5582E4cS>=7j&7I77=*_+1;3gzgbuY0kurL00$e}=3bE1Sjatd zo(uiUP+F-KH6vExw?9otxJ3Ug$x#o_sQ^04GuB{%TE{Z zuzMNbmY0{u#SgecgjNkmF53 za|08Nl+rk=BP*dr5c2NO2X*!Kl5^V%gXoR{aKf^Xm+qJSqEd$Z&$*+yIkU}3c9_*h zx2KCE&Z7XjS{)7)cjt9%pMUTzZ+pPt0K~!ZpoiyAIKKni){(p+CV?YSNdR`1q(+@x zjwh;(HUQm>R5#rGLihFJ#d>*k_$koej~#>PWMYUY2whJs2b$Vb0DC&kTicd5H}zZ! zscv%82;e`EZYcJt$LoQRUinx_3Wp#VgY4r@HezvEDg!z;$ zIP1r-8kqMWrzcx7@a5RwO>pxy4!d(?$m&!Uu!x)nrF$2sxXIUMLv5H;RFp^$%RE;B zbx=!vqu2NempSpM8$=egnj=hApmXyysvwCVLI4t#_%|vHJZcji9l$Ebo(vQGUxZ?{ zv;*>0a(6@xCuD4rJGsUQDG@w+B0hot2%Rh~42mr}^M`(d4K#38R~*jv%hTV+sF>X; z8r%H9nat&bY?fa;fb%`}QJ|0mmwDT6(%d7YL@A?Q@YVx~K3~1?o2rD@jThAm_n9jF zJ0d`yEJ~9ySPIFDr+rOxFC#tu?v3SLBD|oI5=9_Lzud*|CUiPE2B1dLEm{&5Z~wev z$exlC7qWVJvI7v;(C9%WD78IbE%X7Y=f6sfMBCe(k1o$@S+#0_T(5n$_TfV`^@;Gu zR@fEh?}7qkAalpMo-Uf~gOmvB7Zoy;kG@ON4r|x~!Jca`Q~4?eXMcTI3bJ}SqXO}% zw{|*scC}*D{nKKiB*#*zDccCMy|?!c#Evr%YY-a?8&cP|+xA+j6Ld9 zK6}%9At=aD2q6iH$(hr?X{e`zN3`~1e2_%>r$`UiMvk%uX+SjT@q;5azxBqXhcKdI zLTF(`KfmJ0j7LI82MO{ZVsZiuL{U*uKR>@Akr&e|oX0O;NMQtLphM(8G_$^A;9OF> za|>nObc0_2soRX>iWG+#FYez5Ai0GxyCROC_k-G`81vV3N7W9`5wmnvaP80PEa+ZU zRq6@~oa;}s6dMeS;S{;)u=l^oa*W*8mzRk_*MixAfiPd3MJZ<(P@hdrLjw&Ag;P|` zX9u3PqRD48 zzJCT(^|0^vSv3SnsEFkdu|q#!IDrNzsziZ=&V*9SnFplo+Xz}N^TjrO4wb7a^mvoDp^PsriLq3LzT_E#uvdoyvoQ1Nm#1 zWSpI~y}A2kQxX6ZAYxx%RdldcLGwwfzLw3zw^TE0Y)FWVV7cNOV==t-e z23}%0yxi2@$fzO;H=hHU6P{NND>5Z7)`h{Ng@B1}(YBo5(OkXKG6LYQVmk$LwR@1m z)j2g9*0ZBh1SmS^mZqjup6jb1kFL9$Ni2v$%VSilv17n>J$#(s z(Lz$9Y!M_eW$BfQVyBc^s*#i$q9(8!4-deHFYw%9saF2iG7R@xn^*|2j$m%+Xy|@< zU%yp2T!@jYoXpJxf#J^YvTqGIgJA+>$i#-{3|cgs2U!B^t!xa0=}*VFa@fjn8wbxB zhN@~9743|W!}-{-Z{Q2J)8TyIJ^@QBRa+a~sv-WaPc39zfXqBvTe_*0BUokeB`mQD z@ee2$bxG4WkM37`S#2(|?&t08{mKW%2s71hat443Ub_D8sD)u$8M?&=g$v8bST&x% zh!9L;<;omZ0((7FTMZkgAI_L3PS^WZ*(~;z%fO$|g}oUa7FTCEQOG?funalRiKpBW z&HVO9+xhPi&ZlKxwjU30}%E~8ss=<^J$gAtk- zPd$nDdr{X#`=S{TY?#8g8uo5Al>VGliKj5yo0aTeHflO{!7+GB2U+-`_>dmsIN18x*#>Rabf1_CHzlg2K7K8dCeIlS=YROT{qpKc4`e`b8NYOb zu5zEBh%lUfU-*deT3r~Q=2lk%+bSmi0|mTr6Z<=;2Lb!=aonIR{8o)Q-P0TR#^Nyt zpp1>9V++rpkp3|}z209Ty|lD6(1LESbw2=Q@>an43y{#$)6)^m5PHwXzKU!OrXC%< zxPs;V4JQK2pHHLsGXZGuzrlyBk)U`vN}#Z%fx4Z)@zdPFnSGSk^et4<(SiPjr2e{v zz#sYO!>&8VKXD4$>pwV!>*4>xXB4b~Krkzsb^efgt6AP68*?6Vw-2{4Of*RrXll0@ zWi~Sv{25>$TH4zqK%Lazw+G&hX@B<+phXL#L~n5nAl|KI;{Kdh_y-Yrw-!k>)}8pE z00EaM9q>s71qE$w5-|+I`2hN$I60kQX(`c!K6&!wbIq4qL5arK-=F-!gYbj_xS0wg zBjfq)k;0#6=jQs$Z1lQCLL~?(D5T9qSGlDzo;EZ#`UyjioWQmqz#@M&4+DwL_aPM( z71*snA!nE-6u3TL+ei7_>RFGOn7;O|=-m+}6qryWkn}sBsm0b3U}<;PlvhuD@>G$5 zsL4X?5w@j`4ry>N54;~x>qU~cEY zT)_|Xc~w2QYHkQ4k!@PC^oNHm4EQ9ZKWxXPV{@IXKbv zK4J!M+nOe)zGQKU&k2umIOhFr#gbV0^Bf6;=_h%_QzwyXA6wM1j z-H5F>bjBP5s;eN=T-jlJ_5DfZgyHGpMVCBD%_+|+9E4v8F2O$13CjwiQdu7{^mSWU zu)6A(Fubb33MIaL^i$cDRPt1L$U*N>FDDfw(W=L2bsw)OZBPTFpAEWmv~SWM7kCyK zB-HCHd)R$eH&AUY3q-QT246)W*SGF;6`2^kae~5o#(H*}^<2)2X)3UxgmkI4yze?u z(%p$&jGmkNB<0Te`2Y>`;Sk9lW+n?<6}II&G*$m&TX-E~=5vlL+w~Q>RihfFv;^(8 zdKWu z^=n|*utA9;I+VWKm)DhPYO3tB-(@~5NI7ERL7?KVEDh2q+^dZwW{F6n7Q7dC$tCBX zYo>=NiyF;DmSZ@kO;<{VA&qMWxMaIXO+Ogn(+duSyJ^^E^TB z5oh79$Cpb8`48u%NsN~;y6LNn{MsIiv~+qzz|2vnH0)zQZEtGJJE}F(V6zk+i1s=g zZN8+L$=ngi?bVl`J}_6dU^+WIV@9soYtt!YA|{7S{N9E?tC)e4`(L`rpg=eYx8K0L&53V>3OUp5I+U|2?)jHUh#q<*g;pEE8?;zLJ_U;f8FWH~FJ_Piv zHisE)HKcC2z`<qmQn9?6De+k01|9Rr<{}JW@H}SIX6%$S)oXVZxN1mNx8uApC2ZtH_VP}yc%~pnr?Pvr zcOZLjJ4+=sqgOu1bjdkX;n7L>5I?Y>>5~qPA~w4rF_WT4^NF@!(YF{<_?WEtl&i&8 z(N`^%MvC9^x&O)Zc1 za(aKxpxynHs}zPyMNLK0S$p!BC)RGm@-l^flz0E1qwgf-<1?8l0DqObO}NxyCr=9%;!3QxD*i8a@X0Z zLNymnmh(ZxCgSY)^P%;@jUthtv%SlSm5|bTcvfNV`s&?DS0x#l`qAuE541r1c(U2fDF+j`|Nqz@07 zPx;h&=JZTKC;6$%J!I)2A$huFfSc>5fxI{H-L%ek=(VzH`$B3%7fD)lZ{QHa?CyIe z%6ZoV0VI0*;`dpy(cFEUY&yI)3xQ-C>_3`@-#n9UYh_?mB3gRdx!4qx(y0G2KBRkT vbj0d@E&eSR46VQm!Us{=f65rTK@a@=y?Ek`%noc^4tXQ`?scB9w)g)4EPaK& literal 0 HcmV?d00001 diff --git a/backend/app.py b/backend/app.py index 08d38442..115c02c9 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2459,6 +2459,100 @@ def generate_summary(): except Exception as e: logging.error(f"Failed to clean up: {e}") +from datetime import datetime +from pathlib import Path + +from curation_report_generator import graph +from financial_doc_processor import markdown_to_html, BlobStorageManager +from financial_agent_utils.curation_report_utils import ( + REPORT_TOPIC_PROMPT_DICT, + InvalidReportTypeError, + ReportGenerationError, + StorageError +) +from financial_agent_utils.curation_report_config import ( + WEEKLY_CURATION_REPORT, + ALLOWED_CURATION_REPORTS, + NUM_OF_QUERIES +) + + +@app.route('/api/reports/generate/curation', methods=['POST']) +def generate_report(): + try: + data = request.get_json() + report_topic_rqst = data['report_topic'] # Will raise KeyError if missing + + # Validate report type + if report_topic_rqst not in ALLOWED_CURATION_REPORTS: + raise InvalidReportTypeError(f"Invalid report type. Please choose from: {ALLOWED_CURATION_REPORTS}") + + # Get report configuration + report_topic_prompt = REPORT_TOPIC_PROMPT_DICT[report_topic_rqst] + search_days = 10 if report_topic_rqst in WEEKLY_CURATION_REPORT else 30 + + # Generate report + logger.info(f"Generating report for {report_topic_rqst}") + report = graph.invoke({ + "topic": report_topic_prompt, # this is the prompt to to trigger the agent + "report_type": report_topic_rqst, # this is user request + "number_of_queries": NUM_OF_QUERIES, + "search_mode": "news", + "search_days": search_days + }) + + # Generate file path + current_date = datetime.now() + week_of_month = (current_date.day - 1) // 7 + 1 + if report_topic_rqst in WEEKLY_CURATION_REPORT: + file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}/Week_{week_of_month}.html") + else: + file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}.html") + + file_path.parent.mkdir(parents=True, exist_ok=True) + + # Convert and save report + logger.info("Converting markdown to html") + markdown_to_html(report['final_report'], str(file_path)) + + # Upload to blob storage + logger.info("Uploading to blob storage") + blob_storage_manager = BlobStorageManager() + upload_result = blob_storage_manager.upload_to_blob( + file_path=str(file_path), + blob_folder=f"Reports/Curation_Reports/{report_topic_rqst}" + ) + + # Cleanup files + logger.info("Cleaning up local files") + try: + # Use shutil.rmtree to recursively remove directory and all contents + import shutil + if file_path.exists(): + shutil.rmtree(file_path.parent, ignore_errors=True) + logger.info(f"Successfully removed directory: {file_path.parent}") + except Exception as e: + logger.warning(f"Error while cleaning up directory {file_path.parent}: {str(e)}") + # Continue execution even if cleanup fails + pass + + return jsonify({ + 'status': 'success', + 'message': f'Report generated for {report_topic_rqst}', + 'report_url': upload_result['blob_url'] + }) + + except KeyError: + logger.error("Missing report_topic in request") + return jsonify({'error': 'report_topic is required'}), 400 + + except InvalidReportTypeError as e: + logger.error(f"Invalid report topic: {str(e)}") + return jsonify({'error': str(e)}), 400 + + except Exception as e: + logger.error(f"Unexpected error during report generation: {str(e)}", exc_info=True) + return jsonify({'error': 'An unexpected error occurred while generating the report'}), 500 if __name__ == "__main__": diff --git a/backend/curation_report_generator.py b/backend/curation_report_generator.py new file mode 100644 index 00000000..39e0761a --- /dev/null +++ b/backend/curation_report_generator.py @@ -0,0 +1,498 @@ +# library +import os +from dotenv import load_dotenv +import requests +import markdown2 +import json +import operator +from datetime import datetime +from typing import Annotated, List, Optional, Literal +from typing_extensions import TypedDict +from pydantic import BaseModel, Field +from pathlib import Path +from IPython.display import Image, display, Markdown + +from langgraph.constants import Send +from langgraph.graph import START, END, StateGraph +from langchain_core.messages import HumanMessage, SystemMessage +from datetime import datetime +from financial_doc_processor import BlobStorageManager +from importlib import import_module +from llm_config import LLMManager, LLMConfig +from financial_agent_utils.curation_report_config import WEEKLY_CURATION_REPORT + +from prompts.curation_reports.general import ( + report_planner_query_writer_instructions, + report_planner_instructions +) + +from financial_agent_utils.curation_report_tools.web_search import CustomSearchClient + +load_dotenv() + +import logging + + +logging.basicConfig( + level = logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +#################################### +# LLM and Tools +#################################### + +llm_manager = LLMManager() +llm_writing = llm_manager.get_client(client_type='gpt4o', use_langchain=True) + +web_search_tool = CustomSearchClient() +# query (str): Search query +# max_results (int): Maximum number of results to return +# search_mode (str): Search topic (e.g., "news") +# search_days (int): Number of days to search for news +# **kwargs: Additional parameters to pass to the search endpoint (e.g., include_domains) + +MAX_RESULTS = 3 # for web search query results +REPORT_TYPES = Literal["Ecommerce", "Monthly_Economics", "Weekly_Economics"] + +# get the right system prompt for the report + +class ReportPrompts: + def __init__(self, report_type: str): + try: + # Dynamically import the prompt module based on report type + module_name = report_type.lower() + prompt_module = import_module(f"prompts.curation_reports.{module_name}") + + # Get all prompts from the module + self.report_structure = getattr(prompt_module, 'report_structure') + self.final_section_writer_instructions = getattr(prompt_module, 'final_section_writer_instructions') + self.query_writer_instructions = getattr(prompt_module, 'query_writer_instructions') + self.section_writer_instructions = getattr(prompt_module, 'section_writer_instructions') + except (ImportError, AttributeError) as e: + logger.error(f"Failed to load prompts for report type {report_type}: {str(e)}") + raise ValueError(f"Invalid report type or missing prompts: {report_type}") + +#################################### +# State Definitions +#################################### + +class Section(BaseModel): + name: str = Field( + description="Name for this section of the report.", + ) + description: str = Field( + description="Brief overview of the main topics and concepts to be covered in this section.", + ) + research: bool = Field( + description="Whether to perform web research for this section of the report." + ) + content: str = Field( + description="The content of the section." + ) + +class Sections(BaseModel): + sections: List[Section] = Field( + description="Sections of the report.", + ) + +class SearchQuery(BaseModel): + search_query: str = Field(None, description="Query for web search.") + +class Queries(BaseModel): + queries: List[SearchQuery] = Field( + description="List of search queries.", + ) + +class ReportState(TypedDict): + topic: str # Report topic + search_mode: Literal["general", "news"] # Search topic type + report_type: REPORT_TYPES # Report type + number_of_queries: int # Number web search queries to perform per section + sections: list[Section] # List of report sections + completed_sections: Annotated[list, operator.add] # Send() API key + search_days: Optional[int] # Only applicable for news topic + report_sections_from_research: str # String of any completed sections from research to write final sections + final_report: str # Final report + +class ReportStateOutput(TypedDict): + final_report: str # Final report + +class SectionState(TypedDict): + search_mode: Literal["general", "news"] # Search topic type + report_type: REPORT_TYPES # Report type + search_days: Optional[int] # Only applicable for news topic + number_of_queries: int # Number web search queries to perform per section + section: Section # Report section + search_queries: list[SearchQuery] # List of search queries + source_str: str # String of formatted source content from web search + report_sections_from_research: str # String of any completed sections from research to write final sections + completed_sections: list[Section] # Final key we duplicate in outer state for Send() API + +class SectionOutputState(TypedDict): + completed_sections: list[Section] # Final key we duplicate in outer state for Send() API + + +#################################### +# Research Planning +#################################### + +def generate_report_plan(state: ReportState): + logger.info(f"Starting report plan generation for topic: {state['topic']}") + + # Inputs + topic = state["topic"] + report_type = state["report_type"] + number_of_queries = state["number_of_queries"] + search_mode = state["search_mode"] + search_days = state["search_days"] + + # get the right system prompt for the report + report_prompts = ReportPrompts(report_type) + report_structure = report_prompts.report_structure + + # Generate search query + structured_llm = llm_writing.with_structured_output(Queries) + + # Format system instructions + system_instructions_query = report_planner_query_writer_instructions.format(topic=topic, + report_organization=report_structure, + number_of_queries=number_of_queries, + today_date = datetime.now().strftime("%B %Y")) + + # Generate queries + results = structured_llm.invoke([SystemMessage(content=system_instructions_query)]+[HumanMessage(content="Generate search queries that will help with planning the sections of the report.")]) + logger.info(f"Generated {len(results.queries)} search queries to conduct web search") + + # Web search + query_list = [query.search_query for query in results.queries] + + ################################################## + # At this point, we have successfully generated a + # list of search queries ready for web search execution. + ################################################## + + search_tasks = [] + logger.info(f"Conducting web search to design sections") + for query in query_list: + try: + result = web_search_tool.search(query=query, + search_mode=search_mode, + max_results=MAX_RESULTS, + search_days=search_days) + search_tasks.append(result) + except Exception as e: + logger.warning(f"Search failed for query '{query}': {str(e)}") + # Add empty/default search result + search_tasks.append({ + "query": query, + "results": [] + }) + + # Only proceed with formatting if we have any results + if search_tasks: + search_tasks_str = web_search_tool.format_results_for_llm(results=search_tasks) + else: + search_tasks_str = "No search results found. Proceeding with report generation based on general knowledge." + + ################################################## + # At this point, we have successfully conducted X web searches (max_results) on X queries (number of queries). + ################################################## + + # Format system instructions + system_instructions_sections = report_planner_instructions.format(topic=topic, + report_organization=report_structure, + context=search_tasks_str) + + # Generate sections + structured_llm = llm_writing.with_structured_output(Sections) + + logger.info(f"Generating section plan for the report") + report_sections = structured_llm.invoke([SystemMessage(content=system_instructions_sections)]+[HumanMessage(content="Generate the sections of the report. Your response must include a 'sections' field containing a list of sections. Each section must have: name, description, plan, research, and content fields.")]) + + ################################################## + # Now, we have parsed web search results and topic subject matter to sections (intro, subject 1-2 in body, conclusion). + ################################################## + + logger.info(f"Generated a report plan with {len(report_sections.sections)} sections") + return {"sections": report_sections.sections} + +#################################### +# Section writing +#################################### + +def generate_queries(state: SectionState): + """ Generate search queries for a section """ + + # Get state + number_of_queries = state["number_of_queries"] + section = state["section"] + report_type = state["report_type"] + + # Generate queries + structured_llm = llm_writing.with_structured_output(Queries) + + # Format system instructions + report_prompts = ReportPrompts(report_type) + system_instructions = report_prompts.query_writer_instructions.format(section_topic=section.description, number_of_queries=number_of_queries) + + # Generate queries + logger.info(f"Generating {number_of_queries} search queries for section {section.name}") + queries = structured_llm.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate search queries on the provided topic.")]) + + ################################################## + # At this point, we have successfully generated queries ready for web search execution. + ################################################## + + logger.info(f"Search queries generated for section: {state['section'].name}") + return {"search_queries": queries.queries} + +def search_web(state: SectionState): + logger.info(f"Starting web search for section: {state['section'].name}") + + """ Search the web for each query, then return a list of raw sources and a formatted string of sources.""" + + # Get state + search_queries = state["search_queries"] + search_mode = state["search_mode"] + search_days = state["search_days"] + + # Web search + query_list = [query.search_query for query in search_queries] + + search_tasks = [] + + ################################################## + # Here, for each search query, we conduct X web searches (max_results) and return X sources. + ################################################## + + + for query in query_list: + search_tasks.append(web_search_tool.search(query=query, + search_mode = search_mode, + max_results= MAX_RESULTS, + search_days= search_days)) + logger.info(f"Returning {MAX_RESULTS} sources for query: {query}") + + # convert search_tasks to a string + search_tasks_str = web_search_tool.format_results_for_llm(results=search_tasks) + + ################################################## + # total searches conducted = number of search queries * max_results + # all converted to a string before saved to state + ################################################## + + logger.info(f"Completed web search for section {state['section'].name}") + return {"source_str": search_tasks_str} + +def write_section(state: SectionState): + logger.info(f"Writing content for section: {state['section'].name}") + + """ Write a section of the report """ + + # Get state + section = state["section"] + source_str = state["source_str"] + report_type = state["report_type"] + + # Format system instructions + report_prompts = ReportPrompts(report_type) + system_instructions = report_prompts.section_writer_instructions.format(section_title=section.name, + section_topic=section.description, + context=source_str) + + # Generate section + logger.info(f"Generating section content for section {state['section'].name}") + section_content = llm_writing.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate a report section based on the provided sources.")]) + + ################################################## + # Here, we have successfully generated a section of the report. + ################################################## + + + ################################################## + # IMPORTANT: here, it is saving section content directly to state['section'] object + ################################################## + logger.info(f"Saving section content to: {section.name} section object") + section.content = section_content.content + + ################################################## + # content was empty when report plan was generated, now it is added + # research-required sections have content generated and added to completed_sections state + ################################################## + + # Write the updated section to completed sections + logger.info(f"Completed writing content for section: {state['section'].name}") + return {"completed_sections": [section]} + +# Add nodes and edges +section_builder = StateGraph(SectionState, output=SectionOutputState) +section_builder.add_node("generate_queries", generate_queries) +section_builder.add_node("search_web", search_web) +section_builder.add_node("write_section", write_section) + +section_builder.add_edge(START, "generate_queries") +section_builder.add_edge("generate_queries", "search_web") +section_builder.add_edge("search_web", "write_section") +section_builder.add_edge("write_section", END) + +# Compile +logger.info(f"Compiling section builder graph") +section_builder_graph = section_builder.compile() + +# View +# display(Image(section_builder_graph.get_graph(xray=1).draw_mermaid_png())) + +#################################### +# End to end report generation +#################################### + + +def initiate_section_writing(state: ReportState): + """ This is the "map" step when we kick off web research for some sections of the report """ + + # Kick off section writing in parallel via Send() API for any sections that require research + logger.info(f"Kicking off section writing for {len(state['sections'])} research-required sections") + return [ + Send("build_section_with_web_research", {"section": s, + "number_of_queries": state["number_of_queries"], + "search_mode": state["search_mode"], + "search_days": state["search_days"], + "report_type": state["report_type"]}) + for s in state["sections"] + if s.research + ] + +def write_final_sections(state: SectionState): + """ Write final sections of the report, which do not require web search and use the completed sections as context """ + + logger.info(f"Writing final/non-research section: {state['section'].name}") + + # Get state + section = state["section"] + completed_report_sections = state["report_sections_from_research"] + report_type = state["report_type"] + + # Format system instructions + report_prompts = ReportPrompts(report_type) + + current_date = datetime.now() + week_of_month = (current_date.day - 1) // 7 + 1 + year = current_date.year + current_week_and_month_and_year = f"Current week: {week_of_month}, Current month: {current_date.strftime('%B')}, Current year: {year}" + + if report_type in WEEKLY_CURATION_REPORT: + system_instructions = report_prompts.final_section_writer_instructions.format(section_title=section.name, + section_topic=section.description, + context=completed_report_sections, + current_week_and_month=current_week_and_month_and_year) + ################################################## + # Here we need to include include week so that it can write proper report title + ################################################## + else: + system_instructions = report_prompts.final_section_writer_instructions.format(section_title=section.name, + section_topic=section.description, + context=completed_report_sections) + + # Generate section + logger.info(f"Generating final section content for section {state['section'].name}") + section_content = llm_writing.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate a report section based on the provided sources.")]) + + # Write content to section + logger.info(f"Saving final section content to: {section.name} section object") + section.content = section_content.content + + ################################################## + # here, we have generated content for non-research sections + # content is added to completed_sections state to generate final report + ################################################## + + logger.info(f"Completed writing final section: {state['section'].name}") + return {"completed_sections": [section]} + +def format_sections(sections: list[Section]) -> str: + """ Format a list of sections into a string """ + formatted_str = "" + for idx, section in enumerate(sections, 1): + formatted_str += f""" + {'='*60} + Section {idx}: {section.name} + {'='*60} + Description: + {section.description} + Requires Research: + {section.research} + + Content: + {section.content if section.content else '[Not yet written]'} + + """ + return formatted_str + +def gather_completed_sections(state: ReportState): + """ Gather completed sections from research """ + + # List of completed sections + completed_sections = state["completed_sections"] + + # Format completed section to str to use as context for final sections + logger.info(f"Combining completed sections to one single string") + completed_report_sections = format_sections(completed_sections) + + return {"report_sections_from_research": completed_report_sections} + +def initiate_final_section_writing(state: ReportState): + """ This is the "map" step when we kick off research on any sections that require it using the Send API """ + + # Kick off section writing in parallel via Send() API for any sections that do not require research (intro and conclusion) + logger.info(f"Kicking off final section writing for non-research sections") + return [ + Send("write_final_sections", {"section": s, + "report_sections_from_research": state["report_sections_from_research"], + "report_type": state["report_type"]}) + for s in state["sections"] + if not s.research + ] + +def compile_final_report(state: ReportState): + logger.info("Starting final report compilation") + + """ Compile the final report """ + + # Get sections + sections = state["sections"] + completed_sections = {s.name: s.content for s in state["completed_sections"]} + + # Update sections with completed content while maintaining original order + # actually, this is not necessary, since we have already added content to section in previous steps + for section in sections: + section.content = completed_sections[section.name] + + # Compile final report + logger.info(f"Compiling final report with {len(sections)} sections") + all_sections = "\n\n".join([s.content for s in sections]) + + logger.info("Completed final report compilation") + return {"final_report": all_sections} + +# Add nodes +builder = StateGraph(ReportState, output=ReportStateOutput) +builder.add_node("generate_report_plan", generate_report_plan) +builder.add_node("build_section_with_web_research", section_builder.compile()) +builder.add_node("gather_completed_sections", gather_completed_sections) +builder.add_node("write_final_sections", write_final_sections) +builder.add_node("compile_final_report", compile_final_report) + +# Add edges +builder.add_edge(START, "generate_report_plan") +builder.add_conditional_edges("generate_report_plan", initiate_section_writing, ["build_section_with_web_research"]) +builder.add_edge("build_section_with_web_research", "gather_completed_sections") +builder.add_conditional_edges("gather_completed_sections", initiate_final_section_writing, ["write_final_sections"]) +builder.add_edge("write_final_sections", "compile_final_report") +builder.add_edge("compile_final_report", END) + +# Compile +logger.info(f"Compiling report builder graph") +graph = builder.compile() +# display(Image(graph.get_graph(xray=1).draw_mermaid_png())) diff --git a/backend/financial_agent_utils/curation_report_config.py b/backend/financial_agent_utils/curation_report_config.py new file mode 100644 index 00000000..0231ebf2 --- /dev/null +++ b/backend/financial_agent_utils/curation_report_config.py @@ -0,0 +1,10 @@ +#################################### +# Curation report config +#################################### + +ALLOWED_CURATION_REPORTS = ["Ecommerce", "Monthly_Economics", "Weekly_Economics"] + +WEEKLY_CURATION_REPORT = ['Weekly_Economics'] +MONTHLY_CURATION_REPORT = ['Monthly_Economics', 'Ecommerce'] + +NUM_OF_QUERIES = 2 \ No newline at end of file diff --git a/backend/financial_agent_utils/curation_report_tools/__init__.py b/backend/financial_agent_utils/curation_report_tools/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/financial_agent_utils/curation_report_tools/web_search.py b/backend/financial_agent_utils/curation_report_tools/web_search.py new file mode 100644 index 00000000..4e4bba65 --- /dev/null +++ b/backend/financial_agent_utils/curation_report_tools/web_search.py @@ -0,0 +1,150 @@ +from typing import Literal, Optional, List +import requests +from pydantic import BaseModel, HttpUrl +import logging +from pathlib import Path +from pydantic_settings import BaseSettings + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# search default setting +class SearchSettings(BaseSettings): + SEARCH_API_ENDPOINT: HttpUrl = "https://webgpt0-vm2b2htvuuclm.azurewebsites.net/api/web-search" + class Config: + env_prefix = "SEARCH_" # Allow override with env vars like SEARCH_MAX_RESULTS + +search_settings = SearchSettings() + +class SearchResult(BaseModel): + title: str + date: Optional[str] # date is optional because it is not always available + url: str + content: str + +class SearchResponse(BaseModel): + query: str + results: List[SearchResult] + +class CustomSearchClient: + """Client for performing web searches using a custom search endpoint. + + Attributes: + endpoint (str): The search API endpoint + """ + + def __init__(self, endpoint: str = search_settings.SEARCH_API_ENDPOINT): + self.endpoint = endpoint + + def search(self, + query: str, + max_results: int = 2, + search_days: int = 15, + search_mode: Literal["news", "web"] = "news", + include_domains: Optional[List[str]] = None, + **kwargs) -> SearchResponse: + """ + Perform a web search using the custom endpoint. + + Args: + query (str): Search query + max_results (int): Maximum number of results to return + search_days (int): Number of days to search back + search_mode (Literal["news", "web"]): The type of search to perform + include_domains (Optional[List[str]]): List of domains to include in the search + **kwargs: Additional parameters to pass to the search endpoint + + Returns: + SearchResponse: Search results in a format similar to Tavily's response + """ + if not query.strip(): + raise ValueError("The search query must be non-empty.") + if search_days < 0: + search_days = 15 + + payload = { + "query": query, + "mode": search_mode, + "max_results": max_results, + "search_days": search_days, + "include_domains": include_domains, + } + + try: + response = requests.post(self.endpoint, json=payload) + response.raise_for_status() + return SearchResponse(**response.json()) + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP error occurred: {e}") + raise + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error occurred: {e}") + raise + except requests.exceptions.Timeout as e: + logger.error(f"Timeout error occurred: {e}") + raise + except requests.exceptions.RequestException as e: + logger.error(f"An error occurred: {e}") + raise + except ValueError as e: + logger.error(f"Error parsing search response: {e}") + raise + + def format_results_for_llm(self, results: List[SearchResponse]) -> str: + """Format search results for LLM consumption. + + Args: + results: List of search responses to format + + Returns: + Formatted string containing all search results + """ + formatted = [] + + for query_result in results: + formatted.extend([ + f"\nSearch Query: {query_result.query}\n", + "-" * 80, + self._format_individual_results(query_result.results) + ]) + + return "\n".join(formatted) + + def _format_individual_results(self, results: List[SearchResult]) -> str: + """Helper method to format individual search results. + + Args: + results: List of individual search results to format + + Returns: + Formatted string containing the results + """ + formatted = [] + + for idx, result in enumerate(results, 1): + formatted.extend([ + f"Result {idx}:", + f"Title: {result.title}", + f"Date: {result.date}", + f"URL: {result.url}", + "\nContent:", + f"{result.content}\n", + "-" * 40 + ]) + + return "\n".join(formatted) + +if __name__ == "__main__": + # Test the client + try: + client = CustomSearchClient() + query = "Who won the 2024 presidential election?" + + results = client.search(query = query, max_results=4, search_mode="news", search_days=-1) + print(client.format_results_for_llm([results])) + except Exception as e: + logger.error(f"Test failed: {str(e)}") \ No newline at end of file diff --git a/backend/financial_agent_utils/curation_report_utils.py b/backend/financial_agent_utils/curation_report_utils.py new file mode 100644 index 00000000..fcc2f154 --- /dev/null +++ b/backend/financial_agent_utils/curation_report_utils.py @@ -0,0 +1,27 @@ +######################### +# Curation Report Generator +######################### +# get the current month and year to format Month_Year.html +from datetime import datetime +current_month = datetime.now().strftime("%B") +current_year = datetime.now().strftime("%Y") + + +REPORT_TOPIC_PROMPT_DICT = { + "Ecommerce": f"Please provide an ecommerce report for {current_month} {current_year}", + "Monthly_Economics": f"Please provide an economics report for {current_month} {current_year}", + "Weekly_Economics": f"Please provide an economics report for this week" +} + + +class ReportGenerationError(Exception): + """Base exception for report generation errors""" + pass + +class InvalidReportTypeError(ReportGenerationError): + """Raised when report type is invalid""" + pass + +class StorageError(ReportGenerationError): + """Raised when storage operations fail""" + pass \ No newline at end of file diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index dd440ec5..c2ef6470 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -7,6 +7,7 @@ import shutil from pathlib import Path from collections import defaultdict +import markdown2 from typing import Dict, List import pandas as pd @@ -23,12 +24,13 @@ from utils import convert_html_to_pdf from app_config import BLOB_CONTAINER_NAME, PDF_PATH +# Load environment variables +load_dotenv() + BLOB_CONNECTION_STRING = os.getenv('BLOB_CONNECTION_STRING') BLOB_CONTAINER_NAME = os.getenv('BLOB_CONTAINER_NAME') -# Load environment variables -load_dotenv() # configure logging logging.basicConfig( @@ -282,6 +284,61 @@ def create_document_paths(output_path: str, equity_name: str, financial_type: st } } +def markdown_to_html(markdown_text: str, output_file: str): + """Convert markdown to HTML using markdown2""" + # Define CSS styles + css_styles = """ + + """ + + html_content = markdown2.markdown(markdown_text, extras=["tables"]) + + # Combine CSS with HTML content + final_html = f""" + + + + + {css_styles} + + + {html_content} + + + """ + + # Create output directory if it doesn't exist + Path(output_file).parent.mkdir(parents=True, exist_ok=True) + with open(output_file, 'w', encoding='utf-8') as f: + f.write(final_html) + class BlobStorageError(Exception): """Base exception for blob storage operations""" pass @@ -306,6 +363,17 @@ class BlobDownloadError(BlobStorageError): """Failed to download blob""" pass +class ReportGenerationError(Exception): + """Base exception for report generation errors""" + pass + +class InvalidReportTypeError(ReportGenerationError): + """Raised when report type is invalid""" + pass + +class StorageError(ReportGenerationError): + """Raised when storage operations fail""" + pass class BlobStorageManager: def __init__(self): try: @@ -399,18 +467,71 @@ def download_documents(self, equity_name: str, return downloaded_files # make sure the document_paths is a dict with the structure of create_document_paths - def upload_to_blob(self, document_paths: dict) -> Dict: + def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: """ - Upload files to Azure Blob Storage with organized folder structure based on filing type. + Upload files to Azure Blob Storage. Can handle either a document_paths dictionary + or a single file path. Args: - document_paths (dict): Nested dictionary with equity IDs and their filing types - container_client: Azure blob container client - base_folder (str): Base folder name in blob storage + document_paths (dict, optional): Nested dictionary with equity IDs and their filing types + file_path (str, optional): Direct path to a file to upload + blob_folder (str, optional): Custom folder path in blob storage (defaults to self.blob_base_folder) Returns: - dict: Dictionary of upload results with equity IDs and filing types as keys + dict: Dictionary of upload results """ + if not document_paths and not file_path: + raise ValueError("Either document_paths or file_path must be provided") + + if document_paths and file_path: + raise ValueError("Cannot provide both document_paths and file_path") + + # Handle single file upload + if file_path: + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + try: + # Use provided blob folder or default to base folder + base_folder = blob_folder if blob_folder else self.blob_base_folder + blob_path = f"{base_folder}/{os.path.basename(file_path)}" + # set the content type based on the file extension + if blob_path.endswith('.pdf'): + content_type = 'application/pdf' + elif blob_path.endswith('.html'): + content_type = 'text/html' + elif blob_path.endswith('.txt'): + content_type = 'text/plain' + else: + content_type = 'application/octet-stream' + with open(file_path, "rb") as data: + try: + self.container_client.upload_blob( + name=blob_path, + data=data, + overwrite=True, + content_settings=ContentSettings(content_type=content_type) + ) + except Exception as e: + raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") + + # get the blob url for the uploaded file + blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" + + result = { + "status": "success", + "blob_path": blob_path, + "blob_url": blob_url + } + logger.info(f'Document has been uploaded to {blob_path}') + return result + + except Exception as e: + result = {"status": "failed", "error": str(e)} + logger.error(f"Failed to upload file {file_path}: {str(e)}") + return result + + # Handle document_paths dictionary upload (original functionality) if not isinstance(document_paths, dict): raise ValueError("document_paths must be a dictionary") @@ -424,22 +545,34 @@ def upload_to_blob(self, document_paths: dict) -> Dict: blob_path = f"{self.blob_base_folder}/{filing_type}/{equity}_summary.pdf" if "summary" in document_path else f"{self.blob_base_folder}/{filing_type}/{equity}.pdf" + # set the content type based on the file extension + if blob_path.endswith('.pdf'): + content_type = 'application/pdf' + elif blob_path.endswith('.html'): + content_type = 'text/html' + elif blob_path.endswith('.txt'): + content_type = 'text/plain' + else: + content_type = 'application/octet-stream' + with open(document_path, "rb") as data: try: self.container_client.upload_blob( name=blob_path, data=data, overwrite=True, - content_settings=ContentSettings(content_type='application/pdf') + content_settings=ContentSettings(content_type=content_type) ) except Exception as e: raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") # get the blob url for the uploaded file blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" - upload_results[equity][filing_type] = {"status": "success", - "blob_path": blob_path, - "blob_url": blob_url} + upload_results[equity][filing_type] = { + "status": "success", + "blob_path": blob_path, + "blob_url": blob_url + } logger.info(f'Document has been uploaded to {blob_path}') except Exception as e: upload_results[equity][filing_type] = {"status": "failed", "error": str(e)} diff --git a/backend/llm_config.py b/backend/llm_config.py index 8ad3f9f6..18689240 100644 --- a/backend/llm_config.py +++ b/backend/llm_config.py @@ -3,6 +3,7 @@ from typing import Dict, Optional import json from openai import AzureOpenAI +from langchain_openai import AzureChatOpenAI import os from dotenv import load_dotenv @@ -60,7 +61,7 @@ class Config: class LLMManager: def __init__(self): self.prompts = PromptTemplate() - self._clients: Dict[str, AzureOpenAI] = {} + self._clients: Dict[str, AzureOpenAI | AzureChatOpenAI] = {} self.config: Dict[str, LLMConfig] = { "gpt4o": LLMConfig( api_base=os.getenv('OPENAI_API_BASE'), @@ -76,16 +77,31 @@ def __init__(self): ) } - def get_client(self, client_type: str = "gpt4o") -> AzureOpenAI: - """Get or create an Azure OpenAI client""" - if client_type not in self._clients: + def get_client(self, client_type: str = "gpt4o", use_langchain: bool = False) -> AzureOpenAI | AzureChatOpenAI: + """Get or create an Azure OpenAI client + + Args: + client_type: Type of client to create ("gpt4o" or "embedding") + use_langchain: If True, returns a LangChain AzureChatOpenAI client instead of regular AzureOpenAI + """ + client_key = f"{client_type}_langchain" if use_langchain else client_type + + if client_key not in self._clients: config = self.config[client_type] - self._clients[client_type] = AzureOpenAI( - api_key=config.api_key, - api_version=config.api_version, - base_url=f"{config.api_base}/openai/deployments/{config.model_name}" - ) - return self._clients[client_type] + if use_langchain: + self._clients[client_key] = AzureChatOpenAI( + openai_api_key=config.api_key, + openai_api_version=config.api_version, + azure_endpoint=config.api_base, + deployment_name=config.model_name + ) + else: + self._clients[client_key] = AzureOpenAI( + api_key=config.api_key, + api_version=config.api_version, + base_url=f"{config.api_base}/openai/deployments/{config.model_name}" + ) + return self._clients[client_key] def get_prompt(self, prompt_type: str) -> str: """Get a prompt template by type""" diff --git a/backend/prompts/curation_reports/__init__.py b/backend/prompts/curation_reports/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/prompts/curation_reports/ecommerce.py b/backend/prompts/curation_reports/ecommerce.py new file mode 100644 index 00000000..8964c694 --- /dev/null +++ b/backend/prompts/curation_reports/ecommerce.py @@ -0,0 +1,204 @@ +# Structure +report_structure = """ +This report type is focused on ecommerce trends and the industry news this month. + +The report shouild adhere to the following structure: + +1. **Introduction** (no research needed) + - Provide a brief overview of the ecommerce landscape + - Offer context for analyzing recent business trends + +2. **Main Body**: + - One dedicated section for each major ecommerce platform/company in this list: + * Overall industry trends, Amazon, Shopify, Walmart, Target, Home Depot, Lowe's + - Each section should examine the news and highlight any of the following: + * Tracking significant business events (funding, acquisitions, partnerships) + * Analyzing product launches and feature updates + * Shifts in market strategy and positioning + * Identifying emerging patterns across the industry + * Considering competitive responses and market dynamics + +3. No Main Body Sections other than the ones dedicated to each platform/company in the provided list + +4. Conclusion +- A timeline of key events across companies +- Analysis of emerging industry patterns +- Implications for the broader market""" + +query_writer_instructions=""" + +Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. + +Topic for this section: + +``` +{section_topic} +``` + +When generating {number_of_queries} search queries, ensure they: +1. Cover key aspects of the eCommerce topic, such as: + - Recent business events (e.g, funding, mergers, acquisitions) + - Product launches and feature updates + - Shifts in market strategies and competitive positioning + - Emerging industry patterns or trends + - Customer behavior and technological adoption + +2. Include eCommerce-specific terms, company names, or platform features to refine the search + +3. Target recent information by including relevant time markers (e.g.,"Q4 2024", "December 2024") + +4. Seek insights on: + - Comparisions on differentiators between eCommerce plaforms or companies + - Implications of new strategies or technologies in the industry + +5. Focus on credible sources, such as: + - Official announcements, press releases + - Market research reports + - Blogs, forums, and articles on practical implementation or customer feedbacks + +Your queries should be: +- Specific enough to avoid generic results +- Targeted enough to the eCommerce industry and the topic +- Diverse enough to cover all aspects of the section plan +""" + +# Section writer instructions +section_writer_instructions = """ +You are an expert technical writer responsible for crafting one section of an eCommerce report. + +### Title of the section: +``` +{section_title} +``` + +### Topic for this section: +``` +{section_topic} +``` + +### Guidelines for Writing: + +1. **Technical Accuracy:** + - Include specific metrics, dates, and version numbers where applicable. + - Reference concrete business events (e.g., funding, partnerships, product launches). + - Cite official sources like press releases, financial reports, or industry studies. + - Use precise eCommerce terminology (e.g., platform names, market strategies). + +2. **Length and Style:** + - Limit the section to **150-200 words**. + - Avoid any marketing language; maintain a technical and analytical focus. + - Write in clear, simple language suitable for professional readers. + - Start with your **most important insight in bold**. + - Use short paragraphs (2-3 sentences) for better readability. + +3. **Structure:** + - Use `##` for the section title (Markdown format). + - Include only ONE of the following structural elements, if it clarifies your point: + * A **focused Markdown table** comparing 2-3 key metrics, features, or trends: + - Example: | Platform | Key Feature | Date | + * A **short Markdown list** (3-5 items): + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Properly format and indent all structural elements. + +4. **Writing Approach:** + - Include at least one **specific example or case study** related to the eCommerce topic. + - Focus on concrete insights (e.g., measurable impacts of a strategy or feature). + - Prioritize clarity and conciseness—avoid generalizations or unnecessary details. + - Begin directly with the content; no preamble or introductions. + - Emphasize the single most important insight in your analysis. + - Don't include any sources in the content section. Save sources for the sources section. + +5. **Sources:** + - Use the provided source material to support your analysis: + ``` + {context} + ``` + - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT + ``` + - : [Source](url) + ``` + + Here is a good example example: + ``` + Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) + ``` + + This is a bad example: + ``` + Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) + ``` + + - Include title, date, and URL for each source. + +6. **Quality Checks:** + - Strictly adhere to the **150-200 word count** (excluding title and sources). + - Use only one structural element (table or list) where necessary. + - Start with **bold insight** to capture attention. + - Ensure your writing is concise, specific, and actionable. +""" + + +final_section_writer_instructions="""You are an expert technical writer crafting a section that synthesizes information from the rest of the report. + +Title of the section: +``` +{section_title} +``` + +Section description: +``` +{section_topic} +``` + +Available report content: +``` +{context} +``` + +1. Section-Specific Approach: + +For Introduction: +- Use # for report title (Markdown format). You must include a title for the report +- The title should mention the month and year of the report along with the main ecommerce theme of the month. Here is an example" + +``` +January 2024: eCommerce Trends to Kickstart the New Year +``` + +- 50-100 word limit +- Write in simple and clear language +- Focus on the core motivation for the report in 1-2 paragraphs +- Use a clear narrative arc to introduce the report +- Include NO structural elements (no lists or tables) +- No sources section needed + +For Conclusion/Summary: +- Use ## for section title (Markdown format) +- 100-150 word limit +- Leverage the insights in this report by aligning strategies with market trends, mitigating identified risks, and implementing recommended actions to drive immediate business impact. +- Highlight (bold) the actionable, insightful suggestions +- For comparative reports: + * Must include a focused comparison table using Markdown table syntax + * Table should distill insights from the report + * Keep table entries clear and concise +- For non-comparative reports: + * Only use ONE structural element IF it helps distill the points made in the report: + * Either a focused table comparing items present in the report (using Markdown table syntax) + * Or a short list using proper Markdown list syntax: + - Use `*` or `-` for unordered lists + - Use `1.` for ordered lists + - Ensure proper indentation and spacing +- End with specific next steps or implications +- No sources section needed + +3. Writing Approach: +- Use concrete details over general statements +- Make every word count +- Focus on your single most important point + +4. Quality Checks: +- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section +- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section +- Markdown format +- Do not include word count or any preamble in your response""" \ No newline at end of file diff --git a/backend/prompts/curation_reports/general.py b/backend/prompts/curation_reports/general.py new file mode 100644 index 00000000..55f9f709 --- /dev/null +++ b/backend/prompts/curation_reports/general.py @@ -0,0 +1,65 @@ + +# Prompt to generate a search query to help with planning the report outline +## general prompt: +report_planner_query_writer_instructions=""" +You are an expert technical writer, helping to plan a report. + +Current month and year: {today_date} + +The report will be focused on the following topic: + +``` +{topic} +``` + +The report structure will follow these guidelines: + +``` +{report_organization} +``` + +Your goal is to generate {number_of_queries} search queries that will help gather comprehensive information for planning the report sections. + +The query should: + +1. Be related to the topic +2. Help satisfy the requirements specified in the report organization + +Make the query specific enough to find high-quality, relevant sources while covering the breadth needed for the report structure.""" + +# Prompt generating the report outline +## general prompt: +report_planner_instructions=""" + +You are an expert technical writer, helping to plan a report. + +Your goal is to generate the outline of the sections of the report. + +The overall topic of the report is: + +``` +{topic} +``` + +The report should follow this organization: + +``` +{report_organization} +``` + +You should reflect on this information to plan the sections of the report: + +``` +{context} +``` + +Now, generate the sections of the report. Each section should have the following fields: + +- Name - Name for this section of the report. +- Description - Brief overview of the main topics and concepts to be covered in this section. +- Research - Whether to perform web research for this section of the report. +- Content - The content of the section, which you will leave blank for now. + +Consider which sections require web research. For example, introduction and conclusion will not require research because they will distill information from other parts of the report.""" + + diff --git a/backend/prompts/curation_reports/monthly_economics.py b/backend/prompts/curation_reports/monthly_economics.py new file mode 100644 index 00000000..2939754a --- /dev/null +++ b/backend/prompts/curation_reports/monthly_economics.py @@ -0,0 +1,212 @@ +# Structure +report_structure = """ +This report type is focused on analyzing key economic trends and significant events of the past month. + +The report shouild adhere to the following structure: + +1. **Introduction** (no research needed) + - Provide a brief overview of the domestic and global economic landscape + - Offer context for understanding the key economic events and trends analyzed in the report + + +2. **Main Body**: + - Organize sections based on the following categories: + * **Global Economic Trends**: + - Overview of major global economic indicators (e.g., GDP growth, inflation, unemployment rates) + - Analysis of significant developments (e.g., central bank policies, trade agreements, geopolitical events) + * **Regional Highlights**: + - Focus on major regions: North America, Europe, Asia-Pacific, and Emerging Markets + - Key trends, policy changes, and regional challenges + * **Industry-Specific Analysis**: + - Highlight significant trends in major industries such as technology, energy, finance, and healthcare + - Include macroeconomic influences and sectoral performance metrics + * **Financial Market Insights**: + - Overview of stock market performance, bond yields, and currency movements + - Analysis of investor sentiment and market outlook + +3. **Conclusion** + - Recap of key economic events and trends for the month + - Emerging global and regional patterns + - Implications for businesses, policymakers, and investors + +""" + +query_writer_instructions=""" + +Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. + +Topic for this section: + +``` +{section_topic} +``` + +When generating {number_of_queries} search queries, ensure they: +1. Cover key aspects of the eCommerce topic, such as: + - Recent global and regional economic events (e.g., GDP growth, inflation, unemployment) + - Central bank policies and monetary decisions + - Trade agreements, geopolitical developments, and regulatory changes + - Industry-specific trends and performance metrics + - Financial market movements (e.g., stock indices, bond yields, currencies) + +2. Include economics-specific terms, key metrics, and relevant regions or countries to refine the search. + +3. Target recent information by including relevant time markers (e.g., "Q4 2024," "December 2024"). + +4. Seek insights on: + - Comparisons of economic indicators across regions or industries + - Implications of policy changes, global events, or economic shifts for businesses and investors + +5. Focus on credible sources, such as: + - Reports from international economic organizations (e.g., IMF, World Bank, OECD) + - Official government or central bank statements + - Market research, industry reports, and financial analyst commentary + - News articles, blogs, and expert opinion pieces on key economic topics + +Your queries should be: +- Specific enough to avoid generic results +- Targeted to the economics topic and region of interest +- Diverse enough to cover all aspects of the section plan +""" + +# Section writer instructions +section_writer_instructions = """ +You are an expert technical writer responsible for crafting one section of a Monthly Economics Report. + +### Title of the section: +``` +{section_title} +``` + +### Topic for this section: +``` +{section_topic} +``` + +### Guidelines for Writing: + +1. **Technical Accuracy:** + - Include specific metrics, dates, and key economic indicators (e.g., GDP growth, inflation rates, unemployment figures). + - Reference concrete events (e.g., central bank decisions, trade agreements, geopolitical developments). + - Cite official sources such as government reports, financial analyses, or statements from international organizations. + - Use precise economic terminology and maintain clarity. + +2. **Length and Style:** + - Limit the section to **150-200 words**. + - Maintain an analytical and professional tone; avoid opinionated or speculative language. + - Write in clear, concise language suitable for policymakers, analysts, and professionals. + - Start with your **most important insight in bold**. + - Use short paragraphs (2-3 sentences) for better readability. + +3. **Structure:** + - Use `##` for the section title (Markdown format). + - Include only ONE of the following structural elements if relevant: + * A **focused Markdown table** summarizing key metrics or comparisons: + - Example: | Region | GDP Growth (%) | Inflation (%) | + * A **short Markdown list** (3-5 items): + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Properly format and indent all structural elements. + +4. **Writing Approach:** + - Include at least one **specific example or case study** related to the economic topic. + - Focus on actionable insights (e.g., implications of a policy change or economic trend). + - Avoid generalizations or excessive detail; prioritize clarity and conciseness. + - Begin directly with the content; avoid introductions or background that restates the title or topic. + - Emphasize the single most critical insight in your analysis. + - Do not include sources in the main content; list them in the sources section. + +5. **Sources:** + - Use the provided source material to support your analysis: + ``` + {context} + ``` + - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT + ``` + - : [Source](url) + ``` + + Here is a good example example: + ``` + Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) + ``` + + This is a bad example: + ``` + Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) + ``` + + - Include title, date, and URL for each source. + +6. **Quality Checks:** + - Strictly adhere to the **150-200 word count** (excluding title and sources). + - Use only one structural element (table or list) where necessary. + - Start with **bold insight** to capture attention. + - Ensure your writing is concise, specific, and actionable. +""" + + +final_section_writer_instructions="""You are an expert technical writer crafting a section that synthesizes information from the rest of the report. + +Title of the section: +``` +{section_title} +``` + +Section description: +``` +{section_topic} +``` + +Available report content: +``` +{context} +``` + +1. Section-Specific Approach: + +For Introduction: +- Use # for report title (Markdown format). You must include a title for the report +- The title should mention the month and year of the report along with the main economic theme of the month. Example: + +``` +January 2024: Key Economic Trends Shaping the Global Landscape +``` + +- 50-100 word limit +- Write in simple and clear language +- Focus on the purpose and scope of the report in 1-2 paragraphs +- Use a concise narrative arc to introduce the report +- Include NO structural elements (no lists or tables) +- No sources section needed + +For Conclusion/Summary: +- Use ## for section title (Markdown format) +- 100-150 word limit +- Leverage the insights from this report by identifying actionable strategies for policymakers, businesses, or investors to address risks and capitalize on trends. +- Highlight (bold) key takeaways and actionable insights. +- For comparative reports: + * Must include a focused comparison table using Markdown table syntax. + * Table should distill insights from the report. + * Keep table entries clear and concise. +- For non-comparative reports: + * Use ONLY ONE structural element IF it helps clarify points made in the report: + * Either a focused table summarizing key metrics or findings (using Markdown table syntax). + * Or a short list using proper Markdown list syntax: + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Ensure proper indentation and spacing. +- End with actionable implications or recommendations. +- No sources section needed. + +3. Writing Approach: +- Prioritize concrete details over generalizations. +- Ensure every word contributes to clarity and precision. +- Focus on the single most critical insight for each section. + +4. Quality Checks: +- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section. +- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section. +- Use Markdown format. +- Do not include word count or any preamble in your response. +""" \ No newline at end of file diff --git a/backend/prompts/curation_reports/weekly_economics.py b/backend/prompts/curation_reports/weekly_economics.py new file mode 100644 index 00000000..7aa07dd7 --- /dev/null +++ b/backend/prompts/curation_reports/weekly_economics.py @@ -0,0 +1,215 @@ +# Structure +report_structure = """ +This report type is focused on analyzing key economic trends and significant events of the past week. + +The report should adhere to the following structure: + +1. **Introduction** (no research needed) + - Provide a brief overview of the domestic and global economic landscape for the week. + - Offer context for understanding the key economic events and trends analyzed in the report. + +2. **Main Body**: + - Organize sections based on the following categories: + * **Global Economic Trends**: + - Overview of major global economic indicators for the week (e.g., GDP updates, inflation snapshots, unemployment figures). + - Analysis of significant developments (e.g., central bank announcements, trade disputes, geopolitical updates). + * **Regional Highlights**: + - Focus on major regions: North America, Europe, Asia-Pacific, and Emerging Markets. + - Key events, policy changes, and notable economic challenges. + * **Industry-Specific Updates**: + - Highlight weekly developments in key industries such as technology, energy, finance, and healthcare. + - Brief analysis of macroeconomic influences on sectoral performance. + * **Financial Market Movements**: + - Weekly performance of stock markets, bond yields, and currency movements. + - Analysis of investor sentiment and short-term market trends. + +3. **Conclusion** + - Recap of key economic events and trends for the week. + - Emerging global and regional patterns. + - Implications for businesses, policymakers, and investors over the near term. +""" + +query_writer_instructions=""" + +Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. + +Topic for this section: + +``` +{section_topic} +``` + +When generating {number_of_queries} search queries, ensure they: +1. Cover key aspects of the weekly economic topic, such as: + - Major global and regional economic events of the week (e.g., GDP updates, inflation reports, unemployment rates) + - Central bank statements or decisions announced during the week + - Significant trade agreements, geopolitical developments, or regulatory changes + - Weekly trends in specific industries and performance metrics + - Financial market movements (e.g., weekly stock index changes, bond yields, currency fluctuations) + +2. Include economics-specific terms, key metrics, and relevant regions or countries to refine the search. + +3. Target recent information by including weekly time markers (e.g., "week of December 18, 2024," "last week December 2024"). + +4. Seek insights on: + - Week-to-week comparisons of economic indicators across regions or industries + - Immediate implications of new policies, global events, or economic shifts for businesses, policymakers, and investors + +5. Focus on credible sources, such as: + - Reports and updates from international economic organizations (e.g., IMF, World Bank, OECD) + - Central bank announcements and government statements + - Market research, weekly financial analyses, and expert commentary + - News articles or blogs providing real-time insights on economic events + +Your queries should be: +- Specific enough to avoid generic results +- Focused on recent events relevant to the week in review +- Diverse enough to cover all aspects of the section plan +""" + +# Section writer instructions +section_writer_instructions = """ +You are an expert technical writer responsible for crafting one section of a Weekly Economics Report. + +### Title of the section: +``` +{section_title} +``` + +### Topic for this section: +``` +{section_topic} +``` + +### Guidelines for Writing: + +1. **Technical Accuracy:** + - Include specific metrics, dates, and key economic indicators relevant to the week (e.g., GDP updates, weekly inflation rates, unemployment figures). + - Reference concrete events (e.g., central bank announcements, trade negotiations, geopolitical updates). + - Cite official sources such as government releases, financial analyses, or reports from international organizations. + - Use precise economic terminology while maintaining clarity. + +2. **Length and Style:** + - Limit the section to **150-200 words**. + - Maintain an analytical and professional tone; avoid subjective or speculative language. + - Write in clear, concise language suitable for policymakers, investors, and professionals. + - Start with your **most important insight in bold**. + - Use short paragraphs (2-3 sentences) for better readability. + +3. **Structure:** + - Use `##` for the section title (Markdown format). + - Include only ONE of the following structural elements if relevant: + * A **focused Markdown table** summarizing key weekly metrics or comparisons: + - Example: | Indicator | Value | Date | + * A **short Markdown list** (3-5 items): + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Properly format and indent all structural elements. + +4. **Writing Approach:** + - Include at least one **specific example or case study** relevant to the economic topic of the week. + - Focus on actionable insights (e.g., immediate implications of a policy change or trend). + - Avoid generalizations or excessive background information; prioritize clarity and conciseness. + - Begin directly with the content; avoid introductions or redundant restatements of the title or topic. + - Highlight the single most important takeaway in your analysis. + - Do not include sources in the main content; list them in the sources section. + +5. **Sources:** + - Use the provided source material to support your analysis: + ``` + {context} + ``` + - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT + ``` + - : [Source](url) + ``` + + Here is a good example example: + ``` + Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) + ``` + + This is a bad example: + ``` + Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) + ``` + + - Include title, date, and URL for each source. + +6. **Quality Checks:** + - Strictly adhere to the **150-200 word count** (excluding title and sources). + - Use only one structural element (table or list) where necessary. + - Start with **bold insight** to capture attention. + - Ensure your writing is concise, specific, and actionable. +""" + +final_section_writer_instructions=""" +You are an expert technical writer crafting a section that synthesizes information from the rest of the report. + +Current week and month: +``` +{current_week_and_month} +``` + +Title of the section: +``` +{section_title} +``` + +Section description: +``` +{section_topic} +``` + +Available report content: +``` +{context} +``` + +1. Section-Specific Approach: + +For Introduction: +- Use # for report title (Markdown format). You must include a title for the report +- The title should mention the week and month of the report along with the main economic theme of the week. Example: + +``` +Week 1 of January 2024: Key Economic Trends Shaping the Global Landscape +``` + +- 50-100 word limit +- Write in simple and clear language +- Focus on the purpose and scope of the report in 1-2 paragraphs +- Use a concise narrative arc to introduce the report +- Include NO structural elements (no lists or tables) +- No sources section needed + +For Conclusion/Summary: +- Use ## for section title (Markdown format). +- 100-150 word limit. +- Leverage the insights from this report by identifying actionable strategies for policymakers, businesses, or investors to address risks and capitalize on trends. +- Highlight (bold) key takeaways and actionable insights. +- For comparative reports: + * Must include a focused comparison table using Markdown table syntax. + * Table should distill insights from the report. + * Keep table entries clear and concise. +- For non-comparative reports: + * Use ONLY ONE structural element IF it helps clarify points made in the report: + * Either a focused table summarizing key metrics or findings (using Markdown table syntax). + * Or a short list using proper Markdown list syntax: + - Use `*` or `-` for unordered lists. + - Use `1.` for ordered lists. + - Ensure proper indentation and spacing. +- End with actionable implications or recommendations. +- No sources section needed. + +3. Writing Approach: +- Prioritize concrete details over generalizations. +- Ensure every word contributes to clarity and precision. +- Focus on the single most critical insight for each section. + +4. Quality Checks: +- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section. +- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section. +- Use Markdown format. +- Do not include word count or any preamble in your response. +""" \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index db99eeac..28c929e8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,7 @@ azure-identity==1.15.0 flask==2.2.2 flask-cors==3.0.10 werkzeug==2.2.2 -requests +requests==2.28.2 python-dotenv==1.0.0 azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 @@ -21,9 +21,13 @@ sec-edgar-downloader==5.0.2 pdfkit==1.0.0 pydantic<=2.10.3 tavily-python==0.5.0 -openai<=1.57.4 +openai tiktoken<=0.8.0 pandas<=2.2.3 pymupdf==1.23.7 reportlab==4.2.5 -Flask-Session==0.8.0 \ No newline at end of file +Flask-Session==0.8.0 +langgraph==0.2.60 +markdown2==2.5.2 +langchain-openai==0.2.14 +pydantic-settings==2.7.0 \ No newline at end of file From c05c36ac16f3a34fe56d70f5675b39f34209411f Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 26 Dec 2024 15:03:20 +0000 Subject: [PATCH 258/820] removed requests version --- backend/curation_report_generator.py | 1 - backend/requirements.previous.txt | Bin 1270 -> 0 bytes backend/requirements.txt | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 backend/requirements.previous.txt diff --git a/backend/curation_report_generator.py b/backend/curation_report_generator.py index 39e0761a..fe987bbc 100644 --- a/backend/curation_report_generator.py +++ b/backend/curation_report_generator.py @@ -10,7 +10,6 @@ from typing_extensions import TypedDict from pydantic import BaseModel, Field from pathlib import Path -from IPython.display import Image, display, Markdown from langgraph.constants import Send from langgraph.graph import START, END, StateGraph diff --git a/backend/requirements.previous.txt b/backend/requirements.previous.txt deleted file mode 100644 index 01da5d6871e4f0af8cd54524eae50c2a986e7f5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1270 zcmai!-EPxR5QOI%iFe=uaN4w#7DNz8s22#NB9MB4s>;nzV;aZ7PC?1T1K*qtv0W}u zaiZFi!3xDVyU$Xt_|1`C?kzO zO!#vxh-q|}PLFsVm$`hh)GZn3{44lxhQky)(5cQ6!)(tG+G^O0JKo`jyG)m8smZhD z_a0Vb`uSYnch06TtamJdI7_;$X+<4t^i-(HsITJAHU3C^t}gWbuNM!`?_k-$l9PV{ zXGZ=hm0OdwJj7dJ#c56@p2`hT72nZgs~+k_QsYRYYdW|B8wjgxr!a=cU3RXD&w_~8 zAdPSzkM&fq(%thtXFIt}pY=%}^p?0_DREX zy;*i*_#+&Xk!M1ERuT^V3BvAhcJF|@8ob^%XJ2tLV>g^T{L%HXXEPx>HhGK_VspG% z_D1N>skk7|hOFIbny>G+igWLbciLRuniKYUz`G>43bfdX-il?yn4#+ytFfkLuf~1~ mE%w<*I&`6TI^WlGl$?B;GI%O0_-nHGEfag=H(5B=D*gZ?9>z5Q diff --git a/backend/requirements.txt b/backend/requirements.txt index 28c929e8..e38ce669 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,7 @@ azure-identity==1.15.0 flask==2.2.2 flask-cors==3.0.10 werkzeug==2.2.2 -requests==2.28.2 +requests python-dotenv==1.0.0 azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 From 325970cc271ef56ffc09c46ba4c756b5c85dc04d Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 26 Dec 2024 10:08:00 -0500 Subject: [PATCH 259/820] Revert "FA 294 curation report (#230)" This reverts commit 2da25c6386b4de1586538c68f6e01a3e5ae04bfc. --- .../Curation_Report_Agent.png | Bin 31974 -> 0 bytes backend/app.py | 94 ---- backend/curation_report_generator.py | 498 ------------------ .../curation_report_config.py | 10 - .../curation_report_tools/__init__.py | 0 .../curation_report_tools/web_search.py | 150 ------ .../curation_report_utils.py | 27 - backend/financial_doc_processor.py | 157 +----- backend/llm_config.py | 36 +- backend/prompts/curation_reports/__init__.py | 0 backend/prompts/curation_reports/ecommerce.py | 204 ------- backend/prompts/curation_reports/general.py | 65 --- .../curation_reports/monthly_economics.py | 212 -------- .../curation_reports/weekly_economics.py | 215 -------- backend/requirements.txt | 10 +- 15 files changed, 25 insertions(+), 1653 deletions(-) delete mode 100644 backend/Agent_Graph_Images/Curation_Report_Agent.png delete mode 100644 backend/curation_report_generator.py delete mode 100644 backend/financial_agent_utils/curation_report_config.py delete mode 100644 backend/financial_agent_utils/curation_report_tools/__init__.py delete mode 100644 backend/financial_agent_utils/curation_report_tools/web_search.py delete mode 100644 backend/financial_agent_utils/curation_report_utils.py delete mode 100644 backend/prompts/curation_reports/__init__.py delete mode 100644 backend/prompts/curation_reports/ecommerce.py delete mode 100644 backend/prompts/curation_reports/general.py delete mode 100644 backend/prompts/curation_reports/monthly_economics.py delete mode 100644 backend/prompts/curation_reports/weekly_economics.py diff --git a/backend/Agent_Graph_Images/Curation_Report_Agent.png b/backend/Agent_Graph_Images/Curation_Report_Agent.png deleted file mode 100644 index 6843d7defda528c33e39cafc7df4433ed3fe671f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31974 zcma%jWk6J2)GpTRh=2n~2oeK=bV;WUAYIZe-Jo=b4j>^NA_CIg-69Rr-Q68B^xTc_ z`+nd3ZhelhIZSN1+ zI1-0~O3|_6T~aML#d$T7tS8a*v<7rqet|>@xt;y}D%7=4ML#qhg!$1h<28E9pP!hi zd#a@Sg_NMj7W_2IIwDl2XjO9?85u00c<)jy5)gp5s#00GP_~ys!D1Y#9edjw;XWg@ zcE~oTY)V`_Ih94Ekrk%hTiMl@<9bn6#*(R_#IzxRD5UkK_Juq{bOhD5dXem#d`xx_J44F72^`P)R+319ig z&(f^D$od9`6)GG?$!o**U|jWw<9LhYmUZ*4)SJ{2R|gzOY-gJ zq~}a!9br*-9-K|@a3|wDEOi>90AH2)qTs^A>vrtEZas2a48uagQ;Q;6a_TKV6wdEq zEGL(bc;RA50A~yf)3E;btJJ~Fw>F=sBk$pwu&rpX8#WjX?>&QM8B(?#_AIB%9_B1;?-lpFi%28V|Hu}ID@&+OjJUMufdDa{Eo zS|kV1KNT1MeEE&8bT1lOtTtViuzgzl_44GeH_9gKaU~@~Z{0}1DQ)6%D=y)=kuW2(=qpYD}uCLGL zdFd=CC+B|G%ggJ<%}*(%SjZ=l6pxFO&qN%XrIlQ66S0qs%eT_ZClaQ%6;-&cTpK@Y zxdf>BPEq9Q&oy|wfBzl^gSiU|F)@Yo80ThXG3Yk%GcYhbd)6{Lt0N`#&;~zIz-@JH zEi*Y8n~aPMAK%!}(9p{2v)uB@&J;zs=~CP0m6a80YHHnv`iblu)~>)-Mph%=I}ct# zk`MI*0*UIWGQNd}HyM|H{P^(+Mq80y^S+OYiV6g#sHmu^spt6LHYsVZap{(uTdAUQW-OkhhUsG9y$WHdrwUHw3>ba?_gCQlZNX1E zJ3CtpsHv!W;$E~54GoQrsZxYPe90b3j*pLbEFE~TM6n=VA&FnX&9mM;5p@{><9q%* zQzoACf$vXmZ|{}v*q~MiC+*#tT7t)qm6Vms4Lc(^EJm|(bEVSXoaN5(kV29pZnJ9Q zofAM@)j5=^jt?VZHkG(}cz$n=Z0#>J6LVP)4-Lh0zh8`9+McbS>hEWvp!m@AHVh}A zFN#2l-C=%G_^y>WW(=M7ls9cb4CmYFw(Oi7IAp-IjMbpc)WV|C{R{`zqF)S!S65db z4xm4L$jHce;0y*69UJR4>F2JqLtta$XwRFGaj!8EG8q;+{`7q6xlxQ`Po39MM>yqJ zp)T_9u&d|hXJrLRXd#$Z~)t@Zf+S+O} zTNmDGcv^8`(%{Y)*=gDKHJ2`3;*pe=*e3fDJ03w9jAL>ltHHwOPN$!rpR%$tc+%j{ zpD`g2LOw?ZIHb8~fB&7b@`%IMXab)TlaP?t=5TgMNQlK)fp)2}OkbjaYN1X-Y^)RN z`ieL6Y-K9Ga@9$$dvd}(^JC`leXyaGlqLK2I*${KUpm5;r9SlxS@f}O{e;y*CB1c2 zFpa2X-nO{!tKMp|lF`!A+I(7&ovkh}zrXU%#AMB=JBGs|&f!yVF#r3-me}TMEo%vh z&e&$K$VPHix8lFDl(Ow8yCmu5UtZ&3-nSBG3>Tup)ymbL+bA0u8IhEf)WtF>o%s5d z(NjQ0K|w)Xo$}nD&~Ia7V+6Tbo$w^#A}ikU0~m$4IHR03zKUx1_Ic$%H%93j5jhmC zEiof`Efn2{@wWyqa->5Rx4b8pvPF4tNK@z~zu-+A`~J3`j{q@YhG$4{Sl^l_OZ>YO zdqP$qf-JhD^PT5@lr^+k!&Y3(s5LWF{^jBUgwM(<(5d|p3@u7NY>P17+3OKBh~ zGSVYIuAllHD;#}Zz#nt?fPhxpy*fRSh#P|HR>@fN=L^ zDLqWGgHy*FziqNM$)DsshwUZC7-a@TkVbK1uEaa}Qho&a2lH@2rZv}%YA-R`3_Yss zZdcAG1|A2mY646pEH}7?!py9=Do>3ex+C$~<9Ll+HTxy|^@7)q+URIFgWT%0c-3S% z`)fP{>uROCIp@b4igtE(m)kSOErvtmPiu0Bgtg=KCMC*yaVcIUki&-AEXRu&H7f|8 zH0h}3crX1&I)pPC8~1qiG3_&g4;`-d5SK`ymA`~%iOOJ}{CM;58A2m4D5#U^*6{5I z$@nBBRbIDm&Qd*p?!S%mk}(WwUR+j&xHB6CPRFFBJ>%~7dGmL!m6v)b@aQi46L5vE zSAPZ0g9wl%3PM`NFzr1YIOEybSy8$yZ5R~o6S!V$OUqqX^*86H9*aZD z)zsAbP4|=KA?}33hv&a7vXvwq@#ZPPnU})CbW#!)>P#MOKj+hm2%x31-^!a@SIJ1MjtlexMi~U5uyHR{LF6l`9*3p zoMlS1jE^JL9(;f7$EFgc%9tIa;kb4}D84!**C{PNnS0n0bY|^yjzP7)?Nc+;7$Iiz z-Lt{H1f6^*H!hl@#(}fXt`6362U>)j?=~D4dJn%jHFZYb@Z)`!cV6|Kd-`17u?H0? zWnaF@a+^Jna8oE~Ql+rieOcUZs^)2o$5TpeRAAJK!zf_Hd*@vB`>Qg0_13eIv|nGp z?a}xp58fWPyGN8$^CnQd#{4hO)?4I%zuG_R=w^|j{l5-`*+fJ~$Hhgza>SES(9rmL ze7?dZYj`PwnnRtI$I+$Gh>H(-bcmCYa37E4t{D;xK8~cmuT|&lw@vrhJB2zeE32p~ zt(7cK+C88|gu(j6;!9(*=)fsMPEJSlG<-pRQRg|P{h94_a`T0)ZOLTZS3UNx1utz5 zZ0ND!aq3SrE3KXPSrpXc%*^yS^nSAEVkJP~dO>SMPrkAc23H+rwvLR{yZ(;ju)F2o z+?46Kx*cH9o_cg>Ywz&;{xS0DhEPnWAwC`+7V$1Vo6+djvTCKZc4<+OMy2tV&&S%u zCFZqd<0EJHN64n8SBmTQuEr{_`<;al{Uvh*48o@KFZb>p#o|0xOk!TE;?Z~VSgDuw zq5#qMW?_1x(~^M!*&Wq~%*?bR#K?YO04laPmZYsB;yA#_g{m-I)PYT!!Y-!3J! zwGF=eBddyq*a(xn>~eB)HnkLVugGa+^>8BIZB6EINY&rJIN2V!7!O!~E=MYp#7O{q z_IS1D3s2LJAEvJB{20vzT0BA)CuuvHk18xo{Ghy!{56faVyA(h%7&e`I2qOs%7in1 z!67>+yR`${fo6|8($bTSUP-pc<37ia%pMY4hV;?%x`u}KefqssF|=XNn{Yk z$U~rcd0BP9|HQW1{F?H%i^iqs!`MQ|BVw|rgFT}`_ubGFeY8}ZIxL!-BpXJgE?Razzhb5iz?@T#alLk(xYG|<8wO#hLW!s1@ z6pkWa$tV4Mw)`t@DI;TNtydiZEh#A>Wwq^3XhFy(@R9}3=4U6Dd+u;vs`kwE(7&55 zs(P4dg6I{7JRu@tzWn_NqnTGjeNOySqZ7)pBTC)HrP^Qr#E(&9VcG^w@I9%Vn?Xz0 zM)J|V5TC;)znOlP`>1B>b(ByNC*_N)wjO5`wog}AR}4M?lLiJuy$MzassTZ;{f(!M z8am#nD(i|9Nb;_uK%3mLkh^nZP^so|vH&}gRnMk4lYbC&(RSpgDx>G+soq$DwEEh zQR-{=2g1}NZ#<^>qN=*KYPf_lVO2lu zlP2$XJ3nTHuQz=ZDORha;I|ZYCcMU(<>>3~SvWX_!1!uOlX`2ulcRn#X(M;%n$|vf z-P;n;!tjw>6PKSg*-IHPJvphDO%l`qP;UL;qxH68Hzy}5I(n`_20uV#b(I7PKf4DP zYj22r-g9bis64`ZUL}b^m96vvinF=~L;O?~SvMW4nFwrX&{J2Zg@0<*@4mmf_VBrX zuwJP$b#PKnp-q+3-Y9Y-`#0XizP{km#?uE~Zn1}}rChc=T5-}UupAnC`VslDOYU#E zeq3UwNtP8|surf?Sm0&kWYM53otssoEgalyLp6(?&>w&ufuZ^+S0E63V0wjMQ$Nu&=32Oo!1 zwu@R$`yzkuY`gM%oS!NWK0Nz^hlke^xjEW%tGQ^XSd92EW^oaYi79ibCW zlcs(Mdv=MM+sME>VqA%iikYr^Vq-9a2Q##qSs7Tm4@wfRjkeE?Q25>WCzkk{G`}n? zt=QtW7t>3%u*O=dNZ{h?64lEnWHoLcjc*2vRmTCY|Me9zu@R+)(M*W(+ST=IT--f4 zjp_6#oLZFRhB zU*UM;v#=~1N=}V-cX9hv%0~GzK08H@ukh!s!aPdl5R(3S$zN2GldF9SzeM7bV z?(|OKsT8X}(yrQkG@+)<#zt0taBi#sY$xhShpY1uFS3~=HW%vnp4X7Lc&Dm&2hzi% zn2}!JUKOPfm7gl5vOL&$JF#&ZAbj{)710mfTd?!gF#T>rSnaY`L4pm!gFd0Lp56+d z<+5rfKCcxhHs+5vQjxnes|TkZ7ggp*4x3L)b?2^^+GKf2+!#1dzN9KPW_4WAt1=3b zxUqAaqWkIT=`C1{G6e-|WTvKWZ7vR!=@V&^`Zs?Ln0WrH&tk6Ez4h>O0T{0%CoyLK2|h1^qgD7yzf z_4$}A=ur|oYgt;_>0o;4t0ijOye#UxTbtx-y`w8s5zQ8Zmuuw~!?n+F9(Y!L&&e@w zFX$;QZfDZHTGOKG*vpl7&!4a9da1vDEm}TZ z9a*Grq?W)eYCVgb=}yLPJ@tbjq@|~vDvImR$66N`DM&--raF1v{Sqk&GS6z&qt9)1 zm{y9s>Xg;NnV6j1o5ogLlltu-IE5v;Y2?Eh)S?sfb9?9B4MP5JK@RvtSR|AP(`F_F&Lqj$}}W3!GD)d!w-1xkk6qLP3f=Kh{XEy;H*K7%%5L zCQh`}c#eI;EuI9dL7YK12Wg!XrAQ7Z+g{IhiDxCvxkC&)Biuw? z8m`jALfQE=NGK;jrsbVtX;0LJMQum{5t-@aK|TINlAXU=->-MQfcL9HPr zB7%^oc~*HU_M$54`sMh1$em7S@|k&$<)0_Z+E z_~k!bZVDMtvmBRX8TEYSc&CG({&ztp_6#TdpIam*CK3@5{h5`Wn`J=^XS_h}F#}@h zpNG^pFp!dz{CAE3J;A}jadW#SM?j5!$4tft({fWby_HHZzbPJ__un$bO5GGc<5=L{ zWw2q}-fj)ujHlHu{6Q>q7rAAUo0cZ#2(!ZaoY7)z;l)$8t20_^!l%<1 zqVVR;8B@7WD5(&>!}|AM`md(0wI$MVs1>`LmpWpZM`rxU5ey7c8 zzRS=tNLRZ#xg{_++2ED8v|TH|xxHqja8rMt?1eHiFi`(9pVKoWJWt0jQ^WkITUx1T zC>CEDWO}Yj*Tqk@#?hTXm_0%vKZyYf&~x8BvnMrd)h=l)!N;+HSnZWq`qD|mBOA&fhkFe~yGMTzXY?%A^* z>ODV)N((bh=I|3l4FXytn4R85YG!6eGt$%RyuMnHihbSMsx1te zskZyllx6T*wP1R(`p6kP?8S-Sn~gC!PYH47A|okjSJ#+^hLEJB_Kxt6P&kExk&@D{ zbg2i?(IeENV`g-MoECjSV()8^_0%g1XIPV^){Wxob4-3SPE3IEuG2Fc_$J%QWh)pg^Rgcy3LUO6ln-NJyL= zEX9VBHRk1ZhXzggg7>?bkr`?tuZv*}AtfG7gCTk=#lGhVdfnnZEmv65UQ1cxXCIHiyx|vuu&O zxT*!0k&%&wneAl!<1Uw{Uu}%t-50N~iYF%Enwq(Do(~5G7OuLg{rGzisR8fW7gw557G)axPIy(S zX!9Kp&ly!UpbfY*I!sJr~9Jf z9cf!HSBe>@D5UP-Vr#gr_ERm&$*`u63GJVcUO_O)b~)cK z+VD9(B^I2Pk;tBmhVx!AQ{Q8NpRg@p7&+ku7VH4pdmy-A0Z@83_?2YzQ{{N-#1D@5NL z@9^>y%2%Y06cr_@C@5~>*vR|(_ZgR}Lz1VbFYDdp3e{MpbTTU{R=c8W0Xv!$9^PzR`r#ue@PqZ5 z|C%oZaKYyEUFF%)^F=QZZ$|oTKxZaYOWTb}i(7rxL!X7v4VefQr#@Ts0gO`2wM%H4 zUe$W05p@h+byw`7;#4Jb6ES_U+S4ab6xG!7k`I0lWyP0}z%z%gF8O1aKD%CD%8V6| zd}Z+!3)R8L#}6eHP*1esy`s;L^YQsE6^rF|^3AX`uGznqmY$vxfrCRD*t(c3EWDB` z@$8Me%|^cFBa)|<`_AlI8XCsd*4N|3!pp)>Nv!(51;|Ntw+KDEToQvUZ1Hl%u4`a% zaD;sP*i&xd^jwSPEn5wKzOgy=|=xg{2B__5zJC~5Ylw-j6_q9TQIE8rBL_q&s zjN4RxPAV41_?H^1lXX#b^@9edgPGc6EmOAev% z^K)KIuZMnm@mwgPCr@y(-?Q+ZPwzGM6_sv1dyId0_7%KrGx#YlH|(|-yHv2)moi#XIf z*)G-%5i$-&hLDIop#HS?E>b*gj$8MGGLnjfL`PWA!i9fd2jq8(#j(E8_~hgt)6<4S z8M>4dSbyF9Ma*gW&VfX4YeHUK)RGaNGo^l z_qx}N1ajGPCl|?UYD!&~Y9^s{{dLc_-^oaezj}oY|I|KYIyrksMur0+|LWBe7O5R~ z&O`i5ZpZEZYP+#tai!-!eRs^wzkT?iFCbv$5>Ma0lC!u~udp9lTC~69(sOnudiagq zCn!kR`Lvfxib7dQt)M`Fj*gC!(g4K&MePLUb~0A|`2y|s#;cTs!rs`oBObIujs5-d zl?wN=vPUl8j!gvf@i=UaFO5MFD8T; znwh~<2bgr~CbHrdYhPhv+`V;6Cp~@T$rBy{SNj<8XU}D|!b24m6ppftKrF9yN^*BK zgeGwWr3WJ;w!YmG?|+Ai|uBU%9G#ATpkpy%>UGVQ~`rkqEdRbZ@9CD=!Ew z8?-rFSXk7%pN)>I$*IPAf!%cYzD57NgG2e{82X&62xOu5b=qJ}ygV3NAo{>c_k%;& zssA-2(STn|tM=Lf`?%$5z9wq6?(FULWx#2hcWUa++0hu-%+CCu*VpZ6X`iU5IJhVA z+M7PdQNBLx_Dk|4GBM5Kv(K`y+}4ebR+g2maB=Z{@ZgaPe@f;v5G_&r%?wUX&tJSq zU212xBg8OrzwrcFg*6V90gB6!dsTn zxmASyK#wiDJ%ogaz=@HAqxr|n{f*rZ)pj32)&$^W@tqR5k4Q>RN(yg5!{PCEWK@*x z#+#q6PvN-i(VWBjZajM38Mo?S3 z!C%bAJzleSYD)B34!=Fk?&TTrrs<7m-U-;Rhm|3v7 zwdL&0?P2iZ%^sc-1n=i>*xcu&IZGp>zdkmFbsCP0=o4WYunIj6g=uTVd+U-d zE-17MM#Kngr4@tRi14*4I7;`pP${!eMGoNEioPa2FXLj)Ea(-m%SiT52mFKL8$ox@ z{dO`h(~UFEE@ToVo_sDQb^zUhhnVi*z(CH?Fy0^@UUXFS$ooN>&wd`ir7^2@y=I)x zY=35E$`TQ8?aYxe%~1}+kF28hgxdFB>rF!OgC>@(xzvY8st=o8WH^m~%|Dlm5S+nW z-wdwq6omv$RHA$B%qVqDE}*Aiya_c{1(>gy)Ftthky$$cXi%%ql6$39D9{o}0+E&w z8QY-PeJqdPn>-V~?)Uukv&+P5K8PnNoQ^%@tv|KDqy~ zjHb?$H=%)XQ?A4!IZ!1&Y;$w7{-Q$rYD-^K)Fqb5Q9^>b?JwEGC!7AzbL7C4b}i*x zoy6eq@TTz=iJf0`#%B0^8gRWM z-6Jj2Bby-jMx8cry3S^BSpky#gB6!r_RH&{?Y+57>tMCB#T7*+)qK@HLD2Rh-|{(T z%o(9Jyy%qy1oszq)~LR*i3$$`qjb}-HF-q;M}H@ROWvWGo^Ge{j)=vS$V{xhID!%b zP4?MrwQ^U9cl}Y`-rp_8t}9V}BqR)c0DX(mMKhsa032-aYyE(TNF$VV24!#t56}8( zA>X5j%W0)9UwYDplc@vQpHUi@X3F;o*z63{msTYF6SG*3} zmvkGg8{?_HSARhZF&EPtoc1cR-aKw_H*gWH_VF1YYb?|}x%ePwqPkX6sErcS(2!g& zt6o_#LJ8K)(Jc*_;{t`<(xNQp2@&U@7sITJ8aC<4@2Mc;CFp^eqva zx3EF9*6#VjLL)LmI`IjM?NfNi%*<9(b4y_XbTK{S1z{s2x=E=;!Q%GNl&FqSQ;CU@ zk(!2@%cB!f2$*Bb!TafG8Yb|ru5Qrq#lQ&O7dk*iMLhXsSvy?bZ(v36^mP_hyd ztng3W*+}dDfPhL{+tT_vAwg6PP+}laLD@qez4j;&e$Kc8+zT&swKqdZtU#|0qp75} z)}{67Y@_2TV;On4}hrmit@bBG&Bjo$5I^6+S` zcdKjw(#gW!%iGDR26LT}#=~?v(d!}gplSIOOH8>OTaNT<^G&Jx@7X)vZzrD1V2lh8 z2MU%UL9YsQYBu=t90WtN9jFnnQ$G{o)mb<#AL0d_x+lCa2}QBEM}>!XULdm}$z77+ zH;uZ?wv`+_e0<1VS83{_T*@Q?r;_jFLgw9`J}(JiX{o7FVNkFmfqFrqb>C$&Jv|+? z+H|is$0~_GH!pDNWv}3kR&Epeon5iGhGq7(ecJEQ9Q_in-sKjWFR?XU1BYN99XJ*3 zDa(iHYG{<0E*x$o+hN1~{Mv5&?6yjo(9@YGKBn|qrMm^1syG45+ts*-u)|8P?5Zkx zVPV|R`|zx+D)|O~ki=k!{ls|pF2J{YMjxlZ6x-A_97Q17RnUtSsm;x=Kr<%V$`MM_ zb&e8_l+iSI@%_!{W@JPK7gtv1;O4#yi#k3x5~j|2r(r_;0eWuG3g_T(5Nr2@z%cYC zCe%T9(oZjWXq8Fl&~80=K3VuO4X1O3{{8N*j;ByC^_QsH__69L#VIGtu3ue6Wkr&* zvXWK5k=OOlH~BM+EiAlvPsV3Grt!oi zh(Ms1V)M_R%1TO z-7}Uc;d8J?>@`eAyMC2+ikf-{|CjFh;#s76>GLZ*FSD-D7b|<_Ki_ycOz>$;@OvHD z3yU}pZpGTIF_aWt5Jv`&25N2MIby+e?cPi}xCP4Os}| zDIF`-KBqN3Is^HMYQmTOXR5`Sw6WeJxtwV>DZNC4@!(M#wS!feh*Se1Ow67#B(2R{ z11bM_>~;!~`RcaB-Yk_D&va+!*_dujxPrpeh&k?Hs=p%Ws&*=}f(7U5`qxt1^{2fv zWk^u7ib~Mdcm(x0<3kPcT$xWnrSUq04*8B!B;Lli{%Fen zJK@o%56?uX701lVpLdkK>S6$`xWP)SNX1~p{&D}*bBY_FM9r5jogfyiI2Slsm-clu1m-}x%11(? z9{KI8ATLEp+L7n`K+DKr-J_IT|G`P8?9I(QLT!e7wSpoaLqDs(l&bsUaP)X^*m&8f z?B_S}v^PYbGgx$!+QL2=og05ipf?^=2#=8q$N2z#v!Gva{Uuijv;$eB^@x7>5x+2Q z`pRJk3UDBdy8II8;*s`&N+cvbMZ8fM^LdO44Qo33xsiFa#zG$pTF*@n3{d=xSA^vK zG&e##nVz&$yPQd)8^xW*SJ15bBhQnR^yNeVE#a`fo*sr3CH_e>KP90w`%c!wThzJBo)KiH%dWKQ6OOi zU-~0q{O{F=e+vz{pW^)^u}m*3`}U8B(udLEkDAdkTk)ptX7Au)7>i~O^o>#~F=79F z1{-qIm;(=NXd(<6dSI+R&$s3Oh#Md9DK(SVqIpB%Gqo2m1Pex_WO_rx3nR8CetLi? z#%Dx_)s|IYO+qD$D)$gh1*Ysoq<9`43GbKOI|?3xnbq4?BXe*lxRi^N9<3})2oERC z{-Y=i6!fUkR3q%Je2IZ^7Zl8q+K*p`LD3&zJWWYXK0#rMJKec+YXa|&`0(bV8eiuz zJF*l$cK$6s6n{GpRaREno1MYHOum&ISW#Yy+%;m@Ne@mI>2_hJu8rZHI7f~*)1>WP zDoZ+2IlQ%F;k`4&mCEBR4h#cbdOZ~p&;YfQHL{M;Xp<>j3B@0Xc7Bdqe#sF(H}&;K z!pp0hGhjMX<3pn=I3*^<_WgG$F|DX4d}%ltZz)RsVBcvGs1z76LWneaY}rUmwm5d= zmv+1_LR%aN=Kp}Pt7DFmrpp3@LX@31HSnT$-nbp=)= zwcnw$yL(xsjc(r;j)UJ@v6GXxfp|=~&fhDxJoha*%t5X8I<%}Ts^846ySw|^!=5a( z{aVP0R0vyOn(IeP+SLV;RxFf692Hjau4&Jn_X;St#4qD9wjD6UKS*1RxX>$NuKTl#BK6CoYP7%d5cKMZ?JYbN^moGP;Q>*3O`vCcx- zZ@~kDiy-l3H=rjZ%sB2&+HNInH8fvh!wHxZWU3r?+9igfqodQ`Z1f-+XJ+c4@JFPx z+WTA5PF&1$dX!RuL9&Uw{3bdbAyxwn_zz)Gm(*rysPS7@0)>}*mwulYC=^;DjjVGP zB=i{K;v^*{Yz&Nfb@wkQ5c_+sIy$?_U+VRORJf7E-l5G)W;VfUJOcKKM5fJ(rP0Pj_^$AStB473E*S$6XJ0!?fPtpN$j?mI z6H`cZqCKa*>zxJ{O&5Aoil2c z(E`-BprD0A1f0WY@Pkwl{`rtc&FxK<^oO=~!&T=0D>grKCIB2c0Nx$Hy2PcV;($)4 zcNbSI`XjOj3I_|+nJd`4v~&uHC8eaKaILRCLv4xYU9K<9DsLqMrv)Cwp3kAIv#U#5 z#sdk!6Hvzrc^Jsbo|r_Y3~dMsU#jG*e@$X;skM5Eh4aQ=0#_J-FtAGMoj10Z$xP0% zjtKr!QFnfOV~hCztfM=`9%rbyMETx3gCzH5$Uo;zV8ZuWnRGQVsRTtlsMNKpoZCQk z>bAd6mKiK7B~|_Y1?iqX-eU+h94L;ETT`eZE_1Vk#&1NSg`c50)(;61xp08{>VJpR zKwf$()z+4g^x6Gny7R+_%Phu=d$+X_VId*AnGepQa_H{EqICQD`wrRrxys6lcwN|5 zS8~@%jW6|$jR%TTdp66vm$e!GNcr*}d#E490uKum&Okl=V#BVevJwj{U=+C8*WNNQidrkYt5`H zP+74H?&t$Oe6onVJj1OE@`bXov*SbTnRR&SnUxvvRaM7kB()knqq~)d`uiiex%>`RP#wg=itw-(a~%B z?P9>9sB1daySxx&J%|ClA38cE)v;5{5<)+j>-VdH`$hd5O?U2NB@dXYUOUZ>wa?AW z&Rk{Pv%clqjm*sd{H-y7&ACP9fkS z&CCwe7#tU8G%Ic9zVR3|xC=u%Ucoo}2f9{)ri_?t>C8Q$+75qW++GyoD;uAhO3CeU zz7EPb7kgq z_-GO|`>`2B1EufjC5=Zk`q!w%maa$)y2>|t>}aVDrnK@-8Qkn6mXci4_xVCF2y95U zOkWQVBwMvtO8@9Svo1Whl3T+D>40xNI+k7-Dz%*^M&^i&jtcs#=pE}6zo7_KcgLvvm_m@?TPdJ9bk@V59& zs?q=8H(FX+%G$f~HI@*$_+^Q^_>y)y8uzPwblF!{4|KZkqG|_ zE^pj8C;zx{^lX@KEI8@fd>-RdQa4!H1<7;=oc`A{Q(VN7JOH$7u$xm?yvlUGI{5zq zba8P7@))OmCEx$>f5n)c9mf_IwM}>9H2DK!Ma9H|{uwTisXhNQ+&@^weBqr^$KNiX zn888Ye-P~dFXok&J_h6Y1M6ja5PvJyT3bXOr7lxWPBH*Uo0ehRWeCK3H_EDYd!n)% zUrsU*Mx*}${{K2R5E=l5{xd(_{*NYZegz(0w3j!UZL;OsB6s(UC6v?q_iByT1SL`y zTL3U4ut+954cRc7Suj$80Tvj{%;dhXeA});dxju-9dZm>=Sf%_ljR@F%kR11`49oi z4I$^Ro(^%j>&yZsA(nTL>_10yAdr2~!-vAz;_^V3Dukq`qX0rK4)Mhx=0uwt$<53Z zaNL$J>AuB54ttD4$c4j8`IT!nX{shT0O*U*BDgYEkBP0Xr-+j&^X*LRcvvjmseY zPTNcV1}=Y}$#h@I&QQ&%B_9n<`0SA5A?fvLwmRT7UhrdD+G8DXL>Un;Pt1rwg~ zd#D)Qdxn_olA1T>O-p`JbeT8m=woOh1wre1_&*z||K0Te;jI2Y9AF=qs_Yc@(89IW zZbmUMGRnAKMQo>-8yXoI8ymUv8jftH!xY;iRKst7M6hE6{o1=K4A&mfumAdNQlRj4 z;n(NuKavY|1W@+h19Nf$1L<5uX^T{x@vNnqZOF_mEs5;b^W5JmE8}I0dOX7pYIJ|y z_iIBaQYqx8#nywfm9aSR`u(xm7QJ4RO-}U==Q-J!T=oHa99R#e6WH9w-&Kaumg+oj ztZ~@->gyW~c-0Y$7#L!-AfRM#_Ql&Wr~k#vvR|3RxL3>_q#P+rr%1fFD)&#w5+k7D7z1Ph9897ne#l8)B>vf0Eyc9@3bDSwd4pe z{;#ZdKXdkb_?&E9j;JNb`{NInq|b6pODA2KyIUt1DJidJYPn96VwsYHYTpr|P~g>= znb9&`P45e(rXttTkAX*pl-J6dOVhtos5eZ`a3g|4t3U&!J3zWHYnS{*R)y*VrclxMYXH#I8sFc0MFrsTOgUeP6hSRdDZwWCF5CD-y6^7 zV(j4X8!+hm=6f@ZM?=%JRJXHL(*D%~P_+UHx7_vn3?+*2Pp$ln*D1c>3QHe_ImNcI zo(kN8u{+6r@pZX>7Cf4->v=r>YOkpw2%l+eWCUe(P=~s{An?it1l489~ zV6<{M1vcED#yU%Z8Tpm4dbe5@PQSVHHP>24<4`i8OTaiAuljFqGx)qjo`24Lsw|7# z6#%se6b_W=iR`RO*KGyb+smlRf7~gTH+C1;e|5|t+J|Ndi*11;iK9RxAsQ@fa)i6b zWqy8bRju2?o&V(0KQ_IDRM)!P3n9jLS~w2wcL`V60zR%v%h}w#ykIEN>WkZ6LFvAD z@Q(3uC7s}p9)(Nosq`XvkYrFJb*nD6eNK4%l>HuTdD`aXJog-c<>N)3e1eC;8$w8@ z-!CCDU)eoCS-FHFJ=vOSyr)w4({$^B!cxwJgI$%wmd5;-2L#MXX`HingDV>cIMbf}89c0zn)LR@rsObi?7Y>|fgxn0f)Bjl3=3r>>={dmTa z=W`{Pv6~OcS&sc zA9GAzrqgh7DyE+vK;X-Eb&+4*TFv;%W^%ogm?By?g4LxupkAdQ=f7U`Vv%e*Z5;BP z>E!vXPghUr9>juBwq`q$$LC-+H|zd?vA7u9C+O6?D3x3yfB!#Fyp)*O_d=6*+al~iJxokYM7zZM8%)M`Jeqw- zX8|z*3l~5BjR|at2}#Uar(0xm71tUs#DVPV_r{Lxn(T)H>mJ4Lz}YtCU}vgCV9~W4 zcXsA-Kh?=1%R)AuGt+0!s&D2f_@P~Mw;grGV zMiTfQ(5v{3oV{6I^#JU8gPZ6)JvnSH8dLziB&-e%BGdQp82TXGRT=Q50VO) znAQ3Jv51saAl|Yg$}5cC5EA-DMK7pV4QKoekH%90@Sj*&MJc~Ur4H9Epu7UHk16lM zs%*VGI{Nrwu7gY`Q%W^B`o$p`&rliiLKKj}_%pTNw&q_sV)x{iQ{9EJyZ=?=#L7qi zduz=1<0=KV6!W$OFur4Z9}D>K@Vtb=AL^Hi z>My_YUKyQx_W%k_F3Re4yS!BbFt=|HwQDf(=`~=b-zX?ZI6n9e_}H8OOJ;bm!K40B z_U$y_xV}O3|A5qokrVxyIzGT@0%wele&_QH0QbN7W)Bame=sx^W$LcU_-%0czt0E@ zlW;Q!-4NdqiHUdC{u9aacE~M|{U42ebwHHe`t5);5=u!sD4=wsfS@2Dpfo7m z-CYAnh=>SC2}n0cGt%AN&CuQ5@!R-)=bm%!J@=mLU(WEx?EUU{?`N&`JZsC!ndE0@ z0}a6guFvl%!G!CCCpEG#0+%E^&K~|p6(3Eeet?l!s@JpOO#U6iE(*O67W6I9S z`450IUF9P3&))eD);s?pK$Hy(2*4bt^-{fUdQ6PuKhGaZPEQy6>m!_yWYjIt;3XCS z4ZF?)&XVM}!hq-#!iF$G;4ARQ+~>A#AJo>|cHwQ+{vS61U)3^o=R=Y|z$rD=_b-g| z56JTmD+2&|pnoGYdk2TVO%Ecyy^;Sx*cTOjzdiE-MMAQ-C8DC=nCrHbUp|YlkqvOK zdU|%Vbx42X4io|cg1e2o2X6t_{z)e$CI%S2=#Qd;-!!|mb+wkj5#2R41+=GtsxL2b zBrs^dl5tCFX=z<&R8*Xo*(^xpV%%2mdjokqI5_xH1{*tYy4o#F^5Sh)TR3k#3J+WG8PA!-kY`UTwF1~nsB3=|`yB(z8ON;5$=t>?8n zNFNqN&>_7|Y)H#Fh?w}0GeA1y`h234E>-ez5aRrmXk1cudXt=y1QhCpZ2X`@GWSVG zt}SJjo_5Q1GY3Jd?Xa1{oGgSba%ZX4YyK3p{!2W%=Doz250u^ zd7mpJdtt5kKv0qUZ0oIzglJZH(p~;Qq+#~)lA`2wx?0Mb`TxJc^e;|Nneic@$|`k~ zb;+~loyQVi-SzQSY%1|^SDr@gI$xb2GjkgBk&x$JiHQ@-3s2~XgIBfek5S(fo)M!2 zGU3&&$;BkWJj~KOn14AhYc{?9z@k?mNiQf`Qc|yQXR&uJkWhT(2-VtK%4pGMdq%lPBSvmCkGC=9`73JKx96BdBn{4T`Lo7#q|0B8$0^5H7aH zKc{kAE_<2i_LvSgp{FvOSNt2L^U->FQqtZp(pvj%^FX}bu8y$^hejX&06>^ZVdr2H zqA z-n2Kub^+-M@{j=6PA5aUFvorha^=Uzqd|^9ig=I{`RoTHB#``43lbS(dEbCLAJH16GV|;6WfgzecVJB|V0d`St_Ksm7-G>7EOqVX@a>!6mODnc&BL0v z_-Qn&U3+v3<0vlP#+`?eso+4lnpzExe`yiizpe@Uk(hn%xrK0gNw7kWzDw^D65?B3 z@5M2Ng+;tUocSfu>+=_k35Ih8xsA*bIbO>&xp&0x-6JJ6$I!pL zenx(e9U1E+xp#%t_W5?a`hNGtbekKp(2U2N&ZA5s(R@w!r4Cex@o9 zjJ!tBzRps7m%#ptA?z!)&|3x~t0_VdKaLb6-GNNiddfU5IypWLZBGTcF^o(M>LqMs zkmWAT^TweJhESnbH>DQvyE5MSc)e4EY8#2D8D zenSqQ`ezR7W2cynqq*a6a+3=Rv=2tO30hYuV}Ra6NFzy>Lhy33Av4;XrxM%;3w26b^5^lw zGb`2v$PLn&OL;Bz7{ASa=8>5bY&cKE`le6I0|7VI;YwgljL~Aw=i}yHx(K+3b`BKY zV1vRjen-p8S+^4N=F6d>aP|~>koq!R0rUCN3I@8zm4%9S-An#TF;>mg%^L&piO!+5 z!DTtC)#_R+bhNwn_SWxQt~W&|?1%%8wkwy{=c(9fY#Uo1m@+?o+DrGA%D`%8w=sOY z{CQFC ze-f$Jb%U+k$W4WpA+P1E+L7Iivz=f*vcy5wzUdqxEOc59*N_nrl;RFKvJbMG7p*r z!=)G#hczPA$rKXDpUS2|2C z)T5gz3$@-j4tg49bauF|<^kQQF(W39%vFQ;cNLhO7}AB6+rMEtx}qUm7iYd+>xoMR zwN4N(nfw|bkxxioDLw~<3!aas%fO@AQxf9~Zp9|$3hy>F<1s{JR{H827$HNBsP1^> z=1Qc=6OoYEJo^b?rxa$LFW#vzKEGlX8ON(txOw(?)95C3Obv!pWbVyrI4~m!B zD2tJS(fO(4Ko_uKi;RnSbJe|MEA#*lQWkChHcRpN2s@kYswXZMx#s2I>3dbxfY-;t z-*?OmXRYnEZju`N!9SGgp)LX5((gJbKcv%RV_WNeE;>7=&p#TAwdAby-oVcGoBQ?9 zXC_>4szb1*PqIO!Z=P1|M1~AL5#D;viVy3|WT#Q@v_+0yBrYbUrM~`M2(Oiks>aHwf}dL*knL{(HuTX}1!RZT@pccws0lrfJ4+1}nlnw8s!F+kO~H;z9x z>jEnysitwr?9%u~e(k$=am3FJ4Bvau=V3=bMf zJBPaq2{0vpC{_j)YvZM!{*t-b9UNJfUh-KekSu8sO=oFZaSokSAQ0_Ifv%?{~N|pH}qE}UxutTRuXvfEU32Yv! zO2{{FhBSo9_<0?i4#o;>91NFpeyWczF|HW(yvKkzZ9hhU$%-9@1fM+TV1_pcDcf}M zC&>!&@`^t$y7@taiVQs}H+HO_7zg)1N_d!5fPmH{DGZI@@|<#SRtKK zB~>B&>Oy)jMA9_ES&EJ_5J6LUvhI-!MrAN zCuQtMt|Ey5FDp7E^ONX-Y?>?L({5Q6L;RxYU^-&PbH*k7@+D_lS=q!2PG+3%LhDVR zwA>RyNMDsp4)A9{$Gc9a69h>DOPwS{$KSGPYaiasH*+$_l&13;LSDj*%`HU1TS@

    StS-Dedlpo~wOwkdVvMq?{anuoj)R zUSoBlLsV2w=I7^Ydz#GRlYbAR&%l?v)etpy#UR3Wb)IX`f+WK9CDe5dRpEVR=c4KxE(u#i5arH zQ%uh@@_R^dPs`}t_ITynCqq`#zak=+{QX5AKUR;~Z~QTPog^mK5)hyg&amX|!kDe3 z?}O%8Q+?+f#fy-rsEzGuL0;aKw$K@sJoTNWakG>7T)XABl=u05w(D?8_Osjf-Smh25<99 z+XdX8XcyJe;>GVn{Q_>Z`br659`27*e%_SKui{YFz1P_Hh(h}17{q~Qv;PO zs&IQsPS^WzRyAhikJi2Mx%acm0oioW3i0mb5tu~ zbKUsuU}L7dd*-dotf6-0^sudE!f~L9G?u5exrG_{+6=VjCq$}jZ4KuD6QL+A$Gn-{ z;%bc)!*10Y=RI{dm$$JcpM@L_8=5)6n;AyeW89z24f#s?qJL^?ezyJ!gihS-?6(VI zbk=jo>iT-#`x?uImLG$jR6?$sUTJ9#rFkN&|FB))6As76V*C+ zaJ)~-ZR>q(S2i;8+aN8?Y&3WN0QKX?JLo8QS=*8Mg)zbAunqHXOlot}Df=(lB%ek| znzp{nRnDk&eeT{=2L|)d$*NH>pWXwwwh}0=&Sv93*DsQnvP`&lLT;s2?3rK5^O_|p zn2H=!DvehtkyBFk0682K2vbn}-1r3v3X0Xmqdwb0tep${ASwk9%<90~g@%)NdAiy5 zd2c8hUr`f5>sznwzJR#siarHT7%ZWma43y#O3E78yDRQd2sDlI?jqKt(bh(7Jb1#O6n~JKi z+5ioO>sF?9Jpy4D*NlgZZ#6@5yclro{H8Y&E^eOjJ(EwFPbMRHP=wQMPX=G!u+4e# zQw24F4!WwacXARO5Ma`mh=lM<9A$&*gG3k(9-h2nB-DO%o!ch&W`!95BT!OOQhSw; z({;WfwA|cEobyyPNXJFs&y#<8BgEizcMh0s{(Xoj6R`sCU18t4EBqp3v>P$sKV5oU z)W%`lmsnE5Rgv@3N?W*1US0ibE}vXMlxSo_gD_RFIAVb$6mr|gry8P>iHQ%V@Ss?9 zzD!tt$Z8JB+N^<0&Vs&NfBIjO_=C5PkByM;lOOm+;gONLIqwKJ4MJ&IS!EvL&z|Ai z{&`%rIc>oFgyIs0AqSdP4!hvF7W92G)=AUpgN{eWZR)GX^8L>3{cgYdLc|FCdk{*A zytl_X@9E0P-TV6!DX-XQDux%-RlVFLs5e?Eb>lu-+l{TW!`{|XuJny>JaL7(ie_CLC9 z_HohYzG5R8T>nOW4k9enz-IZJ;k#%)y(>H8#s1p^HTKR%ZZsGu-T)HqD?Fp<0Tzv` zIT+QSs+nkjZ&P{0DIE?O*Y43#T0YGo8J#Z%NO~)U!-oYHt)C?&C6y@qYBu`f*LQSe zOGUAO>$(~P6V{a#aVI$pHYiFz*CPsC`p1ubw{82(e&EZ$S5x!6IvQn&00q)ifEGD1sxmh}aqobEm}BTwQxSaj6v z5IRFju~=nU#PL2Bp?bVr?XK*-cl!N##VW69ny32)V+FR0Pq=TU(z;vJLCrpUN+tg5 zVligFFY;~9n$k|t{Lt?q^TG!4sb6#aOLs*Qb6USw(Jbx1mA@|^lEU@7VeaQWh}Xxb z8EjGEK3pi2TM;CoMRNGn;aJ=S63@L}OM=w@=>PX5@Ne@(EGs)SFsh66s1_RIe?oo9 z9Mvgg8_f`5dJxI|s{X~rsgsOES*q+4;YQ?=Y|=!koDnMJ;-Om!=U1TO;iyx zt3SB=Nj9?=ME;`H@NENERSDw+;9nD!&^EWaf98#~Fa`UFQSrMJS9W3U8#%uy=hA{2 zp5*)yqlG%+0v%^W`U83CRTxt_?h~RfPM%-4mc|D=i|OAI-Ak?U5cyfVtHLW&`s>#y z@BPz@9e%J@nZ|1nRU2!6W`$f@>-<*#(5q7jAguK=t6A@S;%Sv z$sI4x#M=vm?TKf4-qtFt7K*qXp1W#)-ua$pM|;2USXZEWQlrNG`aH-NoCwGT&xp|J z?!nyHj7g9Vm`Qqh?s&B!eL3Ya=z6sMfN;w0;#<(rAzo$Q@2=OFpGT1^*; zW&N6#f!x2(0^@goDU*OJQoH3jJL_Sp3*1@Fr@nZ3Ojlp=1)tA^a2m;&EcbSGy)QG4 z^h_M(eKC1sC4D+>m!V~l>W_zw>{Wh{?r>nQR%MS1VbbLDcg4i=^FD7?$oS@mhrPc& z$IjN|w364&5P=1u5^@Rib>)2eb7v>C|Mtma319mQnAIwIfz6cb-IO? z(^Vta8$CDVM=w3m%=;3*^?pF_O ztVv))^cU`3q7DWpJVSxp4{(-V>?7yB4}gB`;ni0$9LnZ-CIY=d(m@hPT|rWcvbYr% z&KWTT`3{<^+l8?*A+@J{|Mzj=pkmnlb$z1Qt#brkd19Ux|Gjg`Zbb%ro#cMCtq&+D z^v)zLFMkdx0PKpq)V(;->|P8@zdr>tkE6X8YAXgBezSXLZe1 zT#c-!l>GL?ciAQP)`p#|+t?u1_SS-qm%&ZsDlY{1ir)X?wwdI%ORtrzwBEZ*G?5*$ zB(Y@clJO2vUF~W@r&;E_yj`v4=0pupZ3>?hsQdwNYGW{G=UW`Povpqa&lX9*CQ_n! z@aQ8WetvEwNN8J)ffPwMpofZTFuxf8_?_{Z`TW#H|p}V1a z8TLEJIFP&4f{u%sef&;gd`|1}|71QCuZZqt*3h9qD(v?rDl9Z!y{b>^G|CEYDAYr* z7I2Ffy2k_t!MZ_RAdS(ViC;se6M7siGP3=!@% z5J(=aCSd;FHK(kDC9-*b6RF_n#`!(`QsCdi>7f_!cU+yEH6fLk19 zm-6?;<>lbW8zBpjuO%q5Q7~_SOSQ^S>>uy%ZBahc)t4|e?eGgUs;Zto#CH~`&SnLU zF_4$e?_aOHLv+gU^6*^wMu0dzO%f9#pY>RGNSjJ#XHw^;`1fx_kwJH0$%>K|u221Y z3edyLV-6cBo>vy5!3ryION1C**pDAS^3-ecE`Kud#7gQ-@@OfQP5?Rq!p7x(@RS7W z2$9^%B_@Hioy(Csx=NwT65n1vM1ho7rB-N_aNC`>?QleMu^7hs`O!jt z#HuMXf=vcofN>`=g@FEuOqgL$Oqu754WI<*#ev0QT8^5H{@uS2yjp5`G%7?EF#gff z+3oEYz!5mn16u8RtU*Kx=QGSo@hMKnR;e8ty`qe0cc#>#m0mnUX3|*pQfSTBv}2 z1b(mq0a4rvJqb5qolAVzPIV$uI{(D)bLd*w(d#zk$!o?(N7%_!j$7-LrH*OKPn4Co ztomb`b(`}(Oy`JGyJS@bYt;f$_KM4`<40Q(?9y9N(%>>HWqMlh)s^XzBPt7vCHl>& z0hoWfV@m<9>Vlg^DKRJkr-_drL1E>>0zGbX?gvtK8hP=I{k{0kj*bTZqm7B?%B1SX zB#~OIQ)Dx{IfYA#quX^)22Efx4aQW`Owm)v6=CNQ~E+Xu&Rqm<-kaL~a$Ceg4 zUdzY3&aS|qm5aBu;CIp460pOeVg=kbo3^&!v61m?;GV*9{f)--DIdw>2RaqflJBd$jHu>9P!sJ{LP`KcUW+{_&{sksk zH3fo$)BsIv0$Cj@oJ{uX=QEEh<7RZ+oY2?N8O~OURn4xo%h=Wt`E+BmI_igQ6#leh zUbhr1Bj`6{GDi#i4x5hqi#p6JT!B(wG&H7eE;n8ut~Z-g#%22TtFx$xiyMu9e>O9X zWYp7?uTB(U&sZ3&h3~NX;c-#$nM)vm`FV&~u#-SU=vhZ|bI|&|?w_P-8LhWJisKq% zIDjEv9B&45Ap=n!Tf%0#e6*qRNbL0|f7mQlAi%d}H?unt{c`QW)bC=RrHBTOBpAmI zbIXhU?s0|Qm}x~?y}na{{pzsN?Ny6zmVmqTDhtXwj5Vi+kdW{$nfe)wS6u9C#<%m) z?_jyZCh7ilm^C)6wzWw%GM#;oE;So?Ci48szZ_Wx2vb!99vC=zp=@Djs+p4`_9G-) zmm^v^a-`%}Oy|-7+*EPrWawVD#XnHszXgE*Cs2egWfT0y715TEWdE$m#It+KRFUHx z6wcE$_C4&pKsN31yuADCe3jlWINc04{qGTvXrYk4lDO+mdOv`rjMJYeL_|Izf2N{$ z8Kf#ID_nucR!ha&(|*m>=$q`i-W9JBcjs+1lNix*E_Ui=ONcz3%9h%7LwT;COdAi! zC~X#(V&xZX!Xl=#t>S??cAjM#Z!r{lMre2?iQPa##1o=*fj1U79KyeTQbgCtCbZKS z2*Gi=f7tq_k)6&wSo``(*#)L|x zhy|^(AXTFh2uFgd{eBH(SM0x#w|*fU{x>B09vS(mwdnS6?7-#*Uwm*dahiNUH50%~ zx0qa*7>MJW2KR2i4|whUFW-ksymrD|Wd;xtC=LVva&a4L>%>ZwLt<%+5AlL9$-fS| zh4mq*;&*6p0Oou~hHvs$=nuvI79|Rh+6U_@CeZFST}#V%e_s_O<^9`FE-r37_|w9r zvx60YRd2y5FyYrB7bXPfQwe!|wfKWX>oEH5!;RaMn93b5XU zNIVu!k`)*Cuh)J+O1d=x54=Tu<`vj4ArSCp1BQHPh9mkbSsm{!MytRFP;+*DJzGAj zL*`z<>yIBl9xDC8cTg^_p^gs8gOFxjutCC}vw&)#uoos&G>qN zPzP@({o?qLIfMf{F?`#0wSHI+>n`T``|q}bkB@ZZ2F8DW4cBEv*G!Ur<@EeB4?|i- zs>FCjanc2Ei~T5a>f^<=IB&HTJASku2S*O8d8qyQsS;K5e%BZwY4L>SY*tmLGlSY2d$Z6QuILsnxdceBwY=m{80W9p+Q-a) zzkH})EI7WbN?Cl^gGj|B@K`)R19g6o2>Zmz)C<+tXjGa+_H3M z%=)JO1;iLpu>~UZpK&pex-5lO#~nmWe7vEq?!+X$HVA0x!p=_HQ!r#X;wjgX}E5582E4cS>=7j&7I77=*_+1;3gzgbuY0kurL00$e}=3bE1Sjatd zo(uiUP+F-KH6vExw?9otxJ3Ug$x#o_sQ^04GuB{%TE{Z zuzMNbmY0{u#SgecgjNkmF53 za|08Nl+rk=BP*dr5c2NO2X*!Kl5^V%gXoR{aKf^Xm+qJSqEd$Z&$*+yIkU}3c9_*h zx2KCE&Z7XjS{)7)cjt9%pMUTzZ+pPt0K~!ZpoiyAIKKni){(p+CV?YSNdR`1q(+@x zjwh;(HUQm>R5#rGLihFJ#d>*k_$koej~#>PWMYUY2whJs2b$Vb0DC&kTicd5H}zZ! zscv%82;e`EZYcJt$LoQRUinx_3Wp#VgY4r@HezvEDg!z;$ zIP1r-8kqMWrzcx7@a5RwO>pxy4!d(?$m&!Uu!x)nrF$2sxXIUMLv5H;RFp^$%RE;B zbx=!vqu2NempSpM8$=egnj=hApmXyysvwCVLI4t#_%|vHJZcji9l$Ebo(vQGUxZ?{ zv;*>0a(6@xCuD4rJGsUQDG@w+B0hot2%Rh~42mr}^M`(d4K#38R~*jv%hTV+sF>X; z8r%H9nat&bY?fa;fb%`}QJ|0mmwDT6(%d7YL@A?Q@YVx~K3~1?o2rD@jThAm_n9jF zJ0d`yEJ~9ySPIFDr+rOxFC#tu?v3SLBD|oI5=9_Lzud*|CUiPE2B1dLEm{&5Z~wev z$exlC7qWVJvI7v;(C9%WD78IbE%X7Y=f6sfMBCe(k1o$@S+#0_T(5n$_TfV`^@;Gu zR@fEh?}7qkAalpMo-Uf~gOmvB7Zoy;kG@ON4r|x~!Jca`Q~4?eXMcTI3bJ}SqXO}% zw{|*scC}*D{nKKiB*#*zDccCMy|?!c#Evr%YY-a?8&cP|+xA+j6Ld9 zK6}%9At=aD2q6iH$(hr?X{e`zN3`~1e2_%>r$`UiMvk%uX+SjT@q;5azxBqXhcKdI zLTF(`KfmJ0j7LI82MO{ZVsZiuL{U*uKR>@Akr&e|oX0O;NMQtLphM(8G_$^A;9OF> za|>nObc0_2soRX>iWG+#FYez5Ai0GxyCROC_k-G`81vV3N7W9`5wmnvaP80PEa+ZU zRq6@~oa;}s6dMeS;S{;)u=l^oa*W*8mzRk_*MixAfiPd3MJZ<(P@hdrLjw&Ag;P|` zX9u3PqRD48 zzJCT(^|0^vSv3SnsEFkdu|q#!IDrNzsziZ=&V*9SnFplo+Xz}N^TjrO4wb7a^mvoDp^PsriLq3LzT_E#uvdoyvoQ1Nm#1 zWSpI~y}A2kQxX6ZAYxx%RdldcLGwwfzLw3zw^TE0Y)FWVV7cNOV==t-e z23}%0yxi2@$fzO;H=hHU6P{NND>5Z7)`h{Ng@B1}(YBo5(OkXKG6LYQVmk$LwR@1m z)j2g9*0ZBh1SmS^mZqjup6jb1kFL9$Ni2v$%VSilv17n>J$#(s z(Lz$9Y!M_eW$BfQVyBc^s*#i$q9(8!4-deHFYw%9saF2iG7R@xn^*|2j$m%+Xy|@< zU%yp2T!@jYoXpJxf#J^YvTqGIgJA+>$i#-{3|cgs2U!B^t!xa0=}*VFa@fjn8wbxB zhN@~9743|W!}-{-Z{Q2J)8TyIJ^@QBRa+a~sv-WaPc39zfXqBvTe_*0BUokeB`mQD z@ee2$bxG4WkM37`S#2(|?&t08{mKW%2s71hat443Ub_D8sD)u$8M?&=g$v8bST&x% zh!9L;<;omZ0((7FTMZkgAI_L3PS^WZ*(~;z%fO$|g}oUa7FTCEQOG?funalRiKpBW z&HVO9+xhPi&ZlKxwjU30}%E~8ss=<^J$gAtk- zPd$nDdr{X#`=S{TY?#8g8uo5Al>VGliKj5yo0aTeHflO{!7+GB2U+-`_>dmsIN18x*#>Rabf1_CHzlg2K7K8dCeIlS=YROT{qpKc4`e`b8NYOb zu5zEBh%lUfU-*deT3r~Q=2lk%+bSmi0|mTr6Z<=;2Lb!=aonIR{8o)Q-P0TR#^Nyt zpp1>9V++rpkp3|}z209Ty|lD6(1LESbw2=Q@>an43y{#$)6)^m5PHwXzKU!OrXC%< zxPs;V4JQK2pHHLsGXZGuzrlyBk)U`vN}#Z%fx4Z)@zdPFnSGSk^et4<(SiPjr2e{v zz#sYO!>&8VKXD4$>pwV!>*4>xXB4b~Krkzsb^efgt6AP68*?6Vw-2{4Of*RrXll0@ zWi~Sv{25>$TH4zqK%Lazw+G&hX@B<+phXL#L~n5nAl|KI;{Kdh_y-Yrw-!k>)}8pE z00EaM9q>s71qE$w5-|+I`2hN$I60kQX(`c!K6&!wbIq4qL5arK-=F-!gYbj_xS0wg zBjfq)k;0#6=jQs$Z1lQCLL~?(D5T9qSGlDzo;EZ#`UyjioWQmqz#@M&4+DwL_aPM( z71*snA!nE-6u3TL+ei7_>RFGOn7;O|=-m+}6qryWkn}sBsm0b3U}<;PlvhuD@>G$5 zsL4X?5w@j`4ry>N54;~x>qU~cEY zT)_|Xc~w2QYHkQ4k!@PC^oNHm4EQ9ZKWxXPV{@IXKbv zK4J!M+nOe)zGQKU&k2umIOhFr#gbV0^Bf6;=_h%_QzwyXA6wM1j z-H5F>bjBP5s;eN=T-jlJ_5DfZgyHGpMVCBD%_+|+9E4v8F2O$13CjwiQdu7{^mSWU zu)6A(Fubb33MIaL^i$cDRPt1L$U*N>FDDfw(W=L2bsw)OZBPTFpAEWmv~SWM7kCyK zB-HCHd)R$eH&AUY3q-QT246)W*SGF;6`2^kae~5o#(H*}^<2)2X)3UxgmkI4yze?u z(%p$&jGmkNB<0Te`2Y>`;Sk9lW+n?<6}II&G*$m&TX-E~=5vlL+w~Q>RihfFv;^(8 zdKWu z^=n|*utA9;I+VWKm)DhPYO3tB-(@~5NI7ERL7?KVEDh2q+^dZwW{F6n7Q7dC$tCBX zYo>=NiyF;DmSZ@kO;<{VA&qMWxMaIXO+Ogn(+duSyJ^^E^TB z5oh79$Cpb8`48u%NsN~;y6LNn{MsIiv~+qzz|2vnH0)zQZEtGJJE}F(V6zk+i1s=g zZN8+L$=ngi?bVl`J}_6dU^+WIV@9soYtt!YA|{7S{N9E?tC)e4`(L`rpg=eYx8K0L&53V>3OUp5I+U|2?)jHUh#q<*g;pEE8?;zLJ_U;f8FWH~FJ_Piv zHisE)HKcC2z`<qmQn9?6De+k01|9Rr<{}JW@H}SIX6%$S)oXVZxN1mNx8uApC2ZtH_VP}yc%~pnr?Pvr zcOZLjJ4+=sqgOu1bjdkX;n7L>5I?Y>>5~qPA~w4rF_WT4^NF@!(YF{<_?WEtl&i&8 z(N`^%MvC9^x&O)Zc1 za(aKxpxynHs}zPyMNLK0S$p!BC)RGm@-l^flz0E1qwgf-<1?8l0DqObO}NxyCr=9%;!3QxD*i8a@X0Z zLNymnmh(ZxCgSY)^P%;@jUthtv%SlSm5|bTcvfNV`s&?DS0x#l`qAuE541r1c(U2fDF+j`|Nqz@07 zPx;h&=JZTKC;6$%J!I)2A$huFfSc>5fxI{H-L%ek=(VzH`$B3%7fD)lZ{QHa?CyIe z%6ZoV0VI0*;`dpy(cFEUY&yI)3xQ-C>_3`@-#n9UYh_?mB3gRdx!4qx(y0G2KBRkT vbj0d@E&eSR46VQm!Us{=f65rTK@a@=y?Ek`%noc^4tXQ`?scB9w)g)4EPaK& diff --git a/backend/app.py b/backend/app.py index 115c02c9..08d38442 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2459,100 +2459,6 @@ def generate_summary(): except Exception as e: logging.error(f"Failed to clean up: {e}") -from datetime import datetime -from pathlib import Path - -from curation_report_generator import graph -from financial_doc_processor import markdown_to_html, BlobStorageManager -from financial_agent_utils.curation_report_utils import ( - REPORT_TOPIC_PROMPT_DICT, - InvalidReportTypeError, - ReportGenerationError, - StorageError -) -from financial_agent_utils.curation_report_config import ( - WEEKLY_CURATION_REPORT, - ALLOWED_CURATION_REPORTS, - NUM_OF_QUERIES -) - - -@app.route('/api/reports/generate/curation', methods=['POST']) -def generate_report(): - try: - data = request.get_json() - report_topic_rqst = data['report_topic'] # Will raise KeyError if missing - - # Validate report type - if report_topic_rqst not in ALLOWED_CURATION_REPORTS: - raise InvalidReportTypeError(f"Invalid report type. Please choose from: {ALLOWED_CURATION_REPORTS}") - - # Get report configuration - report_topic_prompt = REPORT_TOPIC_PROMPT_DICT[report_topic_rqst] - search_days = 10 if report_topic_rqst in WEEKLY_CURATION_REPORT else 30 - - # Generate report - logger.info(f"Generating report for {report_topic_rqst}") - report = graph.invoke({ - "topic": report_topic_prompt, # this is the prompt to to trigger the agent - "report_type": report_topic_rqst, # this is user request - "number_of_queries": NUM_OF_QUERIES, - "search_mode": "news", - "search_days": search_days - }) - - # Generate file path - current_date = datetime.now() - week_of_month = (current_date.day - 1) // 7 + 1 - if report_topic_rqst in WEEKLY_CURATION_REPORT: - file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}/Week_{week_of_month}.html") - else: - file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}.html") - - file_path.parent.mkdir(parents=True, exist_ok=True) - - # Convert and save report - logger.info("Converting markdown to html") - markdown_to_html(report['final_report'], str(file_path)) - - # Upload to blob storage - logger.info("Uploading to blob storage") - blob_storage_manager = BlobStorageManager() - upload_result = blob_storage_manager.upload_to_blob( - file_path=str(file_path), - blob_folder=f"Reports/Curation_Reports/{report_topic_rqst}" - ) - - # Cleanup files - logger.info("Cleaning up local files") - try: - # Use shutil.rmtree to recursively remove directory and all contents - import shutil - if file_path.exists(): - shutil.rmtree(file_path.parent, ignore_errors=True) - logger.info(f"Successfully removed directory: {file_path.parent}") - except Exception as e: - logger.warning(f"Error while cleaning up directory {file_path.parent}: {str(e)}") - # Continue execution even if cleanup fails - pass - - return jsonify({ - 'status': 'success', - 'message': f'Report generated for {report_topic_rqst}', - 'report_url': upload_result['blob_url'] - }) - - except KeyError: - logger.error("Missing report_topic in request") - return jsonify({'error': 'report_topic is required'}), 400 - - except InvalidReportTypeError as e: - logger.error(f"Invalid report topic: {str(e)}") - return jsonify({'error': str(e)}), 400 - - except Exception as e: - logger.error(f"Unexpected error during report generation: {str(e)}", exc_info=True) - return jsonify({'error': 'An unexpected error occurred while generating the report'}), 500 if __name__ == "__main__": diff --git a/backend/curation_report_generator.py b/backend/curation_report_generator.py deleted file mode 100644 index 39e0761a..00000000 --- a/backend/curation_report_generator.py +++ /dev/null @@ -1,498 +0,0 @@ -# library -import os -from dotenv import load_dotenv -import requests -import markdown2 -import json -import operator -from datetime import datetime -from typing import Annotated, List, Optional, Literal -from typing_extensions import TypedDict -from pydantic import BaseModel, Field -from pathlib import Path -from IPython.display import Image, display, Markdown - -from langgraph.constants import Send -from langgraph.graph import START, END, StateGraph -from langchain_core.messages import HumanMessage, SystemMessage -from datetime import datetime -from financial_doc_processor import BlobStorageManager -from importlib import import_module -from llm_config import LLMManager, LLMConfig -from financial_agent_utils.curation_report_config import WEEKLY_CURATION_REPORT - -from prompts.curation_reports.general import ( - report_planner_query_writer_instructions, - report_planner_instructions -) - -from financial_agent_utils.curation_report_tools.web_search import CustomSearchClient - -load_dotenv() - -import logging - - -logging.basicConfig( - level = logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -#################################### -# LLM and Tools -#################################### - -llm_manager = LLMManager() -llm_writing = llm_manager.get_client(client_type='gpt4o', use_langchain=True) - -web_search_tool = CustomSearchClient() -# query (str): Search query -# max_results (int): Maximum number of results to return -# search_mode (str): Search topic (e.g., "news") -# search_days (int): Number of days to search for news -# **kwargs: Additional parameters to pass to the search endpoint (e.g., include_domains) - -MAX_RESULTS = 3 # for web search query results -REPORT_TYPES = Literal["Ecommerce", "Monthly_Economics", "Weekly_Economics"] - -# get the right system prompt for the report - -class ReportPrompts: - def __init__(self, report_type: str): - try: - # Dynamically import the prompt module based on report type - module_name = report_type.lower() - prompt_module = import_module(f"prompts.curation_reports.{module_name}") - - # Get all prompts from the module - self.report_structure = getattr(prompt_module, 'report_structure') - self.final_section_writer_instructions = getattr(prompt_module, 'final_section_writer_instructions') - self.query_writer_instructions = getattr(prompt_module, 'query_writer_instructions') - self.section_writer_instructions = getattr(prompt_module, 'section_writer_instructions') - except (ImportError, AttributeError) as e: - logger.error(f"Failed to load prompts for report type {report_type}: {str(e)}") - raise ValueError(f"Invalid report type or missing prompts: {report_type}") - -#################################### -# State Definitions -#################################### - -class Section(BaseModel): - name: str = Field( - description="Name for this section of the report.", - ) - description: str = Field( - description="Brief overview of the main topics and concepts to be covered in this section.", - ) - research: bool = Field( - description="Whether to perform web research for this section of the report." - ) - content: str = Field( - description="The content of the section." - ) - -class Sections(BaseModel): - sections: List[Section] = Field( - description="Sections of the report.", - ) - -class SearchQuery(BaseModel): - search_query: str = Field(None, description="Query for web search.") - -class Queries(BaseModel): - queries: List[SearchQuery] = Field( - description="List of search queries.", - ) - -class ReportState(TypedDict): - topic: str # Report topic - search_mode: Literal["general", "news"] # Search topic type - report_type: REPORT_TYPES # Report type - number_of_queries: int # Number web search queries to perform per section - sections: list[Section] # List of report sections - completed_sections: Annotated[list, operator.add] # Send() API key - search_days: Optional[int] # Only applicable for news topic - report_sections_from_research: str # String of any completed sections from research to write final sections - final_report: str # Final report - -class ReportStateOutput(TypedDict): - final_report: str # Final report - -class SectionState(TypedDict): - search_mode: Literal["general", "news"] # Search topic type - report_type: REPORT_TYPES # Report type - search_days: Optional[int] # Only applicable for news topic - number_of_queries: int # Number web search queries to perform per section - section: Section # Report section - search_queries: list[SearchQuery] # List of search queries - source_str: str # String of formatted source content from web search - report_sections_from_research: str # String of any completed sections from research to write final sections - completed_sections: list[Section] # Final key we duplicate in outer state for Send() API - -class SectionOutputState(TypedDict): - completed_sections: list[Section] # Final key we duplicate in outer state for Send() API - - -#################################### -# Research Planning -#################################### - -def generate_report_plan(state: ReportState): - logger.info(f"Starting report plan generation for topic: {state['topic']}") - - # Inputs - topic = state["topic"] - report_type = state["report_type"] - number_of_queries = state["number_of_queries"] - search_mode = state["search_mode"] - search_days = state["search_days"] - - # get the right system prompt for the report - report_prompts = ReportPrompts(report_type) - report_structure = report_prompts.report_structure - - # Generate search query - structured_llm = llm_writing.with_structured_output(Queries) - - # Format system instructions - system_instructions_query = report_planner_query_writer_instructions.format(topic=topic, - report_organization=report_structure, - number_of_queries=number_of_queries, - today_date = datetime.now().strftime("%B %Y")) - - # Generate queries - results = structured_llm.invoke([SystemMessage(content=system_instructions_query)]+[HumanMessage(content="Generate search queries that will help with planning the sections of the report.")]) - logger.info(f"Generated {len(results.queries)} search queries to conduct web search") - - # Web search - query_list = [query.search_query for query in results.queries] - - ################################################## - # At this point, we have successfully generated a - # list of search queries ready for web search execution. - ################################################## - - search_tasks = [] - logger.info(f"Conducting web search to design sections") - for query in query_list: - try: - result = web_search_tool.search(query=query, - search_mode=search_mode, - max_results=MAX_RESULTS, - search_days=search_days) - search_tasks.append(result) - except Exception as e: - logger.warning(f"Search failed for query '{query}': {str(e)}") - # Add empty/default search result - search_tasks.append({ - "query": query, - "results": [] - }) - - # Only proceed with formatting if we have any results - if search_tasks: - search_tasks_str = web_search_tool.format_results_for_llm(results=search_tasks) - else: - search_tasks_str = "No search results found. Proceeding with report generation based on general knowledge." - - ################################################## - # At this point, we have successfully conducted X web searches (max_results) on X queries (number of queries). - ################################################## - - # Format system instructions - system_instructions_sections = report_planner_instructions.format(topic=topic, - report_organization=report_structure, - context=search_tasks_str) - - # Generate sections - structured_llm = llm_writing.with_structured_output(Sections) - - logger.info(f"Generating section plan for the report") - report_sections = structured_llm.invoke([SystemMessage(content=system_instructions_sections)]+[HumanMessage(content="Generate the sections of the report. Your response must include a 'sections' field containing a list of sections. Each section must have: name, description, plan, research, and content fields.")]) - - ################################################## - # Now, we have parsed web search results and topic subject matter to sections (intro, subject 1-2 in body, conclusion). - ################################################## - - logger.info(f"Generated a report plan with {len(report_sections.sections)} sections") - return {"sections": report_sections.sections} - -#################################### -# Section writing -#################################### - -def generate_queries(state: SectionState): - """ Generate search queries for a section """ - - # Get state - number_of_queries = state["number_of_queries"] - section = state["section"] - report_type = state["report_type"] - - # Generate queries - structured_llm = llm_writing.with_structured_output(Queries) - - # Format system instructions - report_prompts = ReportPrompts(report_type) - system_instructions = report_prompts.query_writer_instructions.format(section_topic=section.description, number_of_queries=number_of_queries) - - # Generate queries - logger.info(f"Generating {number_of_queries} search queries for section {section.name}") - queries = structured_llm.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate search queries on the provided topic.")]) - - ################################################## - # At this point, we have successfully generated queries ready for web search execution. - ################################################## - - logger.info(f"Search queries generated for section: {state['section'].name}") - return {"search_queries": queries.queries} - -def search_web(state: SectionState): - logger.info(f"Starting web search for section: {state['section'].name}") - - """ Search the web for each query, then return a list of raw sources and a formatted string of sources.""" - - # Get state - search_queries = state["search_queries"] - search_mode = state["search_mode"] - search_days = state["search_days"] - - # Web search - query_list = [query.search_query for query in search_queries] - - search_tasks = [] - - ################################################## - # Here, for each search query, we conduct X web searches (max_results) and return X sources. - ################################################## - - - for query in query_list: - search_tasks.append(web_search_tool.search(query=query, - search_mode = search_mode, - max_results= MAX_RESULTS, - search_days= search_days)) - logger.info(f"Returning {MAX_RESULTS} sources for query: {query}") - - # convert search_tasks to a string - search_tasks_str = web_search_tool.format_results_for_llm(results=search_tasks) - - ################################################## - # total searches conducted = number of search queries * max_results - # all converted to a string before saved to state - ################################################## - - logger.info(f"Completed web search for section {state['section'].name}") - return {"source_str": search_tasks_str} - -def write_section(state: SectionState): - logger.info(f"Writing content for section: {state['section'].name}") - - """ Write a section of the report """ - - # Get state - section = state["section"] - source_str = state["source_str"] - report_type = state["report_type"] - - # Format system instructions - report_prompts = ReportPrompts(report_type) - system_instructions = report_prompts.section_writer_instructions.format(section_title=section.name, - section_topic=section.description, - context=source_str) - - # Generate section - logger.info(f"Generating section content for section {state['section'].name}") - section_content = llm_writing.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate a report section based on the provided sources.")]) - - ################################################## - # Here, we have successfully generated a section of the report. - ################################################## - - - ################################################## - # IMPORTANT: here, it is saving section content directly to state['section'] object - ################################################## - logger.info(f"Saving section content to: {section.name} section object") - section.content = section_content.content - - ################################################## - # content was empty when report plan was generated, now it is added - # research-required sections have content generated and added to completed_sections state - ################################################## - - # Write the updated section to completed sections - logger.info(f"Completed writing content for section: {state['section'].name}") - return {"completed_sections": [section]} - -# Add nodes and edges -section_builder = StateGraph(SectionState, output=SectionOutputState) -section_builder.add_node("generate_queries", generate_queries) -section_builder.add_node("search_web", search_web) -section_builder.add_node("write_section", write_section) - -section_builder.add_edge(START, "generate_queries") -section_builder.add_edge("generate_queries", "search_web") -section_builder.add_edge("search_web", "write_section") -section_builder.add_edge("write_section", END) - -# Compile -logger.info(f"Compiling section builder graph") -section_builder_graph = section_builder.compile() - -# View -# display(Image(section_builder_graph.get_graph(xray=1).draw_mermaid_png())) - -#################################### -# End to end report generation -#################################### - - -def initiate_section_writing(state: ReportState): - """ This is the "map" step when we kick off web research for some sections of the report """ - - # Kick off section writing in parallel via Send() API for any sections that require research - logger.info(f"Kicking off section writing for {len(state['sections'])} research-required sections") - return [ - Send("build_section_with_web_research", {"section": s, - "number_of_queries": state["number_of_queries"], - "search_mode": state["search_mode"], - "search_days": state["search_days"], - "report_type": state["report_type"]}) - for s in state["sections"] - if s.research - ] - -def write_final_sections(state: SectionState): - """ Write final sections of the report, which do not require web search and use the completed sections as context """ - - logger.info(f"Writing final/non-research section: {state['section'].name}") - - # Get state - section = state["section"] - completed_report_sections = state["report_sections_from_research"] - report_type = state["report_type"] - - # Format system instructions - report_prompts = ReportPrompts(report_type) - - current_date = datetime.now() - week_of_month = (current_date.day - 1) // 7 + 1 - year = current_date.year - current_week_and_month_and_year = f"Current week: {week_of_month}, Current month: {current_date.strftime('%B')}, Current year: {year}" - - if report_type in WEEKLY_CURATION_REPORT: - system_instructions = report_prompts.final_section_writer_instructions.format(section_title=section.name, - section_topic=section.description, - context=completed_report_sections, - current_week_and_month=current_week_and_month_and_year) - ################################################## - # Here we need to include include week so that it can write proper report title - ################################################## - else: - system_instructions = report_prompts.final_section_writer_instructions.format(section_title=section.name, - section_topic=section.description, - context=completed_report_sections) - - # Generate section - logger.info(f"Generating final section content for section {state['section'].name}") - section_content = llm_writing.invoke([SystemMessage(content=system_instructions)]+[HumanMessage(content="Generate a report section based on the provided sources.")]) - - # Write content to section - logger.info(f"Saving final section content to: {section.name} section object") - section.content = section_content.content - - ################################################## - # here, we have generated content for non-research sections - # content is added to completed_sections state to generate final report - ################################################## - - logger.info(f"Completed writing final section: {state['section'].name}") - return {"completed_sections": [section]} - -def format_sections(sections: list[Section]) -> str: - """ Format a list of sections into a string """ - formatted_str = "" - for idx, section in enumerate(sections, 1): - formatted_str += f""" - {'='*60} - Section {idx}: {section.name} - {'='*60} - Description: - {section.description} - Requires Research: - {section.research} - - Content: - {section.content if section.content else '[Not yet written]'} - - """ - return formatted_str - -def gather_completed_sections(state: ReportState): - """ Gather completed sections from research """ - - # List of completed sections - completed_sections = state["completed_sections"] - - # Format completed section to str to use as context for final sections - logger.info(f"Combining completed sections to one single string") - completed_report_sections = format_sections(completed_sections) - - return {"report_sections_from_research": completed_report_sections} - -def initiate_final_section_writing(state: ReportState): - """ This is the "map" step when we kick off research on any sections that require it using the Send API """ - - # Kick off section writing in parallel via Send() API for any sections that do not require research (intro and conclusion) - logger.info(f"Kicking off final section writing for non-research sections") - return [ - Send("write_final_sections", {"section": s, - "report_sections_from_research": state["report_sections_from_research"], - "report_type": state["report_type"]}) - for s in state["sections"] - if not s.research - ] - -def compile_final_report(state: ReportState): - logger.info("Starting final report compilation") - - """ Compile the final report """ - - # Get sections - sections = state["sections"] - completed_sections = {s.name: s.content for s in state["completed_sections"]} - - # Update sections with completed content while maintaining original order - # actually, this is not necessary, since we have already added content to section in previous steps - for section in sections: - section.content = completed_sections[section.name] - - # Compile final report - logger.info(f"Compiling final report with {len(sections)} sections") - all_sections = "\n\n".join([s.content for s in sections]) - - logger.info("Completed final report compilation") - return {"final_report": all_sections} - -# Add nodes -builder = StateGraph(ReportState, output=ReportStateOutput) -builder.add_node("generate_report_plan", generate_report_plan) -builder.add_node("build_section_with_web_research", section_builder.compile()) -builder.add_node("gather_completed_sections", gather_completed_sections) -builder.add_node("write_final_sections", write_final_sections) -builder.add_node("compile_final_report", compile_final_report) - -# Add edges -builder.add_edge(START, "generate_report_plan") -builder.add_conditional_edges("generate_report_plan", initiate_section_writing, ["build_section_with_web_research"]) -builder.add_edge("build_section_with_web_research", "gather_completed_sections") -builder.add_conditional_edges("gather_completed_sections", initiate_final_section_writing, ["write_final_sections"]) -builder.add_edge("write_final_sections", "compile_final_report") -builder.add_edge("compile_final_report", END) - -# Compile -logger.info(f"Compiling report builder graph") -graph = builder.compile() -# display(Image(graph.get_graph(xray=1).draw_mermaid_png())) diff --git a/backend/financial_agent_utils/curation_report_config.py b/backend/financial_agent_utils/curation_report_config.py deleted file mode 100644 index 0231ebf2..00000000 --- a/backend/financial_agent_utils/curation_report_config.py +++ /dev/null @@ -1,10 +0,0 @@ -#################################### -# Curation report config -#################################### - -ALLOWED_CURATION_REPORTS = ["Ecommerce", "Monthly_Economics", "Weekly_Economics"] - -WEEKLY_CURATION_REPORT = ['Weekly_Economics'] -MONTHLY_CURATION_REPORT = ['Monthly_Economics', 'Ecommerce'] - -NUM_OF_QUERIES = 2 \ No newline at end of file diff --git a/backend/financial_agent_utils/curation_report_tools/__init__.py b/backend/financial_agent_utils/curation_report_tools/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/financial_agent_utils/curation_report_tools/web_search.py b/backend/financial_agent_utils/curation_report_tools/web_search.py deleted file mode 100644 index 4e4bba65..00000000 --- a/backend/financial_agent_utils/curation_report_tools/web_search.py +++ /dev/null @@ -1,150 +0,0 @@ -from typing import Literal, Optional, List -import requests -from pydantic import BaseModel, HttpUrl -import logging -from pathlib import Path -from pydantic_settings import BaseSettings - -# Configure logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -# search default setting -class SearchSettings(BaseSettings): - SEARCH_API_ENDPOINT: HttpUrl = "https://webgpt0-vm2b2htvuuclm.azurewebsites.net/api/web-search" - class Config: - env_prefix = "SEARCH_" # Allow override with env vars like SEARCH_MAX_RESULTS - -search_settings = SearchSettings() - -class SearchResult(BaseModel): - title: str - date: Optional[str] # date is optional because it is not always available - url: str - content: str - -class SearchResponse(BaseModel): - query: str - results: List[SearchResult] - -class CustomSearchClient: - """Client for performing web searches using a custom search endpoint. - - Attributes: - endpoint (str): The search API endpoint - """ - - def __init__(self, endpoint: str = search_settings.SEARCH_API_ENDPOINT): - self.endpoint = endpoint - - def search(self, - query: str, - max_results: int = 2, - search_days: int = 15, - search_mode: Literal["news", "web"] = "news", - include_domains: Optional[List[str]] = None, - **kwargs) -> SearchResponse: - """ - Perform a web search using the custom endpoint. - - Args: - query (str): Search query - max_results (int): Maximum number of results to return - search_days (int): Number of days to search back - search_mode (Literal["news", "web"]): The type of search to perform - include_domains (Optional[List[str]]): List of domains to include in the search - **kwargs: Additional parameters to pass to the search endpoint - - Returns: - SearchResponse: Search results in a format similar to Tavily's response - """ - if not query.strip(): - raise ValueError("The search query must be non-empty.") - if search_days < 0: - search_days = 15 - - payload = { - "query": query, - "mode": search_mode, - "max_results": max_results, - "search_days": search_days, - "include_domains": include_domains, - } - - try: - response = requests.post(self.endpoint, json=payload) - response.raise_for_status() - return SearchResponse(**response.json()) - except requests.exceptions.HTTPError as e: - logger.error(f"HTTP error occurred: {e}") - raise - except requests.exceptions.ConnectionError as e: - logger.error(f"Connection error occurred: {e}") - raise - except requests.exceptions.Timeout as e: - logger.error(f"Timeout error occurred: {e}") - raise - except requests.exceptions.RequestException as e: - logger.error(f"An error occurred: {e}") - raise - except ValueError as e: - logger.error(f"Error parsing search response: {e}") - raise - - def format_results_for_llm(self, results: List[SearchResponse]) -> str: - """Format search results for LLM consumption. - - Args: - results: List of search responses to format - - Returns: - Formatted string containing all search results - """ - formatted = [] - - for query_result in results: - formatted.extend([ - f"\nSearch Query: {query_result.query}\n", - "-" * 80, - self._format_individual_results(query_result.results) - ]) - - return "\n".join(formatted) - - def _format_individual_results(self, results: List[SearchResult]) -> str: - """Helper method to format individual search results. - - Args: - results: List of individual search results to format - - Returns: - Formatted string containing the results - """ - formatted = [] - - for idx, result in enumerate(results, 1): - formatted.extend([ - f"Result {idx}:", - f"Title: {result.title}", - f"Date: {result.date}", - f"URL: {result.url}", - "\nContent:", - f"{result.content}\n", - "-" * 40 - ]) - - return "\n".join(formatted) - -if __name__ == "__main__": - # Test the client - try: - client = CustomSearchClient() - query = "Who won the 2024 presidential election?" - - results = client.search(query = query, max_results=4, search_mode="news", search_days=-1) - print(client.format_results_for_llm([results])) - except Exception as e: - logger.error(f"Test failed: {str(e)}") \ No newline at end of file diff --git a/backend/financial_agent_utils/curation_report_utils.py b/backend/financial_agent_utils/curation_report_utils.py deleted file mode 100644 index fcc2f154..00000000 --- a/backend/financial_agent_utils/curation_report_utils.py +++ /dev/null @@ -1,27 +0,0 @@ -######################### -# Curation Report Generator -######################### -# get the current month and year to format Month_Year.html -from datetime import datetime -current_month = datetime.now().strftime("%B") -current_year = datetime.now().strftime("%Y") - - -REPORT_TOPIC_PROMPT_DICT = { - "Ecommerce": f"Please provide an ecommerce report for {current_month} {current_year}", - "Monthly_Economics": f"Please provide an economics report for {current_month} {current_year}", - "Weekly_Economics": f"Please provide an economics report for this week" -} - - -class ReportGenerationError(Exception): - """Base exception for report generation errors""" - pass - -class InvalidReportTypeError(ReportGenerationError): - """Raised when report type is invalid""" - pass - -class StorageError(ReportGenerationError): - """Raised when storage operations fail""" - pass \ No newline at end of file diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index c2ef6470..dd440ec5 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -7,7 +7,6 @@ import shutil from pathlib import Path from collections import defaultdict -import markdown2 from typing import Dict, List import pandas as pd @@ -24,13 +23,12 @@ from utils import convert_html_to_pdf from app_config import BLOB_CONTAINER_NAME, PDF_PATH -# Load environment variables -load_dotenv() - BLOB_CONNECTION_STRING = os.getenv('BLOB_CONNECTION_STRING') BLOB_CONTAINER_NAME = os.getenv('BLOB_CONTAINER_NAME') +# Load environment variables +load_dotenv() # configure logging logging.basicConfig( @@ -284,61 +282,6 @@ def create_document_paths(output_path: str, equity_name: str, financial_type: st } } -def markdown_to_html(markdown_text: str, output_file: str): - """Convert markdown to HTML using markdown2""" - # Define CSS styles - css_styles = """ - - """ - - html_content = markdown2.markdown(markdown_text, extras=["tables"]) - - # Combine CSS with HTML content - final_html = f""" - - - - - {css_styles} - - - {html_content} - - - """ - - # Create output directory if it doesn't exist - Path(output_file).parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - f.write(final_html) - class BlobStorageError(Exception): """Base exception for blob storage operations""" pass @@ -363,17 +306,6 @@ class BlobDownloadError(BlobStorageError): """Failed to download blob""" pass -class ReportGenerationError(Exception): - """Base exception for report generation errors""" - pass - -class InvalidReportTypeError(ReportGenerationError): - """Raised when report type is invalid""" - pass - -class StorageError(ReportGenerationError): - """Raised when storage operations fail""" - pass class BlobStorageManager: def __init__(self): try: @@ -467,71 +399,18 @@ def download_documents(self, equity_name: str, return downloaded_files # make sure the document_paths is a dict with the structure of create_document_paths - def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: + def upload_to_blob(self, document_paths: dict) -> Dict: """ - Upload files to Azure Blob Storage. Can handle either a document_paths dictionary - or a single file path. + Upload files to Azure Blob Storage with organized folder structure based on filing type. Args: - document_paths (dict, optional): Nested dictionary with equity IDs and their filing types - file_path (str, optional): Direct path to a file to upload - blob_folder (str, optional): Custom folder path in blob storage (defaults to self.blob_base_folder) + document_paths (dict): Nested dictionary with equity IDs and their filing types + container_client: Azure blob container client + base_folder (str): Base folder name in blob storage Returns: - dict: Dictionary of upload results + dict: Dictionary of upload results with equity IDs and filing types as keys """ - if not document_paths and not file_path: - raise ValueError("Either document_paths or file_path must be provided") - - if document_paths and file_path: - raise ValueError("Cannot provide both document_paths and file_path") - - # Handle single file upload - if file_path: - if not os.path.exists(file_path): - raise FileNotFoundError(f"File not found: {file_path}") - - try: - # Use provided blob folder or default to base folder - base_folder = blob_folder if blob_folder else self.blob_base_folder - blob_path = f"{base_folder}/{os.path.basename(file_path)}" - # set the content type based on the file extension - if blob_path.endswith('.pdf'): - content_type = 'application/pdf' - elif blob_path.endswith('.html'): - content_type = 'text/html' - elif blob_path.endswith('.txt'): - content_type = 'text/plain' - else: - content_type = 'application/octet-stream' - with open(file_path, "rb") as data: - try: - self.container_client.upload_blob( - name=blob_path, - data=data, - overwrite=True, - content_settings=ContentSettings(content_type=content_type) - ) - except Exception as e: - raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") - - # get the blob url for the uploaded file - blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" - - result = { - "status": "success", - "blob_path": blob_path, - "blob_url": blob_url - } - logger.info(f'Document has been uploaded to {blob_path}') - return result - - except Exception as e: - result = {"status": "failed", "error": str(e)} - logger.error(f"Failed to upload file {file_path}: {str(e)}") - return result - - # Handle document_paths dictionary upload (original functionality) if not isinstance(document_paths, dict): raise ValueError("document_paths must be a dictionary") @@ -545,34 +424,22 @@ def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blo blob_path = f"{self.blob_base_folder}/{filing_type}/{equity}_summary.pdf" if "summary" in document_path else f"{self.blob_base_folder}/{filing_type}/{equity}.pdf" - # set the content type based on the file extension - if blob_path.endswith('.pdf'): - content_type = 'application/pdf' - elif blob_path.endswith('.html'): - content_type = 'text/html' - elif blob_path.endswith('.txt'): - content_type = 'text/plain' - else: - content_type = 'application/octet-stream' - with open(document_path, "rb") as data: try: self.container_client.upload_blob( name=blob_path, data=data, overwrite=True, - content_settings=ContentSettings(content_type=content_type) + content_settings=ContentSettings(content_type='application/pdf') ) except Exception as e: raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") # get the blob url for the uploaded file blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" - upload_results[equity][filing_type] = { - "status": "success", - "blob_path": blob_path, - "blob_url": blob_url - } + upload_results[equity][filing_type] = {"status": "success", + "blob_path": blob_path, + "blob_url": blob_url} logger.info(f'Document has been uploaded to {blob_path}') except Exception as e: upload_results[equity][filing_type] = {"status": "failed", "error": str(e)} diff --git a/backend/llm_config.py b/backend/llm_config.py index 18689240..8ad3f9f6 100644 --- a/backend/llm_config.py +++ b/backend/llm_config.py @@ -3,7 +3,6 @@ from typing import Dict, Optional import json from openai import AzureOpenAI -from langchain_openai import AzureChatOpenAI import os from dotenv import load_dotenv @@ -61,7 +60,7 @@ class Config: class LLMManager: def __init__(self): self.prompts = PromptTemplate() - self._clients: Dict[str, AzureOpenAI | AzureChatOpenAI] = {} + self._clients: Dict[str, AzureOpenAI] = {} self.config: Dict[str, LLMConfig] = { "gpt4o": LLMConfig( api_base=os.getenv('OPENAI_API_BASE'), @@ -77,31 +76,16 @@ def __init__(self): ) } - def get_client(self, client_type: str = "gpt4o", use_langchain: bool = False) -> AzureOpenAI | AzureChatOpenAI: - """Get or create an Azure OpenAI client - - Args: - client_type: Type of client to create ("gpt4o" or "embedding") - use_langchain: If True, returns a LangChain AzureChatOpenAI client instead of regular AzureOpenAI - """ - client_key = f"{client_type}_langchain" if use_langchain else client_type - - if client_key not in self._clients: + def get_client(self, client_type: str = "gpt4o") -> AzureOpenAI: + """Get or create an Azure OpenAI client""" + if client_type not in self._clients: config = self.config[client_type] - if use_langchain: - self._clients[client_key] = AzureChatOpenAI( - openai_api_key=config.api_key, - openai_api_version=config.api_version, - azure_endpoint=config.api_base, - deployment_name=config.model_name - ) - else: - self._clients[client_key] = AzureOpenAI( - api_key=config.api_key, - api_version=config.api_version, - base_url=f"{config.api_base}/openai/deployments/{config.model_name}" - ) - return self._clients[client_key] + self._clients[client_type] = AzureOpenAI( + api_key=config.api_key, + api_version=config.api_version, + base_url=f"{config.api_base}/openai/deployments/{config.model_name}" + ) + return self._clients[client_type] def get_prompt(self, prompt_type: str) -> str: """Get a prompt template by type""" diff --git a/backend/prompts/curation_reports/__init__.py b/backend/prompts/curation_reports/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/prompts/curation_reports/ecommerce.py b/backend/prompts/curation_reports/ecommerce.py deleted file mode 100644 index 8964c694..00000000 --- a/backend/prompts/curation_reports/ecommerce.py +++ /dev/null @@ -1,204 +0,0 @@ -# Structure -report_structure = """ -This report type is focused on ecommerce trends and the industry news this month. - -The report shouild adhere to the following structure: - -1. **Introduction** (no research needed) - - Provide a brief overview of the ecommerce landscape - - Offer context for analyzing recent business trends - -2. **Main Body**: - - One dedicated section for each major ecommerce platform/company in this list: - * Overall industry trends, Amazon, Shopify, Walmart, Target, Home Depot, Lowe's - - Each section should examine the news and highlight any of the following: - * Tracking significant business events (funding, acquisitions, partnerships) - * Analyzing product launches and feature updates - * Shifts in market strategy and positioning - * Identifying emerging patterns across the industry - * Considering competitive responses and market dynamics - -3. No Main Body Sections other than the ones dedicated to each platform/company in the provided list - -4. Conclusion -- A timeline of key events across companies -- Analysis of emerging industry patterns -- Implications for the broader market""" - -query_writer_instructions=""" - -Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. - -Topic for this section: - -``` -{section_topic} -``` - -When generating {number_of_queries} search queries, ensure they: -1. Cover key aspects of the eCommerce topic, such as: - - Recent business events (e.g, funding, mergers, acquisitions) - - Product launches and feature updates - - Shifts in market strategies and competitive positioning - - Emerging industry patterns or trends - - Customer behavior and technological adoption - -2. Include eCommerce-specific terms, company names, or platform features to refine the search - -3. Target recent information by including relevant time markers (e.g.,"Q4 2024", "December 2024") - -4. Seek insights on: - - Comparisions on differentiators between eCommerce plaforms or companies - - Implications of new strategies or technologies in the industry - -5. Focus on credible sources, such as: - - Official announcements, press releases - - Market research reports - - Blogs, forums, and articles on practical implementation or customer feedbacks - -Your queries should be: -- Specific enough to avoid generic results -- Targeted enough to the eCommerce industry and the topic -- Diverse enough to cover all aspects of the section plan -""" - -# Section writer instructions -section_writer_instructions = """ -You are an expert technical writer responsible for crafting one section of an eCommerce report. - -### Title of the section: -``` -{section_title} -``` - -### Topic for this section: -``` -{section_topic} -``` - -### Guidelines for Writing: - -1. **Technical Accuracy:** - - Include specific metrics, dates, and version numbers where applicable. - - Reference concrete business events (e.g., funding, partnerships, product launches). - - Cite official sources like press releases, financial reports, or industry studies. - - Use precise eCommerce terminology (e.g., platform names, market strategies). - -2. **Length and Style:** - - Limit the section to **150-200 words**. - - Avoid any marketing language; maintain a technical and analytical focus. - - Write in clear, simple language suitable for professional readers. - - Start with your **most important insight in bold**. - - Use short paragraphs (2-3 sentences) for better readability. - -3. **Structure:** - - Use `##` for the section title (Markdown format). - - Include only ONE of the following structural elements, if it clarifies your point: - * A **focused Markdown table** comparing 2-3 key metrics, features, or trends: - - Example: | Platform | Key Feature | Date | - * A **short Markdown list** (3-5 items): - - Use `*` or `-` for unordered lists. - - Use `1.` for ordered lists. - - Properly format and indent all structural elements. - -4. **Writing Approach:** - - Include at least one **specific example or case study** related to the eCommerce topic. - - Focus on concrete insights (e.g., measurable impacts of a strategy or feature). - - Prioritize clarity and conciseness—avoid generalizations or unnecessary details. - - Begin directly with the content; no preamble or introductions. - - Emphasize the single most important insight in your analysis. - - Don't include any sources in the content section. Save sources for the sources section. - -5. **Sources:** - - Use the provided source material to support your analysis: - ``` - {context} - ``` - - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT - ``` - - : [Source](url) - ``` - - Here is a good example example: - ``` - Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) - ``` - - This is a bad example: - ``` - Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) - ``` - - - Include title, date, and URL for each source. - -6. **Quality Checks:** - - Strictly adhere to the **150-200 word count** (excluding title and sources). - - Use only one structural element (table or list) where necessary. - - Start with **bold insight** to capture attention. - - Ensure your writing is concise, specific, and actionable. -""" - - -final_section_writer_instructions="""You are an expert technical writer crafting a section that synthesizes information from the rest of the report. - -Title of the section: -``` -{section_title} -``` - -Section description: -``` -{section_topic} -``` - -Available report content: -``` -{context} -``` - -1. Section-Specific Approach: - -For Introduction: -- Use # for report title (Markdown format). You must include a title for the report -- The title should mention the month and year of the report along with the main ecommerce theme of the month. Here is an example" - -``` -January 2024: eCommerce Trends to Kickstart the New Year -``` - -- 50-100 word limit -- Write in simple and clear language -- Focus on the core motivation for the report in 1-2 paragraphs -- Use a clear narrative arc to introduce the report -- Include NO structural elements (no lists or tables) -- No sources section needed - -For Conclusion/Summary: -- Use ## for section title (Markdown format) -- 100-150 word limit -- Leverage the insights in this report by aligning strategies with market trends, mitigating identified risks, and implementing recommended actions to drive immediate business impact. -- Highlight (bold) the actionable, insightful suggestions -- For comparative reports: - * Must include a focused comparison table using Markdown table syntax - * Table should distill insights from the report - * Keep table entries clear and concise -- For non-comparative reports: - * Only use ONE structural element IF it helps distill the points made in the report: - * Either a focused table comparing items present in the report (using Markdown table syntax) - * Or a short list using proper Markdown list syntax: - - Use `*` or `-` for unordered lists - - Use `1.` for ordered lists - - Ensure proper indentation and spacing -- End with specific next steps or implications -- No sources section needed - -3. Writing Approach: -- Use concrete details over general statements -- Make every word count -- Focus on your single most important point - -4. Quality Checks: -- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section -- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section -- Markdown format -- Do not include word count or any preamble in your response""" \ No newline at end of file diff --git a/backend/prompts/curation_reports/general.py b/backend/prompts/curation_reports/general.py deleted file mode 100644 index 55f9f709..00000000 --- a/backend/prompts/curation_reports/general.py +++ /dev/null @@ -1,65 +0,0 @@ - -# Prompt to generate a search query to help with planning the report outline -## general prompt: -report_planner_query_writer_instructions=""" -You are an expert technical writer, helping to plan a report. - -Current month and year: {today_date} - -The report will be focused on the following topic: - -``` -{topic} -``` - -The report structure will follow these guidelines: - -``` -{report_organization} -``` - -Your goal is to generate {number_of_queries} search queries that will help gather comprehensive information for planning the report sections. - -The query should: - -1. Be related to the topic -2. Help satisfy the requirements specified in the report organization - -Make the query specific enough to find high-quality, relevant sources while covering the breadth needed for the report structure.""" - -# Prompt generating the report outline -## general prompt: -report_planner_instructions=""" - -You are an expert technical writer, helping to plan a report. - -Your goal is to generate the outline of the sections of the report. - -The overall topic of the report is: - -``` -{topic} -``` - -The report should follow this organization: - -``` -{report_organization} -``` - -You should reflect on this information to plan the sections of the report: - -``` -{context} -``` - -Now, generate the sections of the report. Each section should have the following fields: - -- Name - Name for this section of the report. -- Description - Brief overview of the main topics and concepts to be covered in this section. -- Research - Whether to perform web research for this section of the report. -- Content - The content of the section, which you will leave blank for now. - -Consider which sections require web research. For example, introduction and conclusion will not require research because they will distill information from other parts of the report.""" - - diff --git a/backend/prompts/curation_reports/monthly_economics.py b/backend/prompts/curation_reports/monthly_economics.py deleted file mode 100644 index 2939754a..00000000 --- a/backend/prompts/curation_reports/monthly_economics.py +++ /dev/null @@ -1,212 +0,0 @@ -# Structure -report_structure = """ -This report type is focused on analyzing key economic trends and significant events of the past month. - -The report shouild adhere to the following structure: - -1. **Introduction** (no research needed) - - Provide a brief overview of the domestic and global economic landscape - - Offer context for understanding the key economic events and trends analyzed in the report - - -2. **Main Body**: - - Organize sections based on the following categories: - * **Global Economic Trends**: - - Overview of major global economic indicators (e.g., GDP growth, inflation, unemployment rates) - - Analysis of significant developments (e.g., central bank policies, trade agreements, geopolitical events) - * **Regional Highlights**: - - Focus on major regions: North America, Europe, Asia-Pacific, and Emerging Markets - - Key trends, policy changes, and regional challenges - * **Industry-Specific Analysis**: - - Highlight significant trends in major industries such as technology, energy, finance, and healthcare - - Include macroeconomic influences and sectoral performance metrics - * **Financial Market Insights**: - - Overview of stock market performance, bond yields, and currency movements - - Analysis of investor sentiment and market outlook - -3. **Conclusion** - - Recap of key economic events and trends for the month - - Emerging global and regional patterns - - Implications for businesses, policymakers, and investors - -""" - -query_writer_instructions=""" - -Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. - -Topic for this section: - -``` -{section_topic} -``` - -When generating {number_of_queries} search queries, ensure they: -1. Cover key aspects of the eCommerce topic, such as: - - Recent global and regional economic events (e.g., GDP growth, inflation, unemployment) - - Central bank policies and monetary decisions - - Trade agreements, geopolitical developments, and regulatory changes - - Industry-specific trends and performance metrics - - Financial market movements (e.g., stock indices, bond yields, currencies) - -2. Include economics-specific terms, key metrics, and relevant regions or countries to refine the search. - -3. Target recent information by including relevant time markers (e.g., "Q4 2024," "December 2024"). - -4. Seek insights on: - - Comparisons of economic indicators across regions or industries - - Implications of policy changes, global events, or economic shifts for businesses and investors - -5. Focus on credible sources, such as: - - Reports from international economic organizations (e.g., IMF, World Bank, OECD) - - Official government or central bank statements - - Market research, industry reports, and financial analyst commentary - - News articles, blogs, and expert opinion pieces on key economic topics - -Your queries should be: -- Specific enough to avoid generic results -- Targeted to the economics topic and region of interest -- Diverse enough to cover all aspects of the section plan -""" - -# Section writer instructions -section_writer_instructions = """ -You are an expert technical writer responsible for crafting one section of a Monthly Economics Report. - -### Title of the section: -``` -{section_title} -``` - -### Topic for this section: -``` -{section_topic} -``` - -### Guidelines for Writing: - -1. **Technical Accuracy:** - - Include specific metrics, dates, and key economic indicators (e.g., GDP growth, inflation rates, unemployment figures). - - Reference concrete events (e.g., central bank decisions, trade agreements, geopolitical developments). - - Cite official sources such as government reports, financial analyses, or statements from international organizations. - - Use precise economic terminology and maintain clarity. - -2. **Length and Style:** - - Limit the section to **150-200 words**. - - Maintain an analytical and professional tone; avoid opinionated or speculative language. - - Write in clear, concise language suitable for policymakers, analysts, and professionals. - - Start with your **most important insight in bold**. - - Use short paragraphs (2-3 sentences) for better readability. - -3. **Structure:** - - Use `##` for the section title (Markdown format). - - Include only ONE of the following structural elements if relevant: - * A **focused Markdown table** summarizing key metrics or comparisons: - - Example: | Region | GDP Growth (%) | Inflation (%) | - * A **short Markdown list** (3-5 items): - - Use `*` or `-` for unordered lists. - - Use `1.` for ordered lists. - - Properly format and indent all structural elements. - -4. **Writing Approach:** - - Include at least one **specific example or case study** related to the economic topic. - - Focus on actionable insights (e.g., implications of a policy change or economic trend). - - Avoid generalizations or excessive detail; prioritize clarity and conciseness. - - Begin directly with the content; avoid introductions or background that restates the title or topic. - - Emphasize the single most critical insight in your analysis. - - Do not include sources in the main content; list them in the sources section. - -5. **Sources:** - - Use the provided source material to support your analysis: - ``` - {context} - ``` - - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT - ``` - - : [Source](url) - ``` - - Here is a good example example: - ``` - Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) - ``` - - This is a bad example: - ``` - Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) - ``` - - - Include title, date, and URL for each source. - -6. **Quality Checks:** - - Strictly adhere to the **150-200 word count** (excluding title and sources). - - Use only one structural element (table or list) where necessary. - - Start with **bold insight** to capture attention. - - Ensure your writing is concise, specific, and actionable. -""" - - -final_section_writer_instructions="""You are an expert technical writer crafting a section that synthesizes information from the rest of the report. - -Title of the section: -``` -{section_title} -``` - -Section description: -``` -{section_topic} -``` - -Available report content: -``` -{context} -``` - -1. Section-Specific Approach: - -For Introduction: -- Use # for report title (Markdown format). You must include a title for the report -- The title should mention the month and year of the report along with the main economic theme of the month. Example: - -``` -January 2024: Key Economic Trends Shaping the Global Landscape -``` - -- 50-100 word limit -- Write in simple and clear language -- Focus on the purpose and scope of the report in 1-2 paragraphs -- Use a concise narrative arc to introduce the report -- Include NO structural elements (no lists or tables) -- No sources section needed - -For Conclusion/Summary: -- Use ## for section title (Markdown format) -- 100-150 word limit -- Leverage the insights from this report by identifying actionable strategies for policymakers, businesses, or investors to address risks and capitalize on trends. -- Highlight (bold) key takeaways and actionable insights. -- For comparative reports: - * Must include a focused comparison table using Markdown table syntax. - * Table should distill insights from the report. - * Keep table entries clear and concise. -- For non-comparative reports: - * Use ONLY ONE structural element IF it helps clarify points made in the report: - * Either a focused table summarizing key metrics or findings (using Markdown table syntax). - * Or a short list using proper Markdown list syntax: - - Use `*` or `-` for unordered lists. - - Use `1.` for ordered lists. - - Ensure proper indentation and spacing. -- End with actionable implications or recommendations. -- No sources section needed. - -3. Writing Approach: -- Prioritize concrete details over generalizations. -- Ensure every word contributes to clarity and precision. -- Focus on the single most critical insight for each section. - -4. Quality Checks: -- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section. -- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section. -- Use Markdown format. -- Do not include word count or any preamble in your response. -""" \ No newline at end of file diff --git a/backend/prompts/curation_reports/weekly_economics.py b/backend/prompts/curation_reports/weekly_economics.py deleted file mode 100644 index 7aa07dd7..00000000 --- a/backend/prompts/curation_reports/weekly_economics.py +++ /dev/null @@ -1,215 +0,0 @@ -# Structure -report_structure = """ -This report type is focused on analyzing key economic trends and significant events of the past week. - -The report should adhere to the following structure: - -1. **Introduction** (no research needed) - - Provide a brief overview of the domestic and global economic landscape for the week. - - Offer context for understanding the key economic events and trends analyzed in the report. - -2. **Main Body**: - - Organize sections based on the following categories: - * **Global Economic Trends**: - - Overview of major global economic indicators for the week (e.g., GDP updates, inflation snapshots, unemployment figures). - - Analysis of significant developments (e.g., central bank announcements, trade disputes, geopolitical updates). - * **Regional Highlights**: - - Focus on major regions: North America, Europe, Asia-Pacific, and Emerging Markets. - - Key events, policy changes, and notable economic challenges. - * **Industry-Specific Updates**: - - Highlight weekly developments in key industries such as technology, energy, finance, and healthcare. - - Brief analysis of macroeconomic influences on sectoral performance. - * **Financial Market Movements**: - - Weekly performance of stock markets, bond yields, and currency movements. - - Analysis of investor sentiment and short-term market trends. - -3. **Conclusion** - - Recap of key economic events and trends for the week. - - Emerging global and regional patterns. - - Implications for businesses, policymakers, and investors over the near term. -""" - -query_writer_instructions=""" - -Your goal is to generate targeted web search queries that will gather comprehensive information for writing a technical report section. - -Topic for this section: - -``` -{section_topic} -``` - -When generating {number_of_queries} search queries, ensure they: -1. Cover key aspects of the weekly economic topic, such as: - - Major global and regional economic events of the week (e.g., GDP updates, inflation reports, unemployment rates) - - Central bank statements or decisions announced during the week - - Significant trade agreements, geopolitical developments, or regulatory changes - - Weekly trends in specific industries and performance metrics - - Financial market movements (e.g., weekly stock index changes, bond yields, currency fluctuations) - -2. Include economics-specific terms, key metrics, and relevant regions or countries to refine the search. - -3. Target recent information by including weekly time markers (e.g., "week of December 18, 2024," "last week December 2024"). - -4. Seek insights on: - - Week-to-week comparisons of economic indicators across regions or industries - - Immediate implications of new policies, global events, or economic shifts for businesses, policymakers, and investors - -5. Focus on credible sources, such as: - - Reports and updates from international economic organizations (e.g., IMF, World Bank, OECD) - - Central bank announcements and government statements - - Market research, weekly financial analyses, and expert commentary - - News articles or blogs providing real-time insights on economic events - -Your queries should be: -- Specific enough to avoid generic results -- Focused on recent events relevant to the week in review -- Diverse enough to cover all aspects of the section plan -""" - -# Section writer instructions -section_writer_instructions = """ -You are an expert technical writer responsible for crafting one section of a Weekly Economics Report. - -### Title of the section: -``` -{section_title} -``` - -### Topic for this section: -``` -{section_topic} -``` - -### Guidelines for Writing: - -1. **Technical Accuracy:** - - Include specific metrics, dates, and key economic indicators relevant to the week (e.g., GDP updates, weekly inflation rates, unemployment figures). - - Reference concrete events (e.g., central bank announcements, trade negotiations, geopolitical updates). - - Cite official sources such as government releases, financial analyses, or reports from international organizations. - - Use precise economic terminology while maintaining clarity. - -2. **Length and Style:** - - Limit the section to **150-200 words**. - - Maintain an analytical and professional tone; avoid subjective or speculative language. - - Write in clear, concise language suitable for policymakers, investors, and professionals. - - Start with your **most important insight in bold**. - - Use short paragraphs (2-3 sentences) for better readability. - -3. **Structure:** - - Use `##` for the section title (Markdown format). - - Include only ONE of the following structural elements if relevant: - * A **focused Markdown table** summarizing key weekly metrics or comparisons: - - Example: | Indicator | Value | Date | - * A **short Markdown list** (3-5 items): - - Use `*` or `-` for unordered lists. - - Use `1.` for ordered lists. - - Properly format and indent all structural elements. - -4. **Writing Approach:** - - Include at least one **specific example or case study** relevant to the economic topic of the week. - - Focus on actionable insights (e.g., immediate implications of a policy change or trend). - - Avoid generalizations or excessive background information; prioritize clarity and conciseness. - - Begin directly with the content; avoid introductions or redundant restatements of the title or topic. - - Highlight the single most important takeaway in your analysis. - - Do not include sources in the main content; list them in the sources section. - -5. **Sources:** - - Use the provided source material to support your analysis: - ``` - {context} - ``` - - List sources at the end in this format. YOU MUST STRICTLY FOLLOW THIS FORMAT - ``` - - : [Source](url) - ``` - - Here is a good example example: - ``` - Yoolax Smart Blinds Launches Exclusive Christmas Discounts: Up to 15% Off Now Through December 31, 2024 - Markets Insider: [Source](https://markets.businessinsider.com/news/stocks/yoolax-smart-blinds-launches-exclusive-christmas-discounts-up-to-15-off-now-through-december-31-2024-1034147545) - ``` - - This is a bad example: - ``` - Lantern AI Quiz Builder Reveals Key Insights to Boost Shopify Store Revenue : [Markets Insider](https://markets.businessinsider.com/news/stocks/lantern-ai-quiz-builder-reveals-key-insights-to-boost-shopify-store-revenue-1034142499) - ``` - - - Include title, date, and URL for each source. - -6. **Quality Checks:** - - Strictly adhere to the **150-200 word count** (excluding title and sources). - - Use only one structural element (table or list) where necessary. - - Start with **bold insight** to capture attention. - - Ensure your writing is concise, specific, and actionable. -""" - -final_section_writer_instructions=""" -You are an expert technical writer crafting a section that synthesizes information from the rest of the report. - -Current week and month: -``` -{current_week_and_month} -``` - -Title of the section: -``` -{section_title} -``` - -Section description: -``` -{section_topic} -``` - -Available report content: -``` -{context} -``` - -1. Section-Specific Approach: - -For Introduction: -- Use # for report title (Markdown format). You must include a title for the report -- The title should mention the week and month of the report along with the main economic theme of the week. Example: - -``` -Week 1 of January 2024: Key Economic Trends Shaping the Global Landscape -``` - -- 50-100 word limit -- Write in simple and clear language -- Focus on the purpose and scope of the report in 1-2 paragraphs -- Use a concise narrative arc to introduce the report -- Include NO structural elements (no lists or tables) -- No sources section needed - -For Conclusion/Summary: -- Use ## for section title (Markdown format). -- 100-150 word limit. -- Leverage the insights from this report by identifying actionable strategies for policymakers, businesses, or investors to address risks and capitalize on trends. -- Highlight (bold) key takeaways and actionable insights. -- For comparative reports: - * Must include a focused comparison table using Markdown table syntax. - * Table should distill insights from the report. - * Keep table entries clear and concise. -- For non-comparative reports: - * Use ONLY ONE structural element IF it helps clarify points made in the report: - * Either a focused table summarizing key metrics or findings (using Markdown table syntax). - * Or a short list using proper Markdown list syntax: - - Use `*` or `-` for unordered lists. - - Use `1.` for ordered lists. - - Ensure proper indentation and spacing. -- End with actionable implications or recommendations. -- No sources section needed. - -3. Writing Approach: -- Prioritize concrete details over generalizations. -- Ensure every word contributes to clarity and precision. -- Focus on the single most critical insight for each section. - -4. Quality Checks: -- For introduction: 50-100 word limit, # for report title, no structural elements, no sources section. -- For conclusion: 100-150 word limit, ## for section title, only ONE structural element at most, no sources section. -- Use Markdown format. -- Do not include word count or any preamble in your response. -""" \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 28c929e8..db99eeac 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,7 @@ azure-identity==1.15.0 flask==2.2.2 flask-cors==3.0.10 werkzeug==2.2.2 -requests==2.28.2 +requests python-dotenv==1.0.0 azure-keyvault-secrets==4.9.0 azure-storage-blob==12.19.0 @@ -21,13 +21,9 @@ sec-edgar-downloader==5.0.2 pdfkit==1.0.0 pydantic<=2.10.3 tavily-python==0.5.0 -openai +openai<=1.57.4 tiktoken<=0.8.0 pandas<=2.2.3 pymupdf==1.23.7 reportlab==4.2.5 -Flask-Session==0.8.0 -langgraph==0.2.60 -markdown2==2.5.2 -langchain-openai==0.2.14 -pydantic-settings==2.7.0 \ No newline at end of file +Flask-Session==0.8.0 \ No newline at end of file From 42ed4bc3528a5f497417ac153d9494ca77e89c65 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:44:35 -0500 Subject: [PATCH 260/820] FA-319 added process_and_summarize_document and date filter for financialdocuments endpoint (#234) * modify financialdocument endpoint and added process_and_summarize_document endpoint * added process_and_summarize_document endpoint * added timestamp to to process_and_summarize_document endpoint --- backend/app.py | 295 ++++++++++++++++++++--------- backend/financial_doc_processor.py | 136 ++++++++++++- backend/utils.py | 40 ++-- 3 files changed, 362 insertions(+), 109 deletions(-) diff --git a/backend/app.py b/backend/app.py index 115c02c9..38912555 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2129,114 +2129,83 @@ def determine_subscription_tiers(subscription): from sec_edgar_downloader import Downloader from app_config import FILING_TYPES, BASE_FOLDER + +doc_processor = FinancialDocumentProcessor() # from financial_doc_processor @app.route('/api/SECEdgar/financialdocuments', methods=['GET']) -def process_financial_documents(): - # payload example: -# { -# "equity_ids": ["AAPL", "MSFT"], -# "filing_types": ["10-Q", "10-K"] -# } +def process_edgar_document(): + """ + Process a single financial document from SEC EDGAR. + + Args for payload: + equity_id (str): Stock symbol/ticker (e.g., 'AAPL') + filing_type (str): SEC filing type (e.g., '10-K') + after_date (str, optional): Filter for filings after this date (YYYY-MM-DD) + + Returns: + JSON Response with processing status and results + + Raises: + 400: Invalid request parameters + 404: Document not found + 500: Internal server error + """ try: - # # Check and install wkhtmltopdf if needed + # Validate request and setup if not check_and_install_wkhtmltopdf(): return jsonify({ "status": "error", - "message": "Failed to install required dependency wkhtmltopdf" + "message": "Failed to install required dependency wkhtmltopdf", + "code": 500 }), 500 - # Initialize the SEC Edgar Downloader - dl = Downloader( - os.getenv("USER_AGENT_NAME", "SalesFactory"), - os.getenv("USER_AGENT_EMAIL", "nam.tran@salesfactory.com") - ) - # Get parameters from request body + # Get and validate parameters data = request.get_json() - if not data: return jsonify({ "status": "error", - "message": "No data provided" + "message": "No data provided", + "code": 400 }), 400 - - # Validate the payload - is_valid, error_message = validate_payload(data) - if not is_valid: + + # Extract and validate parameters + equity_id = data.get('equity_id') + filing_type = data.get('filing_type') + after_date = data.get('after_date', None) + + if not equity_id or not filing_type: return jsonify({ "status": "error", - "message": error_message + "message": "Both equity_id and filing_type are required", + "code": 400 }), 400 - equity_ids = data.get('equity_ids', []) # required to provide a list of equity ids - filing_types = data.get('filing_types', FILING_TYPES) # use default if not provided (10-Q, 10-K, 8-K) + if filing_type not in FILING_TYPES: + return jsonify({ + "status": "error", + "message": f"Invalid filing type. Must be one of: {FILING_TYPES}", + "code": 400 + }), 400 - # Download SEC filings - for equity_id in equity_ids: - for filing_type in filing_types: - try: - logger.info(f"Downloading {filing_type} for {equity_id}") - dl.get(filing_type, equity_id, limit=1, download_details=True) - except Exception as e: - logger.error(f"Failed to download {filing_type} for {equity_id}: {str(e)}") - return jsonify({ - "status": "error", - "message": f"Failed to download {filing_type} for {equity_id}: {str(e)}" - }), 500 + # Download filing + download_result = doc_processor.download_filing(equity_id, filing_type, after_date) - # Collect documents path for all downloaded files - document_paths = collect_filing_documents( - EQUITY_IDS=equity_ids, - FILING_TYPES=filing_types, - get_downloaded_files=get_downloaded_files - ) + if download_result.get("status") != "success": + return jsonify(download_result), download_result.get("code", 500) - blob_manager = BlobStorageManager() # from financial_doc_processor + # Process and upload document + upload_result = doc_processor.process_and_upload(equity_id, filing_type) + return jsonify(upload_result), upload_result.get("code", 500) - results = {} - # Validate collected documents paths - if validate_document_paths(document_paths): - logger.info("Document collection completed successfully") - - # Upload to blob storage - results = blob_manager.upload_to_blob(document_paths) - - # Check if all uploads were successful - all_uploads_successful = True - for equity, filings in results.items(): - for filing_type, result in filings.items(): - if result["status"] != "success": - all_uploads_successful = False - break - - # Only cleanup if all uploads were successful - if all_uploads_successful: - if cleanup_resources(): - logger.info("Successfully cleaned up all files in sec-edgar-filings") - else: - logger.error("Failed to clean up files in sec-edgar-filings") - else: - logger.warning("Skipping cleanup as some uploads failed") - - return jsonify({ - "status": "success", - "message": "Documents processed successfully", - "results": results - }), 200 - else: - return jsonify({ - "status": "error", - "message": "Document collection validation failed" - }), 400 - except Exception as e: logger.error(f"API execution failed: {str(e)}") return jsonify({ "status": "error", - "message": str(e) + "message": str(e), + "code": 500 }), 500 from tavily_tool import TavilySearch - @app.route('/api/web-search', methods = ['POST']) def web_search(): """ @@ -2248,7 +2217,7 @@ def web_search(): "mode": "news" or "general", default is "news" //required "max_results": optional int, default = 2 //optional "include_domains": optional list of strings, default = None // - "days": optional int, default = 30 // + "search_days": optional int, default = 30 // } """ try: @@ -2317,16 +2286,26 @@ def web_search(): 'error': "An error occurred while processing the request." }), 500 + from app_config import IMAGE_PATH from summarization import DocumentSummarizer - @app.route('/api/SECEdgar/financialdocuments/summary', methods=['POST']) def generate_summary(): - # payload example: - # { - # "equity_name": "MS", - # "financial_type": "10-K" - # } + """ + Endpoint to generate a summary of financial documents from SEC Edgar. + + Request Payload Example: + { + "equity_name": "MS", # The name of the equity (e.g., 'MS' for Morgan Stanley) + "financial_type": "10-K" # The type of financial document (e.g., '10-K' for annual reports) + } + + Required Fields: + - equity_name (str): The name of the equity. + - financial_type (str): The type of financial document. + + Both fields must be non-empty strings. + """ try: try: data = request.get_json() @@ -2459,9 +2438,146 @@ def generate_summary(): except Exception as e: logging.error(f"Failed to clean up: {e}") -from datetime import datetime -from pathlib import Path +from utils import _extract_response_data +@app.route('/api/SECEdgar/financialdocuments/process-and-summarize', methods=['POST']) +def process_and_summarize_document(): + """ + Process and summarize a financial document in sequence. + + Args: + equity_id (str): Stock symbol/ticker (e.g., 'AAPL') + filing_type (str): SEC filing type (e.g., '10-K') + after_date (str, optional): Filter for filings after this date (YYYY-MM-DD) + + Returns: + JSON Response with structure: + { + "status": "success", + "edgar_data_process": {...}, + "summary_process": {...} + } + + Raises: + 400: Invalid request parameters + 404: Document not found + 500: Internal server error + """ + # Input validation + try: + data = request.get_json() + if not data: + return jsonify({ + 'status': 'error', + 'error': 'Invalid request', + 'details': 'Request body is requred and must be a valid JSON object', + 'timestamp': datetime.now().isoformat() + }), 400 + + # Validate required fields + required_fields = ['equity_id', 'filing_type'] + if not all(field in data for field in required_fields): + return jsonify({ + 'status': 'error', + 'error': 'Missing required fields', + 'details': f"Missing required fields: {', '.join(required_fields)}", + 'timestamp': datetime.now().isoformat() + }), 400 + + # Validate filing type + if data['filing_type'] not in FILING_TYPES: + return jsonify({ + 'status': 'error', + 'error': 'Invalid filing type', + 'details': f"Invalid filing type. Must be one of: {', '.join(FILING_TYPES)}", + 'timestamp': datetime.now().isoformat() + }), 400 + + # Validate date format if provided + if 'after_date' in data: + try: + datetime.strptime(data['after_date'], '%Y-%m-%d') + except ValueError: + return jsonify({ + 'status': 'error', + 'error': 'Invalid date format', + 'details': 'Use YYYY-MM-DD', + 'timestamp': datetime.now().isoformat() + }), 400 + + except ValueError as e: + logger.error(f"Invalid request data: {str(e)}") + return jsonify({ + 'status': 'error', + 'error': 'Invalid request data', + 'details': str(e), + 'timestamp': datetime.now().isoformat() + }), 400 + try: + # Step 1: Process document + logger.info(f"Starting document processing for {data['equity_id']} {data['filing_type']}") + with app.test_request_context( + '/api/SECEdgar/financialdocuments', + method='GET', + json=data + ) as ctx: + process_result = process_edgar_document() + process_data = _extract_response_data(process_result) + + if process_data.get('status') != 'success': + logger.error(f"Document processing failed: {process_data.get('message')}") + return jsonify({ + 'status': 'error', + 'error': process_data.get('message'), + 'details': process_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), + 'timestamp': datetime.now().isoformat() + }), 500 + + # Step 2: Generate summary + logger.info(f"Starting summary generation for {data['equity_id']} {data['filing_type']}") + summary_payload = { + "equity_name": data["equity_id"], + "financial_type": data["filing_type"] + } + + with app.test_request_context( + '/api/SECEdgar/financialdocuments/summary', + method='POST', + json=summary_payload + ) as ctx: + summary_result = generate_summary() + summary_data = _extract_response_data(summary_result) + + if summary_data.get('status') != 'success': + logger.error(f"Summary generation failed: {summary_data.get('message')}") + return jsonify({ + 'status': 'error', + 'error': summary_data.get('message'), + 'details': summary_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), + 'timestamp': datetime.now().isoformat() + }), 500 + + # Return combined results + response_data = { + 'status': 'success', + 'edgar_data_process': process_data, + 'summary_process': summary_data + } + + logger.info(f"Successfully processed and summarized document for {data['equity_id']}") + return jsonify(response_data), 200 + + except Exception as e: + logger.exception(f"Unexpected error in process_and_summarize_document: {str(e)}") + return jsonify({ + 'status': 'error', + 'error': 'An unexpected error occurred while processing the document', + 'details': str(e), + 'timestamp': datetime.now().isoformat() + }), 500 + + +from pathlib import Path from curation_report_generator import graph from financial_doc_processor import markdown_to_html, BlobStorageManager from financial_agent_utils.curation_report_utils import ( @@ -2476,7 +2592,6 @@ def generate_summary(): NUM_OF_QUERIES ) - @app.route('/api/reports/generate/curation', methods=['POST']) def generate_report(): try: diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index c2ef6470..ca5dad2c 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -362,6 +362,10 @@ class BlobUploadError(BlobStorageError): class BlobDownloadError(BlobStorageError): """Failed to download blob""" pass +# class to catch metadata error +class BlobMetadataError(BlobStorageError): + """Failed to retrieve metadata""" + pass class ReportGenerationError(Exception): """Base exception for report generation errors""" @@ -465,9 +469,33 @@ def download_documents(self, equity_name: str, raise return downloaded_files + + def get_document_metadata(self, remote_file_path: str) -> dict: + """Retrieve metadata for a specific blob from defined container in env + + Args: + remote_file_path (str): Path to the blob in blob storage + + Returns: + dict: Metadata of the blob + + Raises: + BlobMetadataError: If there is an error retrieving metadata + Example: + metadata = doc_processor.get_document_metadata('financial/10-K/AAPL.pdf') + print(metadata) + """ + + try: + blob_client = self.container_client.get_blob_client(remote_file_path) + blob_properties = blob_client.get_blob_properties() + return blob_properties.metadata + except Exception as e: + raise BlobMetadataError(f"Error retrieving metadata for {remote_file_path}: {str(e)}") + # make sure the document_paths is a dict with the structure of create_document_paths - def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: + def upload_to_blob(self, document_paths: dict, metadata: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: """ Upload files to Azure Blob Storage. Can handle either a document_paths dictionary or a single file path. @@ -561,7 +589,8 @@ def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blo name=blob_path, data=data, overwrite=True, - content_settings=ContentSettings(content_type=content_type) + content_settings=ContentSettings(content_type=content_type), + metadata=metadata ) except Exception as e: raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") @@ -571,7 +600,8 @@ def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blo upload_results[equity][filing_type] = { "status": "success", "blob_path": blob_path, - "blob_url": blob_url + "blob_url": blob_url, + "metadata": metadata } logger.info(f'Document has been uploaded to {blob_path}') except Exception as e: @@ -579,3 +609,103 @@ def upload_to_blob(self, document_paths: dict = None, file_path: str = None, blo logger.error(f"Failed to upload {equity} {filing_type}: {str(e)}") return upload_results + +from sec_edgar_downloader import Downloader +from utils import cleanup_resources +class FinancialDocumentProcessor: + def __init__(self): + self.dl = Downloader( + os.getenv("USER_AGENT_NAME", "SalesFactory"), + os.getenv("USER_AGENT_EMAIL", "nam.tran@salesfactory.com") + ) + self.blob_manager = BlobStorageManager() + + def download_filing(self, equity_id: str, filing_type: str, after_date: str = None) -> dict: + """Download a single SEC filing.""" + try: + if after_date: + logger.info(f"Downloading {filing_type} for {equity_id} after {after_date}") + num_downloaded_file = self.dl.get(filing_type, equity_id, limit=1, download_details=True, after=after_date) + if num_downloaded_file == 0: + return { + "status": "error", + "message": f"No {filing_type} found after {after_date} for {equity_id}", + "code": 404 + } + else: + logger.info(f"Downloading most recent {filing_type} for {equity_id}") + self.dl.get(filing_type, equity_id, limit=1, download_details=True) + + return { + "status": "success", + "message": f"Successfully downloaded {filing_type} for {equity_id}", + "code": 200 + } + except Exception as e: + logger.error(f"Download failed: {str(e)}") + return { + "status": "error", + "message": f"Failed to download {filing_type} for {equity_id}: {str(e)}", + "code": 500 + } + + def process_and_upload(self, equity_id: str, filing_type: str) -> dict: + """Process and upload a single document.""" + try: + document_paths = collect_filing_documents( + EQUITY_IDS=[equity_id], + FILING_TYPES=[filing_type], + get_downloaded_files=get_downloaded_files + ) + + if not validate_document_paths(document_paths): + return { + "status": "error", + "message": "Document collection validation failed", + "code": 400 + } + + # add metadata to uploaded document + from datetime import datetime + metadata = { + 'equity_id': equity_id, + 'filing_type': filing_type, + 'uploaded_date': datetime.now().strftime('%Y-%m-%d'), + 'source': 'SEC EDGAR', + } + + results = self.blob_manager.upload_to_blob(document_paths, metadata=metadata) + + equity_result = results.get(equity_id, {}) + filing_result = equity_result.get(filing_type, {}) + upload_successful = filing_result.get("status") == "success" + + if upload_successful: + if cleanup_resources(): + logger.info("Successfully cleaned up files") + else: + logger.warning("Failed to clean up files") + else: + logger.warning("Skipping cleanup as upload failed") + + return { + "status": "success" if upload_successful else "error", + "message": "Document processed successfully" if upload_successful else "Upload failed", + "results": results, + "code": 200 if upload_successful else 500 + } + except Exception as e: + logger.error(f"Processing failed: {str(e)}") + return { + "status": "error", + "message": f"Processing failed: {str(e)}", + "code": 500 + } + +# example usage for get_document_metadata +if __name__ == "__main__": + doc_processor = BlobStorageManager() + metadata = doc_processor.get_document_metadata('financial/10-K/AAPL.pdf') + print(metadata) + +# if upload date is within the past diff --git a/backend/utils.py b/backend/utils.py index c446ac0b..f81adeab 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -85,9 +85,13 @@ def decorated_function(*args, **kwargs): logger = logging.getLogger(__name__) +################################################ +# financialDocument (EDGAR) Ingestion +################################################ + def validate_payload(data: Dict[str, Any]) -> Tuple[bool, str]: """ - Validate the request payload. + Validate the request payload for Edgar financialDocument endpoint Args: data (dict): The request payload @@ -96,26 +100,24 @@ def validate_payload(data: Dict[str, Any]) -> Tuple[bool, str]: tuple: (is_valid: bool, error_message: str) """ # Check if equity_ids exists and is not empty - if not data.get('equity_ids'): - return False, "equity_ids is required" + if not data.get('equity_id'): + return False, "equity_id is required" - # Check if equity_ids is a list - if not isinstance(data['equity_ids'], list): - return False, "equity_ids must be a list" + # check if date is provided + if not data.get('after_date'): + logger.warning("No after_date provided, retrieving most recent filings") # Check if equity_ids is not empty - if len(data['equity_ids']) == 0: - return False, "equity_ids cannot be empty" + if data['equity_id'].strip() == "": + return False, "equity_id cannot be empty" # Validate filing_types if provided - if 'filing_types' in data: - if not isinstance(data['filing_types'], list): - return False, "filing_types must be a list" - - # Check if all filing types are valid - invalid_types = [ft for ft in data['filing_types'] if ft not in ALLOWED_FILING_TYPES] - if invalid_types: - return False, f"Invalid filing type(s): {', '.join(invalid_types)}. Allowed types are: {', '.join(ALLOWED_FILING_TYPES)}" + if not data.get('filing_type'): + return False, "filing_type is required" + + # Check if all filing types are valid + if data['filing_type'] not in ALLOWED_FILING_TYPES: + return False, f"Invalid filing type(s): {data['filing_type']}. Allowed types are: {', '.join(ALLOWED_FILING_TYPES)}" return True, "" @@ -312,3 +314,9 @@ def cleanup_resources() -> bool: except Exception as e: logger.error(f"Error during cleanup: {str(e)}") return False + +def _extract_response_data(response): + """Helper function to extract JSON data from response objects""" + if isinstance(response, tuple): + return response[0].get_json() + return response.get_json() \ No newline at end of file From b9f140f9121a213be699cc2d33761026f95f5c2e Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Sat, 28 Dec 2024 12:05:23 -0500 Subject: [PATCH 261/820] convert time zone to utc --- backend/app.py | 36 ++++++++++++++--------- backend/financial_doc_processor.py | 47 ++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/backend/app.py b/backend/app.py index 38912555..a473894a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2470,7 +2470,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': 'Invalid request', 'details': 'Request body is requred and must be a valid JSON object', - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 400 # Validate required fields @@ -2480,7 +2480,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': 'Missing required fields', 'details': f"Missing required fields: {', '.join(required_fields)}", - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 400 # Validate filing type @@ -2489,7 +2489,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': 'Invalid filing type', 'details': f"Invalid filing type. Must be one of: {', '.join(FILING_TYPES)}", - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 400 # Validate date format if provided @@ -2501,7 +2501,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': 'Invalid date format', 'details': 'Use YYYY-MM-DD', - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 400 except ValueError as e: @@ -2510,7 +2510,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': 'Invalid request data', 'details': str(e), - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 400 try: @@ -2526,12 +2526,20 @@ def process_and_summarize_document(): if process_data.get('status') != 'success': logger.error(f"Document processing failed: {process_data.get('message')}") - return jsonify({ - 'status': 'error', - 'error': process_data.get('message'), - 'details': process_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), - 'timestamp': datetime.now().isoformat() - }), 500 + if process_data.get('code') == 404: + return jsonify({ + 'status': 'not_found', + 'error': process_data.get('message'), + 'code': process_data.get('code'), + 'timestamp': datetime.now(timezone.utc).isoformat() + }), 404 + else: + return jsonify({ + 'status': 'error', + 'error': process_data.get('message'), + 'code': process_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), + 'timestamp': datetime.now(timezone.utc).isoformat() + }), 500 # Step 2: Generate summary logger.info(f"Starting summary generation for {data['equity_id']} {data['filing_type']}") @@ -2554,7 +2562,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': summary_data.get('message'), 'details': summary_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 500 # Return combined results @@ -2573,7 +2581,7 @@ def process_and_summarize_document(): 'status': 'error', 'error': 'An unexpected error occurred while processing the document', 'details': str(e), - 'timestamp': datetime.now().isoformat() + 'timestamp': datetime.now(timezone.utc).isoformat() }), 500 @@ -2617,7 +2625,7 @@ def generate_report(): }) # Generate file path - current_date = datetime.now() + current_date = datetime.now(timezone.utc) week_of_month = (current_date.day - 1) // 7 + 1 if report_topic_rqst in WEEKLY_CURATION_REPORT: file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}/Week_{week_of_month}.html") diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index ca5dad2c..4858ab32 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -9,6 +9,7 @@ from collections import defaultdict import markdown2 from typing import Dict, List +from datetime import datetime, timezone import pandas as pd import fitz @@ -621,16 +622,50 @@ def __init__(self): self.blob_manager = BlobStorageManager() def download_filing(self, equity_id: str, filing_type: str, after_date: str = None) -> dict: - """Download a single SEC filing.""" + """ + Download a single SEC filing. + + Args: + equity_id (str): The equity identifier (e.g., 'AAPL') + filing_type (str): The type of filing (e.g., '10-K') + after_date (str): Date string in 'YYYY-MM-DD' format + + Returns: + dict: Status of the download operation + """ try: if after_date: - logger.info(f"Downloading {filing_type} for {equity_id} after {after_date}") - num_downloaded_file = self.dl.get(filing_type, equity_id, limit=1, download_details=True, after=after_date) - if num_downloaded_file == 0: + # Validate date format + try: + # Parse the input date + parsed_date = datetime.strptime(after_date, '%Y-%m-%d') + + # Ensure date is in UTC timezone + utc_date = parsed_date.replace(tzinfo=timezone.utc) + + # Convert to string format expected by SEC EDGAR + formatted_date = utc_date.strftime('%Y-%m-%d') + + logger.info(f"Downloading {filing_type} for {equity_id} after {formatted_date}") + num_downloaded_file = self.dl.get( + filing_type, + equity_id, + limit=1, + download_details=True, + after=formatted_date + ) + + if num_downloaded_file == 0: + return { + "status": "not_found", + "message": f"No {filing_type} found after {formatted_date} for {equity_id}", + "code": 404 + } + except ValueError as e: return { "status": "error", - "message": f"No {filing_type} found after {after_date} for {equity_id}", - "code": 404 + "message": f"Error: {str(e)}", + "code": 400 } else: logger.info(f"Downloading most recent {filing_type} for {equity_id}") From 2c0716fe3bed8669649b9062bf30b3c345605b11 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:46:13 -0500 Subject: [PATCH 262/820] Patch time zone difference include "before" param to make sure after is valid regardless of timezone --- backend/financial_doc_processor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 4858ab32..e99e3374 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -9,7 +9,7 @@ from collections import defaultdict import markdown2 from typing import Dict, List -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta import pandas as pd import fitz @@ -646,13 +646,17 @@ def download_filing(self, equity_id: str, filing_type: str, after_date: str = No # Convert to string format expected by SEC EDGAR formatted_date = utc_date.strftime('%Y-%m-%d') + # Add one day + next_date = parsed_date + timedelta(days=1) + logger.info(f"Downloading {filing_type} for {equity_id} after {formatted_date}") num_downloaded_file = self.dl.get( filing_type, equity_id, limit=1, download_details=True, - after=formatted_date + after=formatted_date, + before = next_date ) if num_downloaded_file == 0: From dc5829c81c629ec5cc83a6ba40be0dcc4ca399dd Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Thu, 2 Jan 2025 08:24:29 -0400 Subject: [PATCH 263/820] FA-12 Manage Distribution Lists (#236) * FA-305 Create the Distribution Lists page * FA-305 Create the Distribution List page, fix responsiveness, make the right connections to user containers and fix small issues --- backend/app.py | 41 +++++- backend/shared/cosmo_db.py | 91 ++++++++++++ frontend/src/api/api.ts | 18 +++ frontend/src/components/Sidebar/Sidebar.tsx | 2 +- .../reports/DistributionLists.module.css | 133 ++++++++++++++++++ .../src/pages/reports/DistributionLists.tsx | 132 ++++++++++++++++- .../src/pages/reports/ReportManagement.tsx | 2 +- frontend/src/providers/AppProviders.tsx | 1 + 8 files changed, 414 insertions(+), 6 deletions(-) diff --git a/backend/app.py b/backend/app.py index a473894a..9d4712ee 100644 --- a/backend/app.py +++ b/backend/app.py @@ -51,9 +51,11 @@ from shared.cosmo_db import( create_report, get_report, + get_user_container, update_report, delete_report, - get_filtered_reports + get_filtered_reports, + update_user ) load_dotenv() @@ -764,7 +766,44 @@ def deleteReport(report_id): return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 +#Get User for email receivers +@app.route("/api/user/", methods=["GET"]) +def getUserid(user_id): + """ + Endpoint to get a user by ID. + """ + try: + user = get_user_container(user_id) + return jsonify(user), 200 + except NotFound as e: + logging.warning(f"Report with id {user_id} not found.") + return jsonify({"error": f"Report with this id {user_id} not found"}), 404 + except Exception as e: + logging.exception(f"An error occurred retrieving the report with id {user_id}") + return jsonify({"error": "Internal Server Error"}), 500 + +#Update Users +@app.route("/api/user/", methods=["PUT"]) +def updateUser(user_id): + """ + Endpoint to update a user + """ + try: + updated_data = request.get_json() + + if updated_data is None: + return jsonify({"error": "Invalid or missing JSON payload"}), 400 + + updated_data = update_user(user_id, updated_data) + return "", 204 + + except NotFound as e: + logging.warning(f"Tried to update a user that doesn't exist: {user_id}") + return jsonify({"error": f"Tried to update a user with this id {user_id} that does not exist"}), 404 + except Exception as e: + logging.exception(f"Error updating user with ID {user_id}") # Logs the full exception + return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 @app.route("/api/reports", methods=["GET"]) def getFilteredType(): diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index 7dcd7001..fc841280 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -11,6 +11,27 @@ AZURE_DB_NAME = os.environ.get("AZURE_DB_NAME") AZURE_DB_URI = f"https://{AZURE_DB_ID}.documents.azure.com:443/" +def get_cosmos_container_users(): + """ + Establishes the connection to the Cosmos DB `reports` container. + """ + credential = DefaultAzureCredential() + client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") + db = client.get_database_client(database=AZURE_DB_NAME) + container = db.get_container_client("users") + + try: + logging.info("Connection to Cosmos DB established successfully.") + return container + + except AzureError as az_err: + logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") + raise Exception(f"Azure connection error: {az_err}") from az_err + + except Exception as e: + logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") + raise Exception(f"Unexpected connection error: {e}") from e + def get_cosmos_container_report(): """ Establishes the connection to the Cosmos DB `reports` container. @@ -179,4 +200,74 @@ def delete_report(report_id): except Exception as e: logging.error(f"Error deleting report with id {report_id}: {e}") + raise + +def get_user_container(user_id): + """ + Retrieves a specific document (user_id) from the Cosmos DB container using its `id` as partition key. + + Parameters: + user_id (str): The ID of the user to retrieve. + + Returns: + dict: The user document retrieved from the database. + + Raises: + Exception: For any other unexpected error that occurs during retrieval. + CosmosResourceNotFoundError: If the user with the specified ID does not exist in the database. + """ + container = get_cosmos_container_users() + + try: + user = container.read_item(item=user_id, partition_key=user_id) + logging.info(f"User successfully retrieved: {user}") + return user + + except CosmosResourceNotFoundError: + logging.warning(f"Report with id '{user_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving report with id '{user_id}'") + raise + +def update_user(user_id, updated_data): + """ + Updates an existing document using its `id` as the partition key. + + Handles database errors and raises exceptions as needed. + """ + container = get_cosmos_container_users() + + try: + current_user = get_user_container(user_id) + + except CosmosResourceNotFoundError: + logging.warning(f"User with id '{user_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error while retrieving user with id '{user_id}'") + raise + + try: + current_user.update(updated_data) + + current_user["id"] = user_id + + # Perform the upsert operation + container.upsert_item(current_user) + logging.info(f"Report updated successfully: {current_user}") + return current_user + + except CosmosResourceNotFoundError: + logging.error(f"Failed to upsert item: Report ID '{user_id}' not found during upsert.") + raise NotFound(f"Cannot upsert report because it does not exist with id '{user_id}'") + + except AzureError as az_err: + logging.error(f"AzureError while performing upsert: {az_err}") + raise Exception("Error with Azure Cosmos DB operation.") from az_err + + except Exception as e: + logging.error(f"Unexpected error while updating report with id '{user_id}': {e}") raise \ No newline at end of file diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 8374ad18..25aa78d1 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -636,4 +636,22 @@ export async function deleteReport(reportId: string) { if (response.status > 299 || !response.ok) { throw Error(`Error deleting report with ID ${reportId}`); } +} + +export async function updateUser({ userId, updatedData }: { userId: string; updatedData: object }) { + const response = await fetch(`/api/user/${encodeURIComponent(userId)}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updatedData) + }); + + if (response.status === 404) { + throw Error(`User with ID ${userId} not found`); + } + + if (response.status > 299 || !response.ok) { + throw Error(`Error updating user with ID ${userId}`); + } } \ No newline at end of file diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index ebf07195..c317f63b 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -190,7 +190,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon: , to: "/details-settings", tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user"] + roles: ["admin"] } ] }, diff --git a/frontend/src/pages/reports/DistributionLists.module.css b/frontend/src/pages/reports/DistributionLists.module.css index 97f3e4ec..950e61aa 100644 --- a/frontend/src/pages/reports/DistributionLists.module.css +++ b/frontend/src/pages/reports/DistributionLists.module.css @@ -3,4 +3,137 @@ flex-direction: column; width: 100%; padding: 100px 100px 100px 150px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.table { + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +@media (max-width: 900px){ + .table{ + font-size: 0.7em; + } +} + +.title { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + justify-items: flex-start; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 10px 10px 30px 10px; + gap: 20px; + margin-top: 15px; + flex-grow: 2; +} + +.tableContainer{ + border-radius: 10px; + overflow: hidden; + overflow-x: auto; + width: 100%; + border: 1px solid #d9d9d9; + margin-top: 35px; +} + +@media (max-width: 500px) { + .tableContainer{ + word-wrap: break-word; + word-break: break-all; + white-space: normal; + width: 18em; + } +} + +.userBackground{ + background-color: #f8f8f8; +} + +.userBackgroundAlt{ + background-color: white; +} + +.nameElement{ + padding: 10px; + text-align: justify; +} + +.text{ + text-align: justify; +} + +.roleContainer{ + width: 100%; + justify-content: flex-start; + justify-items: center; + text-align: center; + display: flex; + text-transform: capitalize; +} + +.roleAdmin{ + width: 100px; + background-color: #d7e9f4; + padding: 5px; + color: #064789; + border-radius: 15px; +} + +.roleUser{ + width: 100px; + background-color: #d7e5be; + padding: 5px; + color: #1b4332; + border-radius: 15px; +} + +@media (max-width: 900px){ + .roleAdmin{ + width: 50px; + } + .roleUser{ + width: 50px; + } +} + +.checkbox{ + width: 15px; + height: 15px; + border: 2px solid #9fc51d; + background-color: #fff; + border-radius: 4px; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +.checkbox:checked{ + background-color: #a7c444; +} + +.thead{ + background-color: #a7c444; + color: white; } \ No newline at end of file diff --git a/frontend/src/pages/reports/DistributionLists.tsx b/frontend/src/pages/reports/DistributionLists.tsx index ceab37e9..c29a0daa 100644 --- a/frontend/src/pages/reports/DistributionLists.tsx +++ b/frontend/src/pages/reports/DistributionLists.tsx @@ -1,11 +1,137 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import styles from "./DistributionLists.module.css" +import { useAppContext } from "../../providers/AppProviders"; +import { getUsers, updateUser } from "../../api"; +import { Spinner } from "@fluentui/react"; const DistributionLists: React.FC = () => { + const { user } = useAppContext(); + const [filteredUsers, setFilteredUsers] = useState([]); + const [selectedUser, setSelectedUser] = useState({ + id: "", + data: { + name: "", + email: "", + role: "" + } + }); + + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [dataLoad, setDataLoad] = useState(false) + + useEffect(() => { + const getUserList = async () => { + if (!user) { + setUsers([]); + setFilteredUsers([]); + setLoading(false); + return; + } + + setLoading(true); + + try { + let usersList = await getUsers({ + user: { + id: user.id, + name: user.name, + organizationId: user.organizationId, + isReportEmailReceiver: user.isReportEmailReceiver + } + }); + + if (!Array.isArray(usersList)) { + usersList = []; + } + + setUsers(usersList); + setFilteredUsers(usersList); + } catch (error) { + console.error("Error fetching user list:", error); + setUsers([]); + setFilteredUsers([]); + } finally { + setLoading(false); + } + }; + + getUserList(); + }, [dataLoad]); + + const handleCheckbox = async (userID: string, IsEmailReceiver: string | boolean) => { + const newValue = IsEmailReceiver === "true" ? "false" : "true"; + try{ + await updateUser({ + userId: userID, + updatedData: { isReportEmailReceiver: newValue } + }); + setDataLoad(!dataLoad) + } catch (error){ + console.error("Error trying to update the state: ", error) + } + + } + return (

    -

    Distribution Lists

    -

    Welcome to the Distribution Lists page!

    +
    +

    Distribution Lists

    +

    Manage the organization email flow

    +
    +
    +
    + {loading ? ( + + ) : ( + + + + + + + + + + + {filteredUsers.map((user: any, index) => { + return ( + + + + + + + ); + })} + +
    + Name + EmailRoleEmail Receiver
    + {user.data.name} + + {user.data.email} + +
    +
    + {user.data.role} +
    +
    +
    + { +
    + handleCheckbox(user.id, user.isReportEmailReceiver)}> +
    + } +
    + )} +
    +
    ); }; diff --git a/frontend/src/pages/reports/ReportManagement.tsx b/frontend/src/pages/reports/ReportManagement.tsx index 58dff77a..597ea9fa 100644 --- a/frontend/src/pages/reports/ReportManagement.tsx +++ b/frontend/src/pages/reports/ReportManagement.tsx @@ -67,7 +67,7 @@ const ReportManagement: React.FC = () => { }; return ( -
    +

    Report Management

    diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index 2e414def..f30ee3dd 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -17,6 +17,7 @@ interface UserInfo { email: string | null; role?: Role; organizationId?: string; + isReportEmailReceiver?: boolean; } interface OrganizationInfo { From faf257ad3595b235bbb65d35f1e71047c06093eb Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 2 Jan 2025 08:45:56 -0500 Subject: [PATCH 264/820] Fa 321 email distribution endpoint (#240) * create monht_year folder for weekly curation report * Create an endpoint distribution email --- .env.template | 8 +- backend/app.py | 113 ++++++++++++++++++++++++++++- backend/financial_doc_processor.py | 2 +- backend/utils.py | 63 +++++++++++++++- 4 files changed, 181 insertions(+), 5 deletions(-) diff --git a/.env.template b/.env.template index 8711d3bb..edd57e22 100644 --- a/.env.template +++ b/.env.template @@ -10,4 +10,10 @@ SPEECH_SYNTHESIS_LANGUAGE="UPDATE_WITH_TTS_LANGUAGE._Example:_es-MX" SPEECH_SYNTHESIS_VOICE_NAME="UPDATE_WITH_TTS_NEURAL_LANGUAGE._Example:_es-MX-BeatrizNeural" # document storage (to show sources in browser) -STORAGE_ACCOUNT="UPDATE_WITH_STORAGE_ACCOUNT_NAME" \ No newline at end of file +STORAGE_ACCOUNT="UPDATE_WITH_STORAGE_ACCOUNT_NAME" + +############## EMAIL CONFIGURATION ############## +EMAIL_SMTP_SERVER="smtp.gmail.com" +EMAIL_SMTP_PORT=587 +EMAIL_USER_NAME="your-email@gmail.com" +EMAIL_USER_PASSWORD="your-app-specific-password" # Gmail App Password, not your regular Gmail password diff --git a/backend/app.py b/backend/app.py index 9d4712ee..024341af 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2677,12 +2677,16 @@ def generate_report(): logger.info("Converting markdown to html") markdown_to_html(report['final_report'], str(file_path)) - # Upload to blob storage logger.info("Uploading to blob storage") blob_storage_manager = BlobStorageManager() + if report_topic_rqst in WEEKLY_CURATION_REPORT: + blob_folder = f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}" + else: + blob_folder = f"Reports/Curation_Reports/{report_topic_rqst}" + upload_result = blob_storage_manager.upload_to_blob( file_path=str(file_path), - blob_folder=f"Reports/Curation_Reports/{report_topic_rqst}" + blob_folder=blob_folder ) # Cleanup files @@ -2716,6 +2720,111 @@ def generate_report(): logger.error(f"Unexpected error during report generation: {str(e)}", exc_info=True) return jsonify({'error': 'An unexpected error occurred while generating the report'}), 500 +from utils import EmailServiceError, EmailService +@app.route('/api/reports/email', methods=['POST']) +def send_email_endpoint(): + """Send an email with optional attachments. + + Expected JSON payload: + { + "subject": "Email subject", + "html_content": "HTML formatted content", + "recipients": ["email1@domain.com", "email2@domain.com"], + "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes + } + + Returns: + JSON response indicating success/failure + """ + try: + # Get and validate request data + data = request.get_json() + if not data: + return jsonify({ + 'status': 'error', + 'message': 'No JSON data provided' + }), 400 + + # Validate required fields + required_fields = {'subject', 'html_content', 'recipients'} + missing_fields = required_fields - set(data.keys()) + if missing_fields: + return jsonify({ + 'status': 'error', + 'message': f'Missing required fields: {", ".join(missing_fields)}' + }), 400 + + # Validate recipients format + if not isinstance(data['recipients'], list): + return jsonify({ + 'status': 'error', + 'message': 'Recipients must be provided as a list' + }), 400 + + if not data['recipients']: + return jsonify({ + 'status': 'error', + 'message': 'At least one recipient is required' + }), 400 + + # Validate attachment path if provided + attachment_path = data.get('attachment_path') + if attachment_path: + # Convert Windows path to proper format + attachment_path = Path(attachment_path.replace('\\', '/')).resolve() + if not attachment_path.exists(): + return jsonify({ + 'status': 'error', + 'message': f'Attachment file not found: {attachment_path}' + }), 400 + + # Update the attachment_path in data + data['attachment_path'] = str(attachment_path) + + # Validate email configuration + email_config = { + 'smtp_server': os.getenv('EMAIL_SMTP_SERVER'), + 'smtp_port': os.getenv('EMAIL_SMTP_PORT'), + 'username': os.getenv('EMAIL_USER_NAME'), + 'password': os.getenv('EMAIL_USER_PASSWORD') + } + + if not all(email_config.values()): + logger.error("Missing email configuration environment variables") + return jsonify({ + 'status': 'error', + 'message': 'Email service configuration error' + }), 500 + + # Initialize and send email + email_service = EmailService(**email_config) + email_service.send_email( + subject=data['subject'], + html_content=data['html_content'], + recipients=data['recipients'], + attachment_path=data.get('attachment_path') + ) + + return jsonify({ + 'status': 'success', + 'message': 'Email sent successfully' + }), 200 + + except EmailServiceError as e: + logger.error(f"Email service error: {str(e)}") + return jsonify({ + 'status': 'error', + 'message': f'Failed to send email: {str(e)}' + }), 500 + + except Exception as e: + logger.exception("Unexpected error in send_email_endpoint") + return jsonify({ + 'status': 'error', + 'message': f'An unexpected error occurred: {str(e)}' + }), 500 + + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index e99e3374..0771ca17 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -496,7 +496,7 @@ def get_document_metadata(self, remote_file_path: str) -> dict: # make sure the document_paths is a dict with the structure of create_document_paths - def upload_to_blob(self, document_paths: dict, metadata: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: + def upload_to_blob(self, document_paths: dict = None, metadata: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: """ Upload files to Azure Blob Storage. Can handle either a document_paths dictionary or a single file path. diff --git a/backend/utils.py b/backend/utils.py index f81adeab..2ee14116 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -319,4 +319,65 @@ def _extract_response_data(response): """Helper function to extract JSON data from response objects""" if isinstance(response, tuple): return response[0].get_json() - return response.get_json() \ No newline at end of file + return response.get_json() + +################################################ +# Email distribution Utils +################################################ +from typing import List +from email.message import EmailMessage +import smtplib +from dotenv import load_dotenv +load_dotenv() + +class EmailServiceError(Exception): + """Base exception for email service errors""" + pass + +class EmailService: + def __init__( + self, + smtp_server: str, + smtp_port: int, + username: str, + password: str, + ): + self.smtp_server = smtp_server + self.smtp_port = smtp_port + self.username = username + self.password = password + + def send_email(self, + subject: str, + html_content: str, + recipients: List[str], + attachment_path: Optional[str] = None) -> None: + + """ + # TODO: provide docstring + """ + + msg: EmailMessage = EmailMessage() + msg['Subject'] = subject + msg['From'] = self.username + msg['Bcc'] = ','.join(recipients) + msg.add_alternative(html_content, subtype='html') + + if attachment_path: + with open(attachment_path, 'rb') as file: + file_data = file.read() + file_name = attachment_path.split('\\')[-1] + msg.add_attachment(file_data, + maintype='application', + subtype='octet-stream', + filename=file_name) + + try: + with smtplib.SMTP(self.smtp_server, self.smtp_port) as server: + server.starttls() + server.login(self.username, self.password) + server.send_message(msg) + logger.info(f'Email sent to {recipients}') + except Exception as e: + logger.error(f'Failed to send email: {str(e)}') + raise EmailServiceError(f'Failed to send email: {str(e)}') From b2e6066039250e966d7a560dcfdfab568614e641 Mon Sep 17 00:00:00 2001 From: Victor Maldonado Date: Thu, 2 Jan 2025 14:06:58 -0400 Subject: [PATCH 265/820] FA-312 implement api for adding and removing summarization reports (#235) * add endpoint for summarization report templates and error handling * refactor cosmos db connection functions and add template management methods * add endpoints for creating and deleting summarization report templates with error handling * add delete_template function to remove summarization report templates * refactor removeSummarizationReport to return deletion result from delete_template * add endpoints to retrieve summarization report templates by ID and list all templates * add API functions for managing summarization report templates in the frontend * remove unnecessary call to get_templates in cosmo_db.py * fix: function to retrieve summarization report template by ID in frontend * fix: update API endpoints for creating and deleting summarization report templates * fix: add companyTickers field to summarization report template * refactor: update datetime handling to use UTC instead of deprecated method utcnow --- backend/app.py | 104 ++++++++++++++++++++++++++++- backend/shared/cosmo_db.py | 133 ++++++++++++++++++++++++++----------- backend/utils.py | 14 ++++ frontend/src/api/api.ts | 46 ++++++++++++- frontend/src/api/models.ts | 6 ++ 5 files changed, 263 insertions(+), 40 deletions(-) diff --git a/backend/app.py b/backend/app.py index 024341af..8d762b95 100644 --- a/backend/app.py +++ b/backend/app.py @@ -25,7 +25,7 @@ import uuid from identity.flask import Auth -from datetime import timedelta, datetime +from datetime import timedelta, datetime, UTC import smtplib from email.mime.text import MIMEText @@ -44,6 +44,9 @@ SubscriptionError, InvalidSubscriptionError, InvalidFinancialPriceError, + InvalidParameterError, + MissingJSONPayloadError, + MissingRequiredFieldError, require_client_principal, ) import stripe.error @@ -55,6 +58,10 @@ update_report, delete_report, get_filtered_reports, + create_template, + delete_template, + get_templates, + get_template_by_ID, update_user ) @@ -226,7 +233,7 @@ def check_user_authorization( logger.info( f"[auth] Checking authorization for user {client_principal_id} " - f"with email {email} at {datetime.utcnow().isoformat()}" + f"with email {email} at {datetime.now(UTC).isoformat()}" ) # Make the request using a session for better performance @@ -828,6 +835,99 @@ def getFilteredType(): logging.exception(f"Error retrieving reports.") return jsonify({"error": "Internal Server Error"}), 500 +@app.route("/api/reports/summarization/templates", methods=["POST"]) +def addSummarizationReport(): + """ + Endpoint to add a summarization report template. + + This endpoint expects a JSON payload with the following fields: + - name: The name of the report template. Must be one of ["10-K", "10-Q", "8-K", "DEF 14A"]. + - description: A description of the report template. + + MissingJSONPayloadError: If the JSON payload is missing. + MissingRequiredFieldError: If the 'name' or 'description' field is missing. + InvalidParameterError: If the 'name' field is not one of the valid names. + + JSON response with the created report template if successful. + JSON error response with appropriate HTTP status code if an error occurs. + """ + try: + data = request.get_json() + if not data: + raise MissingJSONPayloadError('Missing JSON payload') + if not "name" in data: + raise MissingRequiredFieldError('name') + if not "description" in data: + raise MissingRequiredFieldError('description') + if not "companyTickers" in data: + raise MissingRequiredFieldError('companyTickers') + valid_names=["10-K", "10-Q", "8-K", "DEF 14A"] + if not data["name"] in valid_names: + raise InvalidParameterError('name', f"Must be one of: {', '.join(valid_names)}") + new_template = {'name': data['name'], 'companyTickers': data['companyTickers'], 'description': data['description'], 'status': 'active', 'type': 'summarization'} + # add to cosmosDB container + result = create_template(new_template) + return create_success_response(result) + except MissingJSONPayloadError as e: + return create_error_response("Invalid or Missing JSON payload", HTTPStatus.BAD_REQUEST) + except MissingRequiredFieldError as field: + return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) + except InvalidParameterError as e: + return create_error_response(str(e), HTTPStatus.BAD_REQUEST) + except Exception as e: + logging.exception(e) + return jsonify({"error": "An unexpected error occurred. Please try again later."}), HTTPStatus.INTERNAL_SERVER_ERROR + + +@app.route('/api/reports/summarization/templates/', methods=['DELETE']) +def removeSummarizationReport(template_id): + """ + Endpoint to remove a summarization report template by ID. + + This endpoint expects the following URL parameter: + - template_id: The ID of the report template to be removed. + + NotFound: If the report template with the specified ID does not exist. + Exception: For any other unexpected errors. + + JSON response with appropriate HTTP status code: + - 204 No Content: If the report template is successfully deleted. + - 404 Not Found: If the report template with the specified ID does not exist. + - 500 Internal Server Error: If an unexpected error occurs. + """ + try: + if not template_id: + raise MissingRequiredFieldError('template_id') + #delete from cosmosDB container + result = delete_template(template_id) + return create_success_response(result) + except NotFound as e: + return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) + except MissingRequiredFieldError as field: + return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) + except Exception as e: + return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + +@app.route('/api/reports/summarization/templates/', methods=['GET']) +def getSummarizationReports(): + try: + result = get_templates() + return create_success_response(result) + except Exception as e: + return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + +@app.route('/api/reports/summarization/templates/', methods=['GET']) +def getSummarizationReport(template_id): + try: + result = get_template_by_ID(template_id) + return create_success_response(result) + except NotFound as e: + return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) + except Exception as e: + return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + + + # methods to provide access to speech services and blob storage account blobs diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index fc841280..c3fdf7aa 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -4,53 +4,32 @@ from azure.cosmos.exceptions import CosmosResourceNotFoundError, AzureError import uuid import logging -from datetime import datetime +from datetime import datetime, UTC from werkzeug.exceptions import NotFound AZURE_DB_ID = os.environ.get("AZURE_DB_ID") AZURE_DB_NAME = os.environ.get("AZURE_DB_NAME") AZURE_DB_URI = f"https://{AZURE_DB_ID}.documents.azure.com:443/" -def get_cosmos_container_users(): +def get_cosmos_container(container_name): """ - Establishes the connection to the Cosmos DB `reports` container. + Establishes the connection to the Cosmos DB container specified by `container_name`. """ credential = DefaultAzureCredential() client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") db = client.get_database_client(database=AZURE_DB_NAME) - container = db.get_container_client("users") + container = db.get_container_client(container_name) try: - logging.info("Connection to Cosmos DB established successfully.") + logging.info(f"Connection to Cosmos DB container '{container_name}' established successfully.") return container except AzureError as az_err: - logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") + logging.error(f"AzureError encountered while connecting to Cosmos DB container '{container_name}': {az_err}") raise Exception(f"Azure connection error: {az_err}") from az_err except Exception as e: - logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") - raise Exception(f"Unexpected connection error: {e}") from e - -def get_cosmos_container_report(): - """ - Establishes the connection to the Cosmos DB `reports` container. - """ - credential = DefaultAzureCredential() - client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") - db = client.get_database_client(database=AZURE_DB_NAME) - container = db.get_container_client("reports") - - try: - logging.info("Connection to Cosmos DB established successfully.") - return container - - except AzureError as az_err: - logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") - raise Exception(f"Azure connection error: {az_err}") from az_err - - except Exception as e: - logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") + logging.error(f"Unexpected error while connecting to Cosmos DB container '{container_name}': {e}") raise Exception(f"Unexpected connection error: {e}") from e def create_report(data): @@ -58,10 +37,10 @@ def create_report(data): Creates a new document in the container. """ try: - container = get_cosmos_container_report() + container = get_cosmos_container("reports") data["id"] = str(uuid.uuid4()) - data["createAt"] = datetime.utcnow().isoformat() + "Z" - data["updatedAt"] = datetime.utcnow().isoformat() + "Z" + data["createAt"] = datetime.now(UTC).isoformat() + "Z" + data["updatedAt"] = datetime.now(UTC).isoformat() + "Z" container.upsert_item(data) logging.info(f"Document created: {data}") return data @@ -83,7 +62,8 @@ def get_report(report_id): Exception: For any other unexpected error that occurs during retrieval. CosmosResourceNotFoundError: If the report with the specified ID does not exist in the database. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") + try: report = container.read_item(item=report_id, partition_key=report_id) @@ -112,7 +92,7 @@ def get_filtered_reports(report_type=None): CosmosResourceNotFoundError: If no reports with the specified type are found (when filtered). Exception: For any other unexpected error that occurs during retrieval. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") if report_type: query = "SELECT * FROM c WHERE c.type = @type" parameters = [{"name": "@type", "value": report_type}] @@ -148,7 +128,7 @@ def update_report(report_id, updated_data): Handles database errors and raises exceptions as needed. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") try: current_report = get_report(report_id) @@ -187,7 +167,7 @@ def delete_report(report_id): """ Deletes a specific document using its `id` as partition key. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") try: container.delete_item(item=report_id, partition_key=report_id) @@ -202,6 +182,85 @@ def delete_report(report_id): logging.error(f"Error deleting report with id {report_id}: {e}") raise +# Template management + +def create_template(data): + """ + Creates a new document in the container. + """ + try: + container = get_cosmos_container("templates") + data["id"] = str(uuid.uuid4()) + data["createAt"] = datetime.now(UTC).isoformat() + "Z" + data["updatedAt"] = datetime.now(UTC).isoformat() + "Z" + container.upsert_item(data) + logging.info(f"Document created: {data}") + return data + except Exception as e: + logging.error(f"Error inserting data into Cosmos DB: {e}") + raise + +def delete_template(template_id): + """ + Deletes a specific document using its `id` as partition key. + """ + container = get_cosmos_container("templates") + + try: + container.delete_item(item=template_id, partition_key=template_id) + logging.info(f"Template with id {template_id} deleted successfully.") + return {"message": f"Template with id {template_id} deleted successfully."} + + except CosmosResourceNotFoundError: + logging.warning(f"Template with id '{template_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Error deleting template with id {template_id}: {e}") + raise + +def get_templates(): + """Get all the templates in a cosmosDB container""" + container = get_cosmos_container("templates") + try: + items = list(container.query_items( + query="SELECT * FROM c", + enable_cross_partition_query=True + )) + + if not items: + logging.warning(f"No templates found.") + raise NotFound + + logging.info(f"Templates successfully retrieved: {items}") + print(items) + return items + + except CosmosResourceNotFoundError: + logging.warning(f"No templates found.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving templates: {e}") + raise + + +def get_template_by_ID(template_id): + """Get a template by its ID""" + container = get_cosmos_container("templates") + try: + template = container.read_item(item=template_id, partition_key=template_id) + logging.info(f"Template successfully retrieved: {template}") + return template + + except CosmosResourceNotFoundError: + logging.warning(f"Template with id '{template_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving template with id '{template_id}': {e}") + + def get_user_container(user_id): """ Retrieves a specific document (user_id) from the Cosmos DB container using its `id` as partition key. @@ -216,7 +275,7 @@ def get_user_container(user_id): Exception: For any other unexpected error that occurs during retrieval. CosmosResourceNotFoundError: If the user with the specified ID does not exist in the database. """ - container = get_cosmos_container_users() + container = get_cosmos_container("users") try: user = container.read_item(item=user_id, partition_key=user_id) @@ -237,7 +296,7 @@ def update_user(user_id, updated_data): Handles database errors and raises exceptions as needed. """ - container = get_cosmos_container_users() + container = get_cosmos_container("users") try: current_user = get_user_container(user_id) diff --git a/backend/utils.py b/backend/utils.py index 2ee14116..25c4e75d 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -43,6 +43,20 @@ class InvalidSubscriptionError(SubscriptionError): pass +class MissingJSONPayloadError(Exception): + """Raised when JSON payload is missing""" + + pass + +class MissingRequiredFieldError(Exception): + """Raised when a required field is missing""" + + pass + +class InvalidParameterError(Exception): + """Raised when an invalid parameter is provided""" + + pass # Security: Decorator to ensure client principal ID is present diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 25aa78d1..c14e1e0d 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,4 +1,4 @@ -import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo } from "./models"; +import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo, SummarizationReportProps } from "./models"; export async function getUsers({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; @@ -621,6 +621,50 @@ export async function getFilteredReports(type?: string) { return reports; } +// Summarization reports +export async function getSummarizationTemplates() { + const response = await fetch('/api/reports/summarization/templates', {method: 'GET', headers: {'Content-Type': 'application/json'}}); + if (response.status > 299 || !response.ok) { + throw Error('Error getting summarization templates'); + } + const reports = await response.json(); + return reports; +} + +export async function getSummarizationReportTemplateByID(templateID: string) { + const response = await fetch(`/api/reports/summarization/templates/${templateID}`, {method: 'GET', headers: {'Content-Type': 'application/json'}}); + const report = await response.json(); + return report; +} + +export async function createSummarizationReport(templateData: SummarizationReportProps) { + const response = await fetch('/api/reports/summarization/templates', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(templateData), + }); + + if (response.status > 299 || !response.ok) { + throw Error('Error creating a new summarization report'); + } + + const newReport = await response.json(); + return newReport; +} + +export async function deleteSumarizationReportTemplate(templateID: string) { + const response = await fetch(`/api/reports/summarization/templates/${templateID}`, { + method: 'DELETE', + headers: {'Content-Type': 'application/json'}, + }); + + if (response.status > 299 || !response.ok) { + throw Error('Error deleting summarization report'); + } + const deletedReport = await response.json(); + return deletedReport; +} + export async function deleteReport(reportId: string) { const response = await fetch(`/api/reports/${encodeURIComponent(reportId)}`, { method: "DELETE", diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index f5bf5999..420d88c5 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -105,3 +105,9 @@ export type PostSettingsProps = { } | null; temperature: number; }; + +export type SummarizationReportProps = { + name: string; + description: string; + companyTickers: string[]; +} \ No newline at end of file From 4dd0ded04c510208ae536fab6c418e97c06f28b0 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Thu, 2 Jan 2025 15:45:44 -0400 Subject: [PATCH 266/820] =?UTF-8?q?Revert=20"FA-312=20implement=20api=20fo?= =?UTF-8?q?r=20adding=20and=20removing=20summarization=20reports=20(#?= =?UTF-8?q?=E2=80=A6"=20(#241)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit b2e6066039250e966d7a560dcfdfab568614e641. --- backend/app.py | 104 +---------------------------- backend/shared/cosmo_db.py | 133 +++++++++++-------------------------- backend/utils.py | 14 ---- frontend/src/api/api.ts | 46 +------------ frontend/src/api/models.ts | 6 -- 5 files changed, 40 insertions(+), 263 deletions(-) diff --git a/backend/app.py b/backend/app.py index 8d762b95..024341af 100644 --- a/backend/app.py +++ b/backend/app.py @@ -25,7 +25,7 @@ import uuid from identity.flask import Auth -from datetime import timedelta, datetime, UTC +from datetime import timedelta, datetime import smtplib from email.mime.text import MIMEText @@ -44,9 +44,6 @@ SubscriptionError, InvalidSubscriptionError, InvalidFinancialPriceError, - InvalidParameterError, - MissingJSONPayloadError, - MissingRequiredFieldError, require_client_principal, ) import stripe.error @@ -58,10 +55,6 @@ update_report, delete_report, get_filtered_reports, - create_template, - delete_template, - get_templates, - get_template_by_ID, update_user ) @@ -233,7 +226,7 @@ def check_user_authorization( logger.info( f"[auth] Checking authorization for user {client_principal_id} " - f"with email {email} at {datetime.now(UTC).isoformat()}" + f"with email {email} at {datetime.utcnow().isoformat()}" ) # Make the request using a session for better performance @@ -835,99 +828,6 @@ def getFilteredType(): logging.exception(f"Error retrieving reports.") return jsonify({"error": "Internal Server Error"}), 500 -@app.route("/api/reports/summarization/templates", methods=["POST"]) -def addSummarizationReport(): - """ - Endpoint to add a summarization report template. - - This endpoint expects a JSON payload with the following fields: - - name: The name of the report template. Must be one of ["10-K", "10-Q", "8-K", "DEF 14A"]. - - description: A description of the report template. - - MissingJSONPayloadError: If the JSON payload is missing. - MissingRequiredFieldError: If the 'name' or 'description' field is missing. - InvalidParameterError: If the 'name' field is not one of the valid names. - - JSON response with the created report template if successful. - JSON error response with appropriate HTTP status code if an error occurs. - """ - try: - data = request.get_json() - if not data: - raise MissingJSONPayloadError('Missing JSON payload') - if not "name" in data: - raise MissingRequiredFieldError('name') - if not "description" in data: - raise MissingRequiredFieldError('description') - if not "companyTickers" in data: - raise MissingRequiredFieldError('companyTickers') - valid_names=["10-K", "10-Q", "8-K", "DEF 14A"] - if not data["name"] in valid_names: - raise InvalidParameterError('name', f"Must be one of: {', '.join(valid_names)}") - new_template = {'name': data['name'], 'companyTickers': data['companyTickers'], 'description': data['description'], 'status': 'active', 'type': 'summarization'} - # add to cosmosDB container - result = create_template(new_template) - return create_success_response(result) - except MissingJSONPayloadError as e: - return create_error_response("Invalid or Missing JSON payload", HTTPStatus.BAD_REQUEST) - except MissingRequiredFieldError as field: - return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) - except InvalidParameterError as e: - return create_error_response(str(e), HTTPStatus.BAD_REQUEST) - except Exception as e: - logging.exception(e) - return jsonify({"error": "An unexpected error occurred. Please try again later."}), HTTPStatus.INTERNAL_SERVER_ERROR - - -@app.route('/api/reports/summarization/templates/', methods=['DELETE']) -def removeSummarizationReport(template_id): - """ - Endpoint to remove a summarization report template by ID. - - This endpoint expects the following URL parameter: - - template_id: The ID of the report template to be removed. - - NotFound: If the report template with the specified ID does not exist. - Exception: For any other unexpected errors. - - JSON response with appropriate HTTP status code: - - 204 No Content: If the report template is successfully deleted. - - 404 Not Found: If the report template with the specified ID does not exist. - - 500 Internal Server Error: If an unexpected error occurs. - """ - try: - if not template_id: - raise MissingRequiredFieldError('template_id') - #delete from cosmosDB container - result = delete_template(template_id) - return create_success_response(result) - except NotFound as e: - return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) - except MissingRequiredFieldError as field: - return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) - except Exception as e: - return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) - -@app.route('/api/reports/summarization/templates/', methods=['GET']) -def getSummarizationReports(): - try: - result = get_templates() - return create_success_response(result) - except Exception as e: - return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) - -@app.route('/api/reports/summarization/templates/', methods=['GET']) -def getSummarizationReport(template_id): - try: - result = get_template_by_ID(template_id) - return create_success_response(result) - except NotFound as e: - return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) - except Exception as e: - return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) - - - # methods to provide access to speech services and blob storage account blobs diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index c3fdf7aa..fc841280 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -4,32 +4,53 @@ from azure.cosmos.exceptions import CosmosResourceNotFoundError, AzureError import uuid import logging -from datetime import datetime, UTC +from datetime import datetime from werkzeug.exceptions import NotFound AZURE_DB_ID = os.environ.get("AZURE_DB_ID") AZURE_DB_NAME = os.environ.get("AZURE_DB_NAME") AZURE_DB_URI = f"https://{AZURE_DB_ID}.documents.azure.com:443/" -def get_cosmos_container(container_name): +def get_cosmos_container_users(): """ - Establishes the connection to the Cosmos DB container specified by `container_name`. + Establishes the connection to the Cosmos DB `reports` container. """ credential = DefaultAzureCredential() client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") db = client.get_database_client(database=AZURE_DB_NAME) - container = db.get_container_client(container_name) + container = db.get_container_client("users") try: - logging.info(f"Connection to Cosmos DB container '{container_name}' established successfully.") + logging.info("Connection to Cosmos DB established successfully.") return container except AzureError as az_err: - logging.error(f"AzureError encountered while connecting to Cosmos DB container '{container_name}': {az_err}") + logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") raise Exception(f"Azure connection error: {az_err}") from az_err except Exception as e: - logging.error(f"Unexpected error while connecting to Cosmos DB container '{container_name}': {e}") + logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") + raise Exception(f"Unexpected connection error: {e}") from e + +def get_cosmos_container_report(): + """ + Establishes the connection to the Cosmos DB `reports` container. + """ + credential = DefaultAzureCredential() + client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") + db = client.get_database_client(database=AZURE_DB_NAME) + container = db.get_container_client("reports") + + try: + logging.info("Connection to Cosmos DB established successfully.") + return container + + except AzureError as az_err: + logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") + raise Exception(f"Azure connection error: {az_err}") from az_err + + except Exception as e: + logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") raise Exception(f"Unexpected connection error: {e}") from e def create_report(data): @@ -37,10 +58,10 @@ def create_report(data): Creates a new document in the container. """ try: - container = get_cosmos_container("reports") + container = get_cosmos_container_report() data["id"] = str(uuid.uuid4()) - data["createAt"] = datetime.now(UTC).isoformat() + "Z" - data["updatedAt"] = datetime.now(UTC).isoformat() + "Z" + data["createAt"] = datetime.utcnow().isoformat() + "Z" + data["updatedAt"] = datetime.utcnow().isoformat() + "Z" container.upsert_item(data) logging.info(f"Document created: {data}") return data @@ -62,8 +83,7 @@ def get_report(report_id): Exception: For any other unexpected error that occurs during retrieval. CosmosResourceNotFoundError: If the report with the specified ID does not exist in the database. """ - container = get_cosmos_container("reports") - + container = get_cosmos_container_report() try: report = container.read_item(item=report_id, partition_key=report_id) @@ -92,7 +112,7 @@ def get_filtered_reports(report_type=None): CosmosResourceNotFoundError: If no reports with the specified type are found (when filtered). Exception: For any other unexpected error that occurs during retrieval. """ - container = get_cosmos_container("reports") + container = get_cosmos_container_report() if report_type: query = "SELECT * FROM c WHERE c.type = @type" parameters = [{"name": "@type", "value": report_type}] @@ -128,7 +148,7 @@ def update_report(report_id, updated_data): Handles database errors and raises exceptions as needed. """ - container = get_cosmos_container("reports") + container = get_cosmos_container_report() try: current_report = get_report(report_id) @@ -167,7 +187,7 @@ def delete_report(report_id): """ Deletes a specific document using its `id` as partition key. """ - container = get_cosmos_container("reports") + container = get_cosmos_container_report() try: container.delete_item(item=report_id, partition_key=report_id) @@ -182,85 +202,6 @@ def delete_report(report_id): logging.error(f"Error deleting report with id {report_id}: {e}") raise -# Template management - -def create_template(data): - """ - Creates a new document in the container. - """ - try: - container = get_cosmos_container("templates") - data["id"] = str(uuid.uuid4()) - data["createAt"] = datetime.now(UTC).isoformat() + "Z" - data["updatedAt"] = datetime.now(UTC).isoformat() + "Z" - container.upsert_item(data) - logging.info(f"Document created: {data}") - return data - except Exception as e: - logging.error(f"Error inserting data into Cosmos DB: {e}") - raise - -def delete_template(template_id): - """ - Deletes a specific document using its `id` as partition key. - """ - container = get_cosmos_container("templates") - - try: - container.delete_item(item=template_id, partition_key=template_id) - logging.info(f"Template with id {template_id} deleted successfully.") - return {"message": f"Template with id {template_id} deleted successfully."} - - except CosmosResourceNotFoundError: - logging.warning(f"Template with id '{template_id}' not found in Cosmos DB.") - raise NotFound - - except Exception as e: - logging.error(f"Error deleting template with id {template_id}: {e}") - raise - -def get_templates(): - """Get all the templates in a cosmosDB container""" - container = get_cosmos_container("templates") - try: - items = list(container.query_items( - query="SELECT * FROM c", - enable_cross_partition_query=True - )) - - if not items: - logging.warning(f"No templates found.") - raise NotFound - - logging.info(f"Templates successfully retrieved: {items}") - print(items) - return items - - except CosmosResourceNotFoundError: - logging.warning(f"No templates found.") - raise NotFound - - except Exception as e: - logging.error(f"Unexpected error retrieving templates: {e}") - raise - - -def get_template_by_ID(template_id): - """Get a template by its ID""" - container = get_cosmos_container("templates") - try: - template = container.read_item(item=template_id, partition_key=template_id) - logging.info(f"Template successfully retrieved: {template}") - return template - - except CosmosResourceNotFoundError: - logging.warning(f"Template with id '{template_id}' not found in Cosmos DB.") - raise NotFound - - except Exception as e: - logging.error(f"Unexpected error retrieving template with id '{template_id}': {e}") - - def get_user_container(user_id): """ Retrieves a specific document (user_id) from the Cosmos DB container using its `id` as partition key. @@ -275,7 +216,7 @@ def get_user_container(user_id): Exception: For any other unexpected error that occurs during retrieval. CosmosResourceNotFoundError: If the user with the specified ID does not exist in the database. """ - container = get_cosmos_container("users") + container = get_cosmos_container_users() try: user = container.read_item(item=user_id, partition_key=user_id) @@ -296,7 +237,7 @@ def update_user(user_id, updated_data): Handles database errors and raises exceptions as needed. """ - container = get_cosmos_container("users") + container = get_cosmos_container_users() try: current_user = get_user_container(user_id) diff --git a/backend/utils.py b/backend/utils.py index 25c4e75d..2ee14116 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -43,20 +43,6 @@ class InvalidSubscriptionError(SubscriptionError): pass -class MissingJSONPayloadError(Exception): - """Raised when JSON payload is missing""" - - pass - -class MissingRequiredFieldError(Exception): - """Raised when a required field is missing""" - - pass - -class InvalidParameterError(Exception): - """Raised when an invalid parameter is provided""" - - pass # Security: Decorator to ensure client principal ID is present diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index c14e1e0d..25aa78d1 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,4 +1,4 @@ -import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo, SummarizationReportProps } from "./models"; +import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo } from "./models"; export async function getUsers({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; @@ -621,50 +621,6 @@ export async function getFilteredReports(type?: string) { return reports; } -// Summarization reports -export async function getSummarizationTemplates() { - const response = await fetch('/api/reports/summarization/templates', {method: 'GET', headers: {'Content-Type': 'application/json'}}); - if (response.status > 299 || !response.ok) { - throw Error('Error getting summarization templates'); - } - const reports = await response.json(); - return reports; -} - -export async function getSummarizationReportTemplateByID(templateID: string) { - const response = await fetch(`/api/reports/summarization/templates/${templateID}`, {method: 'GET', headers: {'Content-Type': 'application/json'}}); - const report = await response.json(); - return report; -} - -export async function createSummarizationReport(templateData: SummarizationReportProps) { - const response = await fetch('/api/reports/summarization/templates', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(templateData), - }); - - if (response.status > 299 || !response.ok) { - throw Error('Error creating a new summarization report'); - } - - const newReport = await response.json(); - return newReport; -} - -export async function deleteSumarizationReportTemplate(templateID: string) { - const response = await fetch(`/api/reports/summarization/templates/${templateID}`, { - method: 'DELETE', - headers: {'Content-Type': 'application/json'}, - }); - - if (response.status > 299 || !response.ok) { - throw Error('Error deleting summarization report'); - } - const deletedReport = await response.json(); - return deletedReport; -} - export async function deleteReport(reportId: string) { const response = await fetch(`/api/reports/${encodeURIComponent(reportId)}`, { method: "DELETE", diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 420d88c5..f5bf5999 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -105,9 +105,3 @@ export type PostSettingsProps = { } | null; temperature: number; }; - -export type SummarizationReportProps = { - name: string; - description: string; - companyTickers: string[]; -} \ No newline at end of file From e05a6793b8e678024745cd6da0ce1ba56cbb443f Mon Sep 17 00:00:00 2001 From: Victor Maldonado <67714035+vmlcode@users.noreply.github.com> Date: Fri, 3 Jan 2025 13:16:53 -0400 Subject: [PATCH 267/820] FIX: FA-312 implement api for adding and removing summarization reports (#242) * add endpoint for summarization report templates and error handling * refactor cosmos db connection functions and add template management methods * add endpoints for creating and deleting summarization report templates with error handling * add delete_template function to remove summarization report templates * refactor removeSummarizationReport to return deletion result from delete_template * add endpoints to retrieve summarization report templates by ID and list all templates * add API functions for managing summarization report templates in the frontend * remove unnecessary call to get_templates in cosmo_db.py * fix: function to retrieve summarization report template by ID in frontend * fix: update API endpoints for creating and deleting summarization report templates * fix: add companyTickers field to summarization report template * refactor: update datetime handling to use UTC instead of deprecated method utcnow * Refactor: Replaced deprecated UTC method with timezone.utc in app.py and cosmo_db.py --- backend/app.py | 102 +++++++++++++++++++++++++++- backend/shared/cosmo_db.py | 133 ++++++++++++++++++++++++++----------- backend/utils.py | 14 ++++ frontend/src/api/api.ts | 46 ++++++++++++- frontend/src/api/models.ts | 6 ++ 5 files changed, 262 insertions(+), 39 deletions(-) diff --git a/backend/app.py b/backend/app.py index 024341af..83a7eec3 100644 --- a/backend/app.py +++ b/backend/app.py @@ -44,6 +44,9 @@ SubscriptionError, InvalidSubscriptionError, InvalidFinancialPriceError, + InvalidParameterError, + MissingJSONPayloadError, + MissingRequiredFieldError, require_client_principal, ) import stripe.error @@ -55,6 +58,10 @@ update_report, delete_report, get_filtered_reports, + create_template, + delete_template, + get_templates, + get_template_by_ID, update_user ) @@ -226,7 +233,7 @@ def check_user_authorization( logger.info( f"[auth] Checking authorization for user {client_principal_id} " - f"with email {email} at {datetime.utcnow().isoformat()}" + f"with email {email} at {datetime.now(timezone.utc).isoformat()}" ) # Make the request using a session for better performance @@ -828,6 +835,99 @@ def getFilteredType(): logging.exception(f"Error retrieving reports.") return jsonify({"error": "Internal Server Error"}), 500 +@app.route("/api/reports/summarization/templates", methods=["POST"]) +def addSummarizationReport(): + """ + Endpoint to add a summarization report template. + + This endpoint expects a JSON payload with the following fields: + - name: The name of the report template. Must be one of ["10-K", "10-Q", "8-K", "DEF 14A"]. + - description: A description of the report template. + + MissingJSONPayloadError: If the JSON payload is missing. + MissingRequiredFieldError: If the 'name' or 'description' field is missing. + InvalidParameterError: If the 'name' field is not one of the valid names. + + JSON response with the created report template if successful. + JSON error response with appropriate HTTP status code if an error occurs. + """ + try: + data = request.get_json() + if not data: + raise MissingJSONPayloadError('Missing JSON payload') + if not "name" in data: + raise MissingRequiredFieldError('name') + if not "description" in data: + raise MissingRequiredFieldError('description') + if not "companyTickers" in data: + raise MissingRequiredFieldError('companyTickers') + valid_names=["10-K", "10-Q", "8-K", "DEF 14A"] + if not data["name"] in valid_names: + raise InvalidParameterError('name', f"Must be one of: {', '.join(valid_names)}") + new_template = {'name': data['name'], 'companyTickers': data['companyTickers'], 'description': data['description'], 'status': 'active', 'type': 'summarization'} + # add to cosmosDB container + result = create_template(new_template) + return create_success_response(result) + except MissingJSONPayloadError as e: + return create_error_response("Invalid or Missing JSON payload", HTTPStatus.BAD_REQUEST) + except MissingRequiredFieldError as field: + return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) + except InvalidParameterError as e: + return create_error_response(str(e), HTTPStatus.BAD_REQUEST) + except Exception as e: + logging.exception(e) + return jsonify({"error": "An unexpected error occurred. Please try again later."}), HTTPStatus.INTERNAL_SERVER_ERROR + + +@app.route('/api/reports/summarization/templates/', methods=['DELETE']) +def removeSummarizationReport(template_id): + """ + Endpoint to remove a summarization report template by ID. + + This endpoint expects the following URL parameter: + - template_id: The ID of the report template to be removed. + + NotFound: If the report template with the specified ID does not exist. + Exception: For any other unexpected errors. + + JSON response with appropriate HTTP status code: + - 204 No Content: If the report template is successfully deleted. + - 404 Not Found: If the report template with the specified ID does not exist. + - 500 Internal Server Error: If an unexpected error occurs. + """ + try: + if not template_id: + raise MissingRequiredFieldError('template_id') + #delete from cosmosDB container + result = delete_template(template_id) + return create_success_response(result) + except NotFound as e: + return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) + except MissingRequiredFieldError as field: + return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) + except Exception as e: + return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + +@app.route('/api/reports/summarization/templates/', methods=['GET']) +def getSummarizationReports(): + try: + result = get_templates() + return create_success_response(result) + except Exception as e: + return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + +@app.route('/api/reports/summarization/templates/', methods=['GET']) +def getSummarizationReport(template_id): + try: + result = get_template_by_ID(template_id) + return create_success_response(result) + except NotFound as e: + return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) + except Exception as e: + return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + + + # methods to provide access to speech services and blob storage account blobs diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index fc841280..5eaebca8 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -4,53 +4,32 @@ from azure.cosmos.exceptions import CosmosResourceNotFoundError, AzureError import uuid import logging -from datetime import datetime +from datetime import datetime, timezone from werkzeug.exceptions import NotFound AZURE_DB_ID = os.environ.get("AZURE_DB_ID") AZURE_DB_NAME = os.environ.get("AZURE_DB_NAME") AZURE_DB_URI = f"https://{AZURE_DB_ID}.documents.azure.com:443/" -def get_cosmos_container_users(): +def get_cosmos_container(container_name): """ - Establishes the connection to the Cosmos DB `reports` container. + Establishes the connection to the Cosmos DB container specified by `container_name`. """ credential = DefaultAzureCredential() client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") db = client.get_database_client(database=AZURE_DB_NAME) - container = db.get_container_client("users") + container = db.get_container_client(container_name) try: - logging.info("Connection to Cosmos DB established successfully.") + logging.info(f"Connection to Cosmos DB container '{container_name}' established successfully.") return container except AzureError as az_err: - logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") + logging.error(f"AzureError encountered while connecting to Cosmos DB container '{container_name}': {az_err}") raise Exception(f"Azure connection error: {az_err}") from az_err except Exception as e: - logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") - raise Exception(f"Unexpected connection error: {e}") from e - -def get_cosmos_container_report(): - """ - Establishes the connection to the Cosmos DB `reports` container. - """ - credential = DefaultAzureCredential() - client = CosmosClient(AZURE_DB_URI, credential, consistency_level="Session") - db = client.get_database_client(database=AZURE_DB_NAME) - container = db.get_container_client("reports") - - try: - logging.info("Connection to Cosmos DB established successfully.") - return container - - except AzureError as az_err: - logging.error(f"AzureError encountered while connecting to Cosmos DB: {az_err}") - raise Exception(f"Azure connection error: {az_err}") from az_err - - except Exception as e: - logging.error(f"Unexpected error while connecting to Cosmos DB: {e}") + logging.error(f"Unexpected error while connecting to Cosmos DB container '{container_name}': {e}") raise Exception(f"Unexpected connection error: {e}") from e def create_report(data): @@ -58,10 +37,10 @@ def create_report(data): Creates a new document in the container. """ try: - container = get_cosmos_container_report() + container = get_cosmos_container("reports") data["id"] = str(uuid.uuid4()) - data["createAt"] = datetime.utcnow().isoformat() + "Z" - data["updatedAt"] = datetime.utcnow().isoformat() + "Z" + data["createAt"] = datetime.now(timezone.utc).isoformat() + "Z" + data["updatedAt"] = datetime.now(timezone.utc).isoformat() + "Z" container.upsert_item(data) logging.info(f"Document created: {data}") return data @@ -83,7 +62,8 @@ def get_report(report_id): Exception: For any other unexpected error that occurs during retrieval. CosmosResourceNotFoundError: If the report with the specified ID does not exist in the database. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") + try: report = container.read_item(item=report_id, partition_key=report_id) @@ -112,7 +92,7 @@ def get_filtered_reports(report_type=None): CosmosResourceNotFoundError: If no reports with the specified type are found (when filtered). Exception: For any other unexpected error that occurs during retrieval. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") if report_type: query = "SELECT * FROM c WHERE c.type = @type" parameters = [{"name": "@type", "value": report_type}] @@ -148,7 +128,7 @@ def update_report(report_id, updated_data): Handles database errors and raises exceptions as needed. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") try: current_report = get_report(report_id) @@ -187,7 +167,7 @@ def delete_report(report_id): """ Deletes a specific document using its `id` as partition key. """ - container = get_cosmos_container_report() + container = get_cosmos_container("reports") try: container.delete_item(item=report_id, partition_key=report_id) @@ -202,6 +182,85 @@ def delete_report(report_id): logging.error(f"Error deleting report with id {report_id}: {e}") raise +# Template management + +def create_template(data): + """ + Creates a new document in the container. + """ + try: + container = get_cosmos_container("templates") + data["id"] = str(uuid.uuid4()) + data["createAt"] = datetime.now(timezone.utc).isoformat() + "Z" + data["updatedAt"] = datetime.now(timezone.utc).isoformat() + "Z" + container.upsert_item(data) + logging.info(f"Document created: {data}") + return data + except Exception as e: + logging.error(f"Error inserting data into Cosmos DB: {e}") + raise + +def delete_template(template_id): + """ + Deletes a specific document using its `id` as partition key. + """ + container = get_cosmos_container("templates") + + try: + container.delete_item(item=template_id, partition_key=template_id) + logging.info(f"Template with id {template_id} deleted successfully.") + return {"message": f"Template with id {template_id} deleted successfully."} + + except CosmosResourceNotFoundError: + logging.warning(f"Template with id '{template_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Error deleting template with id {template_id}: {e}") + raise + +def get_templates(): + """Get all the templates in a cosmosDB container""" + container = get_cosmos_container("templates") + try: + items = list(container.query_items( + query="SELECT * FROM c", + enable_cross_partition_query=True + )) + + if not items: + logging.warning(f"No templates found.") + raise NotFound + + logging.info(f"Templates successfully retrieved: {items}") + print(items) + return items + + except CosmosResourceNotFoundError: + logging.warning(f"No templates found.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving templates: {e}") + raise + + +def get_template_by_ID(template_id): + """Get a template by its ID""" + container = get_cosmos_container("templates") + try: + template = container.read_item(item=template_id, partition_key=template_id) + logging.info(f"Template successfully retrieved: {template}") + return template + + except CosmosResourceNotFoundError: + logging.warning(f"Template with id '{template_id}' not found in Cosmos DB.") + raise NotFound + + except Exception as e: + logging.error(f"Unexpected error retrieving template with id '{template_id}': {e}") + + def get_user_container(user_id): """ Retrieves a specific document (user_id) from the Cosmos DB container using its `id` as partition key. @@ -216,7 +275,7 @@ def get_user_container(user_id): Exception: For any other unexpected error that occurs during retrieval. CosmosResourceNotFoundError: If the user with the specified ID does not exist in the database. """ - container = get_cosmos_container_users() + container = get_cosmos_container("users") try: user = container.read_item(item=user_id, partition_key=user_id) @@ -237,7 +296,7 @@ def update_user(user_id, updated_data): Handles database errors and raises exceptions as needed. """ - container = get_cosmos_container_users() + container = get_cosmos_container("users") try: current_user = get_user_container(user_id) diff --git a/backend/utils.py b/backend/utils.py index 2ee14116..25c4e75d 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -43,6 +43,20 @@ class InvalidSubscriptionError(SubscriptionError): pass +class MissingJSONPayloadError(Exception): + """Raised when JSON payload is missing""" + + pass + +class MissingRequiredFieldError(Exception): + """Raised when a required field is missing""" + + pass + +class InvalidParameterError(Exception): + """Raised when an invalid parameter is provided""" + + pass # Security: Decorator to ensure client principal ID is present diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 25aa78d1..c14e1e0d 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -1,4 +1,4 @@ -import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo } from "./models"; +import { AskResponseGpt, ChatRequestGpt, GetSettingsProps, PostSettingsProps, ConversationHistoryItem, ChatTurn, UserInfo, SummarizationReportProps } from "./models"; export async function getUsers({ user }: any): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; @@ -621,6 +621,50 @@ export async function getFilteredReports(type?: string) { return reports; } +// Summarization reports +export async function getSummarizationTemplates() { + const response = await fetch('/api/reports/summarization/templates', {method: 'GET', headers: {'Content-Type': 'application/json'}}); + if (response.status > 299 || !response.ok) { + throw Error('Error getting summarization templates'); + } + const reports = await response.json(); + return reports; +} + +export async function getSummarizationReportTemplateByID(templateID: string) { + const response = await fetch(`/api/reports/summarization/templates/${templateID}`, {method: 'GET', headers: {'Content-Type': 'application/json'}}); + const report = await response.json(); + return report; +} + +export async function createSummarizationReport(templateData: SummarizationReportProps) { + const response = await fetch('/api/reports/summarization/templates', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(templateData), + }); + + if (response.status > 299 || !response.ok) { + throw Error('Error creating a new summarization report'); + } + + const newReport = await response.json(); + return newReport; +} + +export async function deleteSumarizationReportTemplate(templateID: string) { + const response = await fetch(`/api/reports/summarization/templates/${templateID}`, { + method: 'DELETE', + headers: {'Content-Type': 'application/json'}, + }); + + if (response.status > 299 || !response.ok) { + throw Error('Error deleting summarization report'); + } + const deletedReport = await response.json(); + return deletedReport; +} + export async function deleteReport(reportId: string) { const response = await fetch(`/api/reports/${encodeURIComponent(reportId)}`, { method: "DELETE", diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index f5bf5999..420d88c5 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -105,3 +105,9 @@ export type PostSettingsProps = { } | null; temperature: number; }; + +export type SummarizationReportProps = { + name: string; + description: string; + companyTickers: string[]; +} \ No newline at end of file From 5251b4180a735a61b8464e652c02d0b2a0c062a8 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Mon, 6 Jan 2025 09:53:33 -0400 Subject: [PATCH 268/820] FA-327 Configure the new role for the "platformAdmin" (#243) * Fix the routes so that the new role works correctly --- frontend/src/App.tsx | 26 +- frontend/src/components/Sidebar/Sidebar.tsx | 29 +- .../components/Sidebar/SidebarItemTypes.ts | 2 +- frontend/src/pages/admin/Admin.tsx | 325 +++++++++--------- frontend/src/router/ProtectedRoute.tsx | 2 +- 5 files changed, 190 insertions(+), 194 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 106d723b..99917378 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,14 +32,11 @@ export default function App() { } /> } /> - {/* Access Denied Route */} - } /> - {/* Protected Routes for Authenticated Users (Regular and Admin) */} } /> } /> } /> - }/> + } /> + } /> @@ -64,7 +62,7 @@ export default function App() { } /> } /> } /> - }/> - }/> + } /> + } /> @@ -90,7 +88,7 @@ export default function App() { } @@ -105,7 +103,7 @@ export default function App() { } @@ -119,16 +117,16 @@ export default function App() { } > }> } /> - }/> - }/> - }/> + } /> + } /> + } /> diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index c317f63b..8dd69dfc 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -75,14 +75,14 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon: , to: "/", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user"] + roles: ["admin", "user", "platformAdmin"] }, { title: "Notifications", icon: , to: "/notification-settings", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user"] + roles: ["admin", "user", "platformAdmin"] } ] }, @@ -97,28 +97,28 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon: , to: "/admin", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["admin", "platformAdmin"] }, { title: "Invitations", icon: , to: "/invitations", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["admin", "platformAdmin"] }, { title: "Organization Management", icon: , to: "/organization", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["admin", "platformAdmin"] }, { title: "Financial Assistant", icon: , to: "/financialassistant", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["admin", "platformAdmin"] } ] }, @@ -133,14 +133,14 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon: , to: "/subscription-management", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["admin", "platformAdmin"] }, { title: "User Management", icon: , to: "/manage-email-lists", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["admin", "platformAdmin"] } ] }, @@ -157,18 +157,18 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { { title: "Upload Resources", href: "/upload-resources", - roles: ["admin", "user"], + roles: ["admin", "user", "platformAdmin"], tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"] }, { title: "Request Studies", href: "/request-studies", - roles: ["admin", "user"], + roles: ["admin", "user", "platformAdmin"], tiers: ["Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"] } ], tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user"] + roles: ["admin", "user", "platformAdmin"] } ] }, @@ -176,6 +176,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { divider: true }, { + //It is only visible to platform administrators section: "Reports", items: [ { @@ -183,14 +184,14 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon: , to: "/view-manage-reports", tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user"] + roles: ["platformAdmin"] }, { title: "Distribution Lists", icon: , to: "/details-settings", tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin"] + roles: ["platformAdmin"] } ] }, @@ -205,7 +206,7 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { icon: , to: "/help-center", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user"] + roles: ["admin", "user", "platformAdmin"] } ] } diff --git a/frontend/src/components/Sidebar/SidebarItemTypes.ts b/frontend/src/components/Sidebar/SidebarItemTypes.ts index cb5cbdc2..cf771a71 100644 --- a/frontend/src/components/Sidebar/SidebarItemTypes.ts +++ b/frontend/src/components/Sidebar/SidebarItemTypes.ts @@ -1,6 +1,6 @@ // SidebarItemTypes.ts -export type Role = "admin" | "user"; +export type Role = "platformAdmin"|"admin" | "user"; export type SubscriptionTier = | "Basic" diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx index 4afe82fe..55a9e3dc 100644 --- a/frontend/src/pages/admin/Admin.tsx +++ b/frontend/src/pages/admin/Admin.tsx @@ -477,192 +477,189 @@ const Admin = () => { return (
    - {user.role !== "admin" &&

    Access denied

    } - {user.role === "admin" && ( - <> -
    -

    Roles and access

    -
    -
    +
    +

    Roles and access

    +
    +
    + { + setIsOpen(true); }} > - { - setIsOpen(true); - }} - > - - Create user - - + Create user + + { - setSearch(newValue || ""); - }} - iconProps={{ - iconName: "Search", - children: - }} - /> -
    - - {loading ? null : } - { - setIsDeleting(false); + } }} - onConfirm={() => { - deleteUserFromOrganization(selectedUser?.id); + onChange={(_ev, newValue) => { + setSearch(newValue || ""); + }} + iconProps={{ + iconName: "Search", + children: }} - isDeletingUser={isDeletingUser} /> - {loading ? ( - - ) : ( -
    - - - - + ); + })} + +
    + + {loading ? null : } + { + setIsDeleting(false); + }} + onConfirm={() => { + deleteUserFromOrganization(selectedUser?.id); + }} + isDeletingUser={isDeletingUser} + /> + {loading ? ( + + ) : ( +
    + + + + + + + + + + + {filteredUsers.map((user: any, index) => { + return ( + - Name - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - - - + - + - - - ); - })} - -
    + Name + EmailRoleActions
    EmailRoleActions
    - {user.data.name} - + + {user.data.email} + +
    - {user.data.email} -
    -
    +
    +
    + { +
    + +
    + + -
    - { -
    - - -
    - } -
    -
    - )} - - )} + } + +
    +
    + )} +
    ); }; diff --git a/frontend/src/router/ProtectedRoute.tsx b/frontend/src/router/ProtectedRoute.tsx index 0df865a3..7edc116b 100644 --- a/frontend/src/router/ProtectedRoute.tsx +++ b/frontend/src/router/ProtectedRoute.tsx @@ -10,7 +10,7 @@ import LoadingSpinner from "../components/LoadingSpinner/LoadingSpinner"; // Opt * * @param {Array} allowedRoles - Array of roles that are permitted to access the route. */ -type Role = "admin" | "user"; +type Role = "platformAdmin" | "admin" | "user"; type SubscriptionTier = "Basic" | "Custom" | "Premium" | "Basic + Financial Assistant" | "Custom + Financial Assistant" | "Premium + Financial Assistant"; interface ProtectedRouteProps { From 52737cf4b8e3b0b2c94d6a588141c088c9b07ba6 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:18:33 -0500 Subject: [PATCH 269/820] FA-298 integrate report with email (#244) * create monht_year folder for weekly curation report * Create an endpoint distribution email * feat(email): Add email notification system with HTML templates for financial reports * feat(email): Add process_and_send_email endpoint and enhance email sending functionality - Introduced a new endpoint `/api/reports/process-and-email` to process reports and send email notifications. - Updated `send_email_endpoint` to clarify attachment path requirements. - Enhanced error handling and logging in email processing and sending functions. - Refactored email service to improve attachment handling and validation. - Added custom exceptions for better error management in blob service operations. * wkh * plan to integrate report content with financial agent * add some utils and minor fixes * fix merge conflicts * remove some comments * change email host and port env --- backend/app.py | 67 ++- backend/financial_doc_processor.py | 99 +++- backend/llm_config.py | 22 + backend/report2email.ipynb | 541 ++++++++++++++++++ .../report_email_templates/email_templates.py | 61 ++ .../html/report_email.html | 60 ++ backend/requirements.txt | 5 +- backend/rp2email.py | 441 ++++++++++++++ backend/utils.py | 38 +- 9 files changed, 1314 insertions(+), 20 deletions(-) create mode 100644 backend/report2email.ipynb create mode 100644 backend/report_email_templates/email_templates.py create mode 100644 backend/report_email_templates/html/report_email.html create mode 100644 backend/rp2email.py diff --git a/backend/app.py b/backend/app.py index 83a7eec3..2bc6d21c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2824,13 +2824,14 @@ def generate_report(): @app.route('/api/reports/email', methods=['POST']) def send_email_endpoint(): """Send an email with optional attachments. + Note: currently attachment path has to be in the same directory as the app.py file. Expected JSON payload: { "subject": "Email subject", "html_content": "HTML formatted content", "recipients": ["email1@domain.com", "email2@domain.com"], - "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes + "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. } Returns: @@ -2883,10 +2884,10 @@ def send_email_endpoint(): # Validate email configuration email_config = { - 'smtp_server': os.getenv('EMAIL_SMTP_SERVER'), - 'smtp_port': os.getenv('EMAIL_SMTP_PORT'), - 'username': os.getenv('EMAIL_USER_NAME'), - 'password': os.getenv('EMAIL_USER_PASSWORD') + 'smtp_server': os.getenv('EMAIL_HOST'), + 'smtp_port': os.getenv('EMAIL_PORT'), + 'username': os.getenv('EMAIL_USER'), + 'password': os.getenv('EMAIL_PASS') } if not all(email_config.values()): @@ -2923,7 +2924,63 @@ def send_email_endpoint(): 'status': 'error', 'message': f'An unexpected error occurred: {str(e)}' }), 500 + +from rp2email import process_and_send_email +@app.route('/api/reports/digest', methods=['POST']) +def digest_report(): + """ + Process report and send email . + Expected payload: + { + "blob_link": "https://...", + "recipients": ["email1@domain.com"], + "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. + By default, it will automatically attach the document from the blob link (PDF converted). Select "no" to disable this feature. + "email_subject": "Custom email subject" # Optional + } + """ + try: + # Validate request data + data = request.get_json() + if not data: + return jsonify({ + 'status': 'error', + 'message': 'No JSON data provided' + }), 400 + + # Validate required fields + if 'blob_link' not in data or 'recipients' not in data: + return jsonify({ + 'status': 'error', + 'message': 'Missing required fields: blob_link and recipients' + }), 400 + + # Process report and send email + success = process_and_send_email( + blob_link=data['blob_link'], + recipients=data['recipients'], + attachment_path=data.get('attachment_path', None), + email_subject=data.get('email_subject', None), + ) + + if success: + return jsonify({ + 'status': 'success', + 'message': 'Report processed and email sent successfully' + }), 200 + else: + return jsonify({ + 'status': 'error', + 'message': 'Failed to process report and send email' + }), 500 + + except Exception as e: + logger.exception("Error processing report and sending email") + return jsonify({ + 'status': 'error', + 'message': str(e) + }), 500 if __name__ == "__main__": diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 0771ca17..008e8c8c 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -389,6 +389,95 @@ def __init__(self): raise BlobConnectionError(f"Invalid connection string: {str(e)}") except Exception as e: raise BlobConnectionError(f"Failed to initialize blob storage: {str(e)}") + + #todo: add report blob path to the click here link in the html template like this: https://webgpt0-vm2b2htvuuclm.azurewebsites.net/?agent=financial&document=Ecommerce&blobpath=%22https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Ecommerce/De[%E2%80%A6]KdAaWXqNHsjhB5c3cWaAT3rWymLUB3YuZQdOc%2F6FYG8%3D%22 + #todo: then retrieve the document in init and load the report content to the llm + + def get_rpcontent_from_blob_path(self, blob_path: str) -> str: + """ + Get report content from blob path. + + Args: + blob_path (str): Path to the blob, e.g. 'Reports/Curation_Reports/Ecommerce/December_2024.html' + """ + try: + # Remove any leading/trailing slashes + clean_path = blob_path.strip('/') + + logger.info(f"Attempting to access blob at path: {clean_path}") + + blob_client = self.container_client.get_blob_client(clean_path) + + if not blob_client.exists(): + logger.error(f"Blob not found: {clean_path}") + raise BlobDownloadError(f"Blob not found at path: {clean_path}") + + downloaded_blob = blob_client.download_blob() + return downloaded_blob.content_as_text() + + except Exception as e: + logger.exception(f"Error accessing blob at {blob_path}") + raise BlobDownloadError(f"Failed to download blob: {str(e)}") + + # todo: double check this function + def _get_blob_path_parts_from_url(self, url: str) -> List[str]: + """ + Get the blob path parts from a given URL. + """ + from urllib.parse import urlparse + + parsed_url = urlparse(url) + return parsed_url.path.lstrip('/').split('/') + + def download_blob_from_a_link(self, url: str, filename: str = None) -> bool: + """ + Download a document from a given blob URL and save it to the downloads directo ry. + + Args: + url (str): The full Azure blob storage URL + filename (str, optional): Name for the downloaded file. If not provided, + will be extracted from the URL + + Returns: + None + """ + try: + # Parse the URL to get the container and blob path + from urllib.parse import urlparse + parsed_url = urlparse(url) + + # Split the path into parts + path_parts = parsed_url.path.lstrip('/').split('/') + + # Get blob path + blob_path = '/'.join(path_parts[1:]) + + # If filename not provided, use the last part of the blob path + if not filename: + filename = os.path.basename(blob_path) + + # Create downloads directory in project root + downloads_dir = os.path.join(os.getcwd(), 'blob_downloads') + os.makedirs(downloads_dir, exist_ok=True) + + # Construct the full local path + local_data_path = os.path.join(downloads_dir, filename) + + # Get the blob client + blob_client = self.container_client.get_blob_client(blob_path) + + # Download the blob + with open(local_data_path, 'wb') as file: + download_stream = blob_client.download_blob() + file.write(download_stream.readall()) + + logger.info(f"Successfully downloaded blob to {local_data_path}") + return True + + except Exception as e: + logger.error(f"Failed to download blob: {str(e)}") + return False + def download_documents(self, equity_name: str, financial_type: str, @@ -470,7 +559,6 @@ def download_documents(self, equity_name: str, raise return downloaded_files - def get_document_metadata(self, remote_file_path: str) -> dict: """Retrieve metadata for a specific blob from defined container in env @@ -539,7 +627,8 @@ def upload_to_blob(self, document_paths: dict = None, metadata: dict = None, fil name=blob_path, data=data, overwrite=True, - content_settings=ContentSettings(content_type=content_type) + content_settings=ContentSettings(content_type=content_type), + metadata=metadata ) except Exception as e: raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") @@ -744,7 +833,7 @@ def process_and_upload(self, equity_id: str, filing_type: str) -> dict: # example usage for get_document_metadata if __name__ == "__main__": doc_processor = BlobStorageManager() - metadata = doc_processor.get_document_metadata('financial/10-K/AAPL.pdf') - print(metadata) + content = doc_processor.get_rpcontent_from_blob_path('/Reports/Curation_Reports/Monthly_Economics/December_2024.html') + print(content) + -# if upload date is within the past diff --git a/backend/llm_config.py b/backend/llm_config.py index 18689240..e7238e86 100644 --- a/backend/llm_config.py +++ b/backend/llm_config.py @@ -55,6 +55,28 @@ class PromptTemplate(BaseModel): """ ) + email_template: str = Field( + default=""" + Summarize the following report in to 3 main key points. + + I want to maintain the title of the report. + + You should include an intro text with just one sentence capture the main theme of the report, and tell them here are 3 key points of the reports. + + I also want to add a part 'Why it matters' at the end. + + Be concise and to the point. No more than 2 sentences per point. + + No need to include any citations or references. + + If there is any HTML tags, please remove them. + + Here is the report: + + {report_content} + """ + ) + class Config: frozen = True diff --git a/backend/report2email.ipynb b/backend/report2email.ipynb new file mode 100644 index 00000000..856ea39a --- /dev/null +++ b/backend/report2email.ipynb @@ -0,0 +1,541 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:45:39,312 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 63\n", + "2024-12-31 08:45:39,324 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 64\n", + "2024-12-31 08:45:39,324 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 65\n", + "2024-12-31 08:45:39,324 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 66\n", + "2024-12-31 08:45:39,327 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 70\n", + "2024-12-31 08:45:39,327 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 71\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import logging\n", + "import markdown\n", + "\n", + "from llm_config import LLMManager # Load pre-configured LLM\n", + "from financial_doc_processor import BlobStorageManager # Load Blob Storage Manager\n", + "\n", + "logger = logging.getLogger(__name__)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:42:30,812 - azure.core.pipeline.policies.http_logging_policy - INFO - Request URL: 'https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Monthly_Economics/December_2024.html'\n", + "Request method: 'GET'\n", + "Request headers:\n", + " 'x-ms-range': 'REDACTED'\n", + " 'x-ms-version': 'REDACTED'\n", + " 'Accept': 'application/xml'\n", + " 'User-Agent': 'azsdk-python-storage-blob/12.19.0 Python/3.11.9 (Windows-10-10.0.26100-SP0)'\n", + " 'x-ms-date': 'REDACTED'\n", + " 'x-ms-client-request-id': '14f36c90-c77d-11ef-904c-10f60a8f4b42'\n", + " 'Authorization': 'REDACTED'\n", + "No body was attached to the request\n", + "2024-12-31 08:42:31,271 - azure.core.pipeline.policies.http_logging_policy - INFO - Response status: 206\n", + "Response headers:\n", + " 'Content-Length': '13678'\n", + " 'Content-Type': 'text/html'\n", + " 'Content-Range': 'REDACTED'\n", + " 'Last-Modified': 'Mon, 30 Dec 2024 20:06:45 GMT'\n", + " 'Accept-Ranges': 'REDACTED'\n", + " 'ETag': '\"0x8DD290D7C488B92\"'\n", + " 'Server': 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0'\n", + " 'x-ms-request-id': '0aa6194a-001e-005f-3389-5b24c7000000'\n", + " 'x-ms-client-request-id': '14f36c90-c77d-11ef-904c-10f60a8f4b42'\n", + " 'x-ms-version': 'REDACTED'\n", + " 'x-ms-creation-time': 'REDACTED'\n", + " 'x-ms-blob-content-md5': 'REDACTED'\n", + " 'x-ms-lease-status': 'REDACTED'\n", + " 'x-ms-lease-state': 'REDACTED'\n", + " 'x-ms-blob-type': 'REDACTED'\n", + " 'x-ms-server-encrypted': 'REDACTED'\n", + " 'Access-Control-Expose-Headers': 'REDACTED'\n", + " 'Access-Control-Allow-Origin': 'REDACTED'\n", + " 'Date': 'Tue, 31 Dec 2024 13:42:31 GMT'\n", + "2024-12-31 08:42:31,274 - financial_doc_processor - INFO - Successfully downloaded blob to c:\\SF_Repo\\gpt-rag-frontend\\backend\\blob_downloads\\December_2024.html\n" + ] + } + ], + "source": [ + "\n", + "# initialize the blob manager \n", + "blob_manager = BlobStorageManager()\n", + "\n", + "# downlaod the blob\n", + "# todo: check if the status is success before processing this \n", + "#! this is is retrieved from the previous successful run the process_and_summarize endpoint\n", + "blob_link = 'https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Monthly_Economics/December_2024.html?sv=2022-11-02&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2025-01-06T13:29:33Z&st=2024-09-25T04:29:33Z&spr=https&sig=rmMDVy0aPztEj6A%2FMQHFbioHbLuiL3tn622D993%2Fvow%3D'\n", + "\n", + "file = blob_manager.download_blob_from_a_link(blob_link)\n", + "\n", + "if file:\n", + " # process the file \n", + " pass\n", + "else:\n", + " logger.error(\"Failed to download the file\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:42:31,298 - __main__ - INFO - Successfully imported the HTML file\n" + ] + } + ], + "source": [ + "# import the html file \n", + "from pathlib import Path\n", + "\n", + "# Get the file within blob_downloads\n", + "html_file_path = next(Path(os.getcwd()).glob('blob_downloads/*.html'))\n", + "\n", + "# Check if the file exists\n", + "if html_file_path.exists():\n", + " with open(html_file_path, 'r', encoding='utf-8') as file:\n", + " html_content = file.read()\n", + " logger.info(\"Successfully imported the HTML file\")\n", + "else:\n", + " logger.error(\"HTML file not found\")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:42:36,144 - httpx - INFO - HTTP Request: POST https://oai0-vm2b2htvuuclm.openai.azure.com/openai/deployments/Agent/chat/completions?api-version=2024-05-01-preview \"HTTP/1.1 200 OK\"\n" + ] + } + ], + "source": [ + "# process llm \n", + "llm_manager = LLMManager()\n", + "llm = llm_manager.get_client(client_type='gpt4o', use_langchain=True)\n", + "\n", + "# get prompt \n", + "sys_prompt = llm_manager.get_prompt(prompt_type='email_template')\n", + "\n", + "# add the report content to the prompt \n", + "prompt = sys_prompt.format(report_content=html_content)\n", + "\n", + "#* summarize the report\n", + "\n", + "summary = llm.invoke(prompt)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "**December 2024: Key Economic Trends Shaping the Global Landscape**\n", + "\n", + "This report provides an analysis of the global economic environment in December 2024, highlighting the interplay of growth, inflation, and geopolitical tensions. Here are 3 key points of the report:\n", + "\n", + "1. **Global Economic Stability**: The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.\n", + "\n", + "2. **Regional Economic Dynamics**: North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.\n", + "\n", + "3. **Industry Transformations**: The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.\n", + "\n", + "**Why it matters:** Understanding these economic trends is crucial for businesses and investors to strategically position themselves in a complex global landscape, leveraging opportunities for growth while mitigating the risks associated with geopolitical and market uncertainties." + ], + "text/plain": [ + "" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Markdown, HTML\n", + "Markdown(summary.content)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "

    December 2024: Key Economic Trends Shaping the Global Landscape

    \n", + "

    This report provides an analysis of the global economic environment in December 2024, highlighting the interplay of growth, inflation, and geopolitical tensions. Here are 3 key points of the report:

    \n", + "
      \n", + "
    1. \n", + "

      Global Economic Stability: The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.

      \n", + "
    2. \n", + "
    3. \n", + "

      Regional Economic Dynamics: North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.

      \n", + "
    4. \n", + "
    5. \n", + "

      Industry Transformations: The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.

      \n", + "
    6. \n", + "
    \n", + "

    Why it matters: Understanding these economic trends is crucial for businesses and investors to strategically position themselves in a complex global landscape, leveraging opportunities for growth while mitigating the risks associated with geopolitical and market uncertainties.

    " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "#* now convert this summarization HTML \n", + "\n", + "report_html: str = markdown.markdown(summary.content)\n", + "\n", + "HTML(report_html)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In order to parse these information to email, we need to parse it to proper field. " + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "from utils import EmailSchema\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:42:39,982 - httpx - INFO - HTTP Request: POST https://oai0-vm2b2htvuuclm.openai.azure.com/openai/deployments/Agent/chat/completions?api-version=2024-05-01-preview \"HTTP/1.1 200 OK\"\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "title='Key Economic Trends Shaping the Global Landscape - December 2024' intro_text='This report provides an analysis of the global economic environment in December 2024, highlighting the interplay of growth, inflation, and geopolitical tensions.' keypoints=[KeyPoint(title='Global Economic Stability', content='The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.'), KeyPoint(title='Regional Economic Dynamics', content='North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.'), KeyPoint(title='Industry Transformations', content='The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.')] why_it_matters='Understanding these economic trends is crucial for businesses and investors to strategically position themselves in a complex global landscape, leveraging opportunities for growth while mitigating the risks associated with geopolitical and market uncertainties.' document_type='MonthlyMacroeconomics'\n" + ] + } + ], + "source": [ + "# Now that we've built the schema, we will parse the report to this schema\n", + "\n", + "llm_report_parser = llm.with_structured_output(EmailSchema)\n", + "\n", + "email_schema = llm_report_parser.invoke(summary.content)\n", + "\n", + "print(email_schema)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'title': 'Global Economic Stability',\n", + " 'content': 'The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.'},\n", + " {'title': 'Regional Economic Dynamics',\n", + " 'content': 'North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.'},\n", + " {'title': 'Industry Transformations',\n", + " 'content': 'The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.'}]" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# since there are multiple key points, we should convert thme to dict \n", + "\n", + "keypoints_dict = [{'title': point.title, 'content': point.content} for point in email_schema.keypoints]\n", + "\n", + "\n", + "keypoints_dict\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "# create a function to generate the html template\n", + "def generate_html_template(title, intro_text, key_points, why_it_matters, document_type):\n", + " \"\"\"\n", + " Generate HTML email template with customizable content.\n", + " \n", + " Parameters:\n", + " - title (str): The main title of the report\n", + " - intro_text (str): Introductory text below the title\n", + " - key_points (list): List of dictionaries containing 'title' and 'content' for each point\n", + " - why_it_matters (str): The \"Why it matters\" section content\n", + " \"\"\"\n", + " \n", + " # Generate the key points HTML\n", + " key_points_html = \"\"\n", + " for i, point in enumerate(key_points, 1):\n", + " key_points_html += f\"\"\"\n", + "

    {i}. {point['title']}: {point['content']}

    \"\"\"\n", + "\n", + " template = f\"\"\"\n", + "\n", + "\n", + " \n", + " \n", + " {title}\n", + "\n", + "\n", + "
    \n", + "\n", + "\"\"\"\n", + " \n", + " return template\n" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "# generate the html \n", + "email_html_content = generate_html_template(title = email_schema.title, \n", + " intro_text = email_schema.intro_text, \n", + " key_points = keypoints_dict, \n", + " why_it_matters = email_schema.why_it_matters, \n", + " document_type = email_schema.document_type)\n", + "\n", + "# we can now save this html to a file \n", + "with open('email_html_content.html', 'w') as file:\n", + " file.write(email_html_content)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [], + "source": [ + "# wondering if I should create a different endpoint to send email. \n", + "\n", + "# let's see the needed components \n", + "\n", + "import smtplib \n", + "\n", + "from email.message import EmailMessage\n", + "\n", + "EMAIL_CONFIG = {\n", + " 'smtp_server': 'smtp.gmail.com',\n", + " 'port': 587,\n", + " 'email': 'namtran6701@gmail.com',\n", + " 'password': 'elxr mdyx jzch wrns',\n", + "}\n", + "\n", + "EMAIL_RECIPIENTS = {\n", + " 'test': ['namtran6701@gmail.com', 'victorm@hamalsolutions.com'],\n", + " 'production': ['namtran6701@gmail.com', 'nam.tran@salesfactory.com', 'nam3864779@gmail.com']\n", + "} " + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [], + "source": [ + "#! put in utils library \n", + "from typing import Iterable, Optional\n", + "\n", + "def load_html_template(template_path: str) -> str:\n", + " with open(template_path, 'r') as file:\n", + " return file.read()\n", + " \n", + "def send_email(subject: str, \n", + " html_content: str, \n", + " recipients: Iterable[str], \n", + " attachment_path: Optional[str] = None) -> None:\n", + " \n", + " # create the email message\n", + " \n", + " msg = EmailMessage()\n", + "\n", + " msg['Subject'] = subject\n", + " msg['From'] = EMAIL_CONFIG['email']\n", + " msg['Bcc'] = ','.join(recipients)\n", + " msg.add_alternative(html_content, subtype='html')\n", + "\n", + " # Attach file if provided \n", + " if attachment_path:\n", + " with open(attachment_path, 'rb') as file:\n", + " file_data = file.read()\n", + " file_name = attachment_path.split('\\\\')[-1]\n", + " msg.add_attachment(file_data, \n", + " maintype='application', \n", + " subtype='octet-stream', \n", + " filename=file_name)\n", + "\n", + " # send the email \n", + " with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['port']) as server:\n", + " server.starttls()\n", + " server.login(EMAIL_CONFIG['email'], EMAIL_CONFIG['password'])\n", + " server.send_message(msg)\n", + " logger.info(f'Email sent to {recipients}')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:48:08,829 - __main__ - INFO - Email sent to ['namtran6701@gmail.com', 'victorm@hamalsolutions.com']\n" + ] + } + ], + "source": [ + "send_email(subject='Test Email', \n", + " html_content=email_html_content, \n", + " recipients=EMAIL_RECIPIENTS['test'])\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-12-31 08:42:16,181 - __main__ - INFO - Cleaned up the blob downloads directory\n", + "2024-12-31 08:42:16,183 - __main__ - INFO - Cleaned up the email_html_content.html file\n" + ] + } + ], + "source": [ + "# clean up resources \n", + "import shutil \n", + "\n", + "# rempve the blob downloads directory and its contents \n", + "blob_downloads_path = Path(os.getcwd())/'blob_downloads'\n", + "\n", + "# remove the blob downloads directory and its contents \n", + "if blob_downloads_path.exists():\n", + " shutil.rmtree(blob_downloads_path)\n", + " logger.info('Cleaned up the blob downloads directory')\n", + "\n", + "# remove the email_html_content.html file \n", + "email_html_path = Path(os.getcwd())/'email_html_content.html'\n", + "\n", + "if email_html_path.exists():\n", + " email_html_path.unlink()\n", + " logger.info('Cleaned up the email_html_content.html file')\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/backend/report_email_templates/email_templates.py b/backend/report_email_templates/email_templates.py new file mode 100644 index 00000000..a475338b --- /dev/null +++ b/backend/report_email_templates/email_templates.py @@ -0,0 +1,61 @@ +from typing import List, Dict, Literal +from pathlib import Path +import jinja2 + +class EmailRenderError(Exception): + """Exception raised for errors in email rendering. """ + def __init__(self, message: str): + self.message = message + super().__init__(self.message) + +class EmailTemplateManager: + """Manages email template rendering. """ + + def __init__(self): + template_dir = Path(__file__).parent / 'html' + self.env = jinja2.Environment( + loader=jinja2.FileSystemLoader(str(template_dir)), + autoescape = jinja2.select_autoescape(['html', 'xml']) + ) + def render_report_template( + self, + title: str, + intro_text: str, + key_points: List[Dict[str, str]], + why_it_matters: str, + document_type: Literal["WeeklyEconomics", "CompanyAnalysis", "CreativeBrief", "Ecommerce", "MonthlyMacroeconomics"] + ) -> str: + + """ + Render the report email template with provided content. + + Args: + title: Main title of the report + intro_text: Introductory text + key_points: List of dictionaries containing 'title' and 'content' + why_it_matters: Why this information matters section + document_type: Type of document for the chat link + + Returns: + str: Rendered HTML content + """ + try: + template = self.env.get_template('report_email.html') + return template.render( + title=title, + intro_text=intro_text, + key_points=key_points, + why_it_matters=why_it_matters, + document_type=document_type + ) + except Exception as e: + raise EmailRenderError(f"Error rendering email template: {str(e)}") + + + +""" +The next step is: + +- load the report content from the blob link to model context in the report template +- the financial agent will use the blob link to get the report content and load it to the model's context +""" \ No newline at end of file diff --git a/backend/report_email_templates/html/report_email.html b/backend/report_email_templates/html/report_email.html new file mode 100644 index 00000000..5261be55 --- /dev/null +++ b/backend/report_email_templates/html/report_email.html @@ -0,0 +1,60 @@ + + + + + + {{ title }} + + +
    + +
    + + \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index e38ce669..c0a89e4e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -30,4 +30,7 @@ Flask-Session==0.8.0 langgraph==0.2.60 markdown2==2.5.2 langchain-openai==0.2.14 -pydantic-settings==2.7.0 \ No newline at end of file +pydantic-settings==2.7.0 +markdown +pydantic[email] +weasyprint==63.1 \ No newline at end of file diff --git a/backend/rp2email.py b/backend/rp2email.py new file mode 100644 index 00000000..bc885a68 --- /dev/null +++ b/backend/rp2email.py @@ -0,0 +1,441 @@ +import os +import logging +import markdown +from pathlib import Path +from typing import Literal, List, Dict, Optional, Any + +from pydantic import BaseModel, Field, EmailStr + +from report_email_templates.email_templates import EmailTemplateManager +from llm_config import LLMManager +from financial_doc_processor import BlobStorageManager + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +TEMP_DIR = "blob_downloads" +EMAIL_ENDPOINT = '/api/reports/email' +PDF_OUTPUT_NAME = "report.pdf" + +#################################### +# Pydantic Models +#################################### + +class KeyPoint(BaseModel): + title: str = Field(..., description = "The title of the key point") + content: str = Field(..., description= "Detailed content of the key point") + + def to_dict(self) -> Dict[str, str]: + """Convert KeyPoint to dictionary format """ + return { + "title": self.title, + "content": self.content + } + +class EmailSchema(BaseModel): + title: str = Field(..., description = "Title of the report") + intro_text: str = Field(..., description = "Introductory text below the title") + keypoints: List[KeyPoint] = Field(..., description = "3 lists of key points from the report") + why_it_matters: str = Field(..., description = "The 'Why it matters' section") + document_type: Literal["WeeklyEconomics", "CompanyAnalysis", "CreativeBrief", "Ecommerce", "MonthlyMacroeconomics"] = Field(..., description="The type of the document") + + def get_keypoints_dict(self) -> List[Dict[str, str]]: + """Convert keypoints to dictionary format """ + return [point.to_dict() for point in self.keypoints] + + +class UserEmailPayload(BaseModel): + email_blob_link: str = Field(..., description="The blob link to the email html file") + recipients: List[EmailStr] = Field(..., description="The list of recipients") + subject: str = Field(..., description="The subject of the email") + attachment_path: Optional[str] = Field(..., description="The attachment path") + +class AdminEmailPayload(BaseModel): + report_blob_link: str = Field(..., description= "The blob link to the report html file") + admin_recipients: List[EmailStr] = Field(..., description="The list of admin recipients") + subject: str = Field(..., description = "The subject of the email") + attachment_path: Optional[str] = Field(..., description = " Any attachment to be attached to the email") +#################################### +# Custom Exceptions +#################################### + +class BlobServiceError(Exception): + """Base exception for blob service operations""" + pass + +class BlobDownloadError(BlobServiceError): + """Failed to download blob from URL""" + pass + +class BlobUploadError(BlobServiceError): + """Failed to upload blob to blob storage""" + pass + +class BlobFileNotFoundError(BlobServiceError): + """Blob file not found in downloads directory""" + pass + +class ReportProcessingError(Exception): + """Error processing the report""" + pass + +class EmailSendingError(Exception): + """Error sending the email""" + pass + + +#################################### +# Report Processor +#################################### +from contextlib import contextmanager +from typing import Generator +class ReportProcessor: + """Process reports and conver them to email format. """ + + def __init__(self, blob_link: str): + """ + Initialize report processor. + + Args: + blob_link (str): Link to the report blob + """ + + self.blob_link = blob_link + self.blob_manager = BlobStorageManager() + self.llm_manager = LLMManager() + self.template_manager = EmailTemplateManager() + self.downloaded_file: Optional[Path] = None + + @contextmanager + def _resource_cleanup(self) -> Generator[None, None, None]: + """Context manager to clean up resources after processing.""" + try: + yield + finally: + self.cleanup() + + def process(self) -> Dict[str, Any]: + """ + Process a report from blob storage into an email-friendly format. + + This method performs several steps: + 1. Downloads and reads the HTML report content from blob storage + 2. Converts the HTML content to a PDF file + 3. Generates a summary of the report using LLM + 4. Parses the summary into a structured email schema + 5. Renders the email content using an HTML template + + Returns: + Dict[str, Any]: A dictionary containing: + - subject (str): The email subject line derived from the report title + - html_content (str): The rendered HTML email body + - document_type (str): The type of document/report + - attachment_path (str): Local filesystem path to the PDF version + + Raises: + ReportProcessingError: If any step in the processing pipeline fails + BlobServiceError: If downloading the report from blob storage fails + FileNotFoundError: If the downloaded report file cannot be found + """ + try: + # download and read report + logger.info("Downloading report from blob link") + html_content = self._get_report_content() + + # save the html content to a pdf file + pdf_path = self.html_to_pdf(html_content, f"{TEMP_DIR}/{PDF_OUTPUT_NAME}") + + # summarize the report + logger.info("Summarizing the report") + summary = self._summarize_report(html_content) + + # parse to email schema + logger.info("Parsing the report to email schema") + email_data = self._parse_report_to_email_schema(summary) + + # generate HTML email from schema and template + logger.info("Generating HTML email content") + email_html = self.template_manager.render_report_template( + title=email_data.title, + intro_text=email_data.intro_text, + key_points=email_data.get_keypoints_dict(), + why_it_matters=email_data.why_it_matters, + document_type=email_data.document_type + ) + + return { + "subject": email_data.title, + "html_content": email_html, + "document_type": email_data.document_type, + "attachment_path": str(pdf_path) + } + + except Exception as e: + logger.exception("Error processing the report") + raise ReportProcessingError(f"Error processing the report: {str(e)}") + + def _get_report_content(self) -> str: + """Download and read the report content from the blob link. """ + try: + # download blob from link + self.downloaded_file = self.blob_manager.download_blob_from_a_link(self.blob_link) + + # get the file within blob downloads + html_file_path = next(Path(os.getcwd()).glob(f'{TEMP_DIR}/*.html')) + + # read content + if html_file_path.exists(): + with open(html_file_path, 'r', encoding='utf-8') as file: + html_content = file.read() + logger.info("Successfully imported the HTML file") + return html_content + else: + raise FileNotFoundError(f"HTML file not found: {html_file_path}") + + except Exception as e: + logger.exception(f"Error downloading and reading the report: {str(e)}") + raise + + def _summarize_report(self, html_content: str) -> str: + """Summarize the report using the LLM. """ + try: + llm = self.llm_manager.get_client( + client_type='gpt4o', + use_langchain=True + ) + sys_prompt = self.llm_manager.get_prompt(prompt_type='email_template') + + prompt = sys_prompt.format(report_content=html_content) + + # summarize the report + summary = llm.invoke(prompt) + + if not summary.content: + raise ReportProcessingError("Failed to generate summary") + + return summary.content + + except Exception as e: + logger.exception(f"Error summarizing the report: {str(e)}") + raise + + def _upload_email_to_blob(self, email_html: str) -> str: + """Upload the email html to the blob storage.""" + temp_dir = Path('temp_emails') + temp_file = None + + try: + # Generate a unique ID for the email + import uuid + from datetime import datetime, timezone + email_id = str(uuid.uuid4()) + + # Create a temporary file with the email content + temp_dir.mkdir(exist_ok=True) + temp_file = temp_dir / f"{email_id}.html" + + with open(temp_file, 'w', encoding='utf-8') as f: + f.write(email_html) + + # Upload to blob storage + result = self.blob_manager.upload_to_blob( + file_path=str(temp_file), + blob_folder='FA_emails', + metadata={ + 'email_id': email_id, + 'timestamp': datetime.now(timezone.utc).isoformat() + } + ) + + if result['status'] == 'success': + return result['blob_url'] + else: + raise BlobUploadError(f"Failed to upload email: {result.get('error')}") + + except Exception as e: + logger.exception(f"Error uploading email to blob: {str(e)}") + raise + + finally: + # Clean up resources regardless of success or failure + if temp_file and temp_file.exists(): + try: + temp_file.unlink() + except Exception: + logger.warning(f"Failed to delete temporary file: {temp_file}") + + if temp_dir.exists(): + try: + temp_dir.rmdir() + except Exception: + logger.warning(f"Failed to remove temporary directory: {temp_dir}") + + + def _parse_report_to_email_schema(self, summary: str) -> EmailSchema: + """Parse the report summary into the email schema. """ + try: + llm = self.llm_manager.get_client( + client_type='gpt4o', + use_langchain=True + ) + llm_report_parser = llm.with_structured_output(EmailSchema) + return llm_report_parser.invoke(summary) + + except Exception as e: + logger.exception(f"Error parsing the report to email schema: {str(e)}") + raise + + def html_to_pdf(self, html_content: str, output_path: str) -> Path: + """Convert the html content to a pdf file.""" + from weasyprint import HTML + HTML(string=html_content).write_pdf(output_path) + return Path(output_path) + + def cleanup(self) -> None: + """Clean up temporary files. """ + try: + if isinstance(self.downloaded_file, Path) and self.downloaded_file.exists(): + self.downloaded_file.unlink(missing_ok=True) + logger.info("Cleaned up downloaded file") + + # clean up the blob downloads directory + blob_downloads = Path(os.getcwd()) / f'{TEMP_DIR}' + if blob_downloads.exists(): + import shutil + shutil.rmtree(blob_downloads) + logger.info("Cleaned up blob downloads directory") + + except Exception as e: + logger.exception(f"Error cleaning up resources: {str(e)}") + + +#################################### +# Send Email +#################################### +from flask import current_app + +def send_email( + email_data: Dict[str, Any], + recipients: List[str], + attachment_path: Optional[str] = None, + email_subject: Optional[str] = None +) -> bool: + """Send an email to the recipients. + + Args: + email_data: Dictionary containing email content + recipients: List of recipients + attachment_path: Path to the attachment file (local path) + email_subject: Subject of the email + + Returns: + bool: True if the email is sent successfully, False otherwise. + """ + + # todo: allow attachment to be a blob link + # validate input + if not recipients: + raise ValueError("Recipients list is empty") + if not all(isinstance(r, str) and '@' in r for r in recipients): + raise ValueError("Recipients list contains invalid email addresses") + + try: + # prepare email payload + payload = { + "subject": email_data["subject"], + "html_content": email_data["html_content"], + "recipients": recipients, + "attachment_path": email_data["attachment_path"] + } + + # overwrite the attachment path if provided + if attachment_path: + payload["attachment_path"] = attachment_path + + # if attachment path is 'no', set it to None + if attachment_path.lower() == 'no': + payload["attachment_path"] = None + + if email_subject: + payload["subject"] = email_subject + + # send email using the endpoint + with current_app.test_client() as client: + response = client.post(EMAIL_ENDPOINT, json=payload) + + if response.status_code == 200: + logger.info("Email sent successfully") + return True + + error_data = response.get_json() + error_message = error_data.get('message', 'Unknown error') + logger.error(f"Failed to send email. Status code: {response.status_code}. Error: {error_message}") + raise EmailSendingError(error_message) # Raise the custom exception + + except EmailSendingError: + raise # Re-raise email-specific errors + except Exception as e: + logger.exception(f"Error sending email: {str(e)}") + raise EmailSendingError(f"Unexpected error while sending email: {str(e)}") +def process_and_send_email(blob_link: str, + recipients: List[str], + attachment_path: Optional[str] = None, + email_subject: Optional[str] = None + ) -> bool: + """ + Process the report and send the email. + + Args: + blob_link: Link to the report blob + recipients: List of recipients + + Returns: + bool: True if the email is sent successfully, False otherwise. + """ + processor = None + success = False + processor = ReportProcessor(blob_link) + + with processor._resource_cleanup(): + try: + # initialize and process report + email_data = processor.process() + # send email + success = send_email(email_data, recipients, attachment_path, email_subject) + return success + + except Exception as e: + logger.exception(f"Error processing and sending email: {str(e)}") + return False + + + +# """ + +# Requirements for this endpoint: +# - Blob link from the report +# - List of recipients +# - Optional: attachment path and email subject +# - email_subject: subject of the email + + + +# What does this endpoint do? + +# 1. Download the report from the blob link -> that's why we need the blob link +# 2. Open the report and extract the content from the local path +# 3. Summarize the report -> that's why we need to initialize the llm and the prompt +# 4. parse the summary into the email schema -> use llm to parse +# 5. render the email template -> use the email template manager to render the email template with jinja2 +# 6. pass the formatted email to the email service endpoint +# 7. send the email to the recipients +# --------------------------------------------------- +# What do I need for the email service endpoint? ? +# 1. List of recipients +# 2. Subject of the email +# 3. Link to the blob report content (original). Get the link where it is downloaded +# 4. html content for the email (formatted) +# """ \ No newline at end of file diff --git a/backend/utils.py b/backend/utils.py index 25c4e75d..6fd818c4 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -368,7 +368,16 @@ def send_email(self, attachment_path: Optional[str] = None) -> None: """ - # TODO: provide docstring + Send an email to the recipients. + + Args: + subject (str): Subject of the email + html_content (str): HTML content of the email + recipients (List[str]): List of recipients + attachment_path (Optional[str]): Path to the attachment file + + Returns: + None """ msg: EmailMessage = EmailMessage() @@ -378,14 +387,24 @@ def send_email(self, msg.add_alternative(html_content, subtype='html') if attachment_path: - with open(attachment_path, 'rb') as file: - file_data = file.read() - file_name = attachment_path.split('\\')[-1] - msg.add_attachment(file_data, - maintype='application', - subtype='octet-stream', - filename=file_name) - + try: + # convert to path object and resolve to absolute path + file_path = Path(attachment_path).resolve() + # validate file exists and is accessible + if not file_path.exists(): + raise EmailServiceError(f"File not found: {attachment_path}") + + with open(file_path, 'rb') as file: + file_data = file.read() + file_name = file_path.name + msg.add_attachment(file_data, + maintype='application', + subtype='octet-stream', + filename=file_name) + except (OSError, EmailServiceError) as e: + logger.error(f"Error adding attachment: {str(e)}") + raise EmailServiceError(f"Error adding attachment: {str(e)}") + try: with smtplib.SMTP(self.smtp_server, self.smtp_port) as server: server.starttls() @@ -395,3 +414,4 @@ def send_email(self, except Exception as e: logger.error(f'Failed to send email: {str(e)}') raise EmailServiceError(f'Failed to send email: {str(e)}') + \ No newline at end of file From 835fff31def2331677bb64fba5e205846c2ac46d Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:31:43 -0500 Subject: [PATCH 270/820] Fa 334 save email to blobs (#245) * create monht_year folder for weekly curation report * Create an endpoint distribution email * feat(email): Add email notification system with HTML templates for financial reports * feat(email): Add process_and_send_email endpoint and enhance email sending functionality - Introduced a new endpoint `/api/reports/process-and-email` to process reports and send email notifications. - Updated `send_email_endpoint` to clarify attachment path requirements. - Enhanced error handling and logging in email processing and sending functions. - Refactored email service to improve attachment handling and validation. - Added custom exceptions for better error management in blob service operations. * wkh * plan to integrate report content with financial agent * add some utils and minor fixes * fix merge conflicts * remove some comments * change email host and port env * save email to blob storage * use smtp_ssl instead of smtp for email service --- backend/app.py | 43 +++++++++--- backend/rp2email.py | 13 ++-- backend/utils.py | 165 +++++++++++++++++++++++++++++--------------- 3 files changed, 152 insertions(+), 69 deletions(-) diff --git a/backend/app.py b/backend/app.py index 2bc6d21c..0bef882e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -65,7 +65,7 @@ update_user ) -load_dotenv() +load_dotenv(override=True) SPEECH_REGION = os.getenv("SPEECH_REGION") ORCHESTRATOR_ENDPOINT = os.getenv("ORCHESTRATOR_ENDPOINT") @@ -2832,6 +2832,7 @@ def send_email_endpoint(): "html_content": "HTML formatted content", "recipients": ["email1@domain.com", "email2@domain.com"], "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. + "save_email": "yes" # Optional, default is "no" } Returns: @@ -2882,9 +2883,9 @@ def send_email_endpoint(): # Update the attachment_path in data data['attachment_path'] = str(attachment_path) - # Validate email configuration + # Validate email configuration email_config = { - 'smtp_server': os.getenv('EMAIL_HOST'), + 'smtp_server': os.getenv('EMAIL_HOST'), 'smtp_port': os.getenv('EMAIL_PORT'), 'username': os.getenv('EMAIL_USER'), 'password': os.getenv('EMAIL_PASS') @@ -2899,16 +2900,29 @@ def send_email_endpoint(): # Initialize and send email email_service = EmailService(**email_config) - email_service.send_email( - subject=data['subject'], - html_content=data['html_content'], - recipients=data['recipients'], - attachment_path=data.get('attachment_path') - ) + + email_params = { + "subject": data['subject'], + "html_content": data['html_content'], + "recipients": data['recipients'], + "attachment_path": data.get('attachment_path') + } + + # send the email + email_service.send_email(**email_params) + + # save the email to blob storage + if data.get('save_email', 'no').lower() == 'yes': + blob_name = email_service._save_email_to_blob(**email_params) + logger.info(f"Email has been saved to blob storage: {blob_name}") + else: + logger.info("Email has not been saved to blob storage because save_email is set to no") + blob_name = None return jsonify({ 'status': 'success', - 'message': 'Email sent successfully' + 'message': 'Email sent successfully', + 'blob_name': blob_name }), 200 except EmailServiceError as e: @@ -2917,6 +2931,13 @@ def send_email_endpoint(): 'status': 'error', 'message': f'Failed to send email: {str(e)}' }), 500 + + except BlobUploadError as e: + logger.error(f"Blob upload error: {str(e)}") + return jsonify({ + 'status': 'error', + 'message': f'Email has been sent, but failed to upload to blob storage: {str(e)}' + }), 500 except Exception as e: logger.exception("Unexpected error in send_email_endpoint") @@ -2938,6 +2959,7 @@ def digest_report(): "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. By default, it will automatically attach the document from the blob link (PDF converted). Select "no" to disable this feature. "email_subject": "Custom email subject" # Optional + "save_email": "yes" # Optional, default is "yes" } """ try: @@ -2962,6 +2984,7 @@ def digest_report(): recipients=data['recipients'], attachment_path=data.get('attachment_path', None), email_subject=data.get('email_subject', None), + save_email=data.get('save_email', 'yes') ) if success: diff --git a/backend/rp2email.py b/backend/rp2email.py index bc885a68..80c268d8 100644 --- a/backend/rp2email.py +++ b/backend/rp2email.py @@ -321,7 +321,8 @@ def send_email( email_data: Dict[str, Any], recipients: List[str], attachment_path: Optional[str] = None, - email_subject: Optional[str] = None + email_subject: Optional[str] = None, + save_email: Optional[str] = "yes" ) -> bool: """Send an email to the recipients. @@ -330,6 +331,7 @@ def send_email( recipients: List of recipients attachment_path: Path to the attachment file (local path) email_subject: Subject of the email + save_email: Whether to save the email to blob storage Returns: bool: True if the email is sent successfully, False otherwise. @@ -348,7 +350,8 @@ def send_email( "subject": email_data["subject"], "html_content": email_data["html_content"], "recipients": recipients, - "attachment_path": email_data["attachment_path"] + "attachment_path": email_data["attachment_path"], + "save_email": save_email } # overwrite the attachment path if provided @@ -380,10 +383,12 @@ def send_email( except Exception as e: logger.exception(f"Error sending email: {str(e)}") raise EmailSendingError(f"Unexpected error while sending email: {str(e)}") + def process_and_send_email(blob_link: str, recipients: List[str], attachment_path: Optional[str] = None, - email_subject: Optional[str] = None + email_subject: Optional[str] = None, + save_email: Optional[str] = "yes" ) -> bool: """ Process the report and send the email. @@ -404,7 +409,7 @@ def process_and_send_email(blob_link: str, # initialize and process report email_data = processor.process() # send email - success = send_email(email_data, recipients, attachment_path, email_subject) + success = send_email(email_data, recipients, attachment_path, email_subject, save_email) return success except Exception as e: diff --git a/backend/utils.py b/backend/utils.py index 6fd818c4..23964d24 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -349,69 +349,124 @@ class EmailServiceError(Exception): pass class EmailService: - def __init__( - self, - smtp_server: str, - smtp_port: int, - username: str, - password: str, - ): + def __init__(self, smtp_server, smtp_port, username, password): self.smtp_server = smtp_server - self.smtp_port = smtp_port + self.smtp_port = int(smtp_port) self.username = username self.password = password + self._server = None - def send_email(self, - subject: str, - html_content: str, - recipients: List[str], - attachment_path: Optional[str] = None) -> None: + def _get_server(self): + """Get or create SMTP server connection with SSL""" + if self._server is None: + try: + # Use SMTP_SSL instead of SMTP + server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port, timeout=30) + server.login(self.username, self.password) + self._server = server + except Exception as e: + logger.error(f"Failed to create SMTP connection: {str(e)}") + raise EmailServiceError(f"SMTP connection failed: {str(e)}") + return self._server + + def send_email(self, subject, html_content, recipients, attachment_path=None): + max_retries = 3 + retry_delay = 2 # seconds + import time + + for attempt in range(max_retries): + try: + msg = EmailMessage() + msg['Subject'] = subject + msg['From'] = self.username + msg['Bcc'] = ','.join(recipients) + msg.add_alternative(html_content, subtype='html') - """ - Send an email to the recipients. + if attachment_path: + self._add_attachment(msg, attachment_path) - Args: - subject (str): Subject of the email - html_content (str): HTML content of the email - recipients (List[str]): List of recipients - attachment_path (Optional[str]): Path to the attachment file + server = self._get_server() + server.send_message(msg) + return # Success, exit the function + + except smtplib.SMTPServerDisconnected: + logger.warning(f"SMTP server disconnected (attempt {attempt + 1}/{max_retries})") + self._server = None # Reset the connection + if attempt < max_retries - 1: + time.sleep(retry_delay) + continue + raise EmailServiceError("Failed to maintain SMTP connection after multiple attempts") + + except Exception as e: + logger.error(f"Error sending email (attempt {attempt + 1}/{max_retries}): {str(e)}") + if attempt < max_retries - 1: + time.sleep(retry_delay) + continue + raise EmailServiceError(f"Failed to send email: {str(e)}") + + def _add_attachment(self, msg, attachment_path): + """Add an attachment to the email message""" + try: + # convert to path object and resolve to absolute path + file_path = Path(attachment_path).resolve() + # validate file exists and is accessible + if not file_path.exists(): + raise EmailServiceError(f"File not found: {attachment_path}") + + with open(file_path, 'rb') as file: + file_data = file.read() + file_name = file_path.name + msg.add_attachment(file_data, + maintype='application', + subtype='octet-stream', + filename=file_name) + except (OSError, EmailServiceError) as e: + logger.error(f"Error adding attachment: {str(e)}") + raise EmailServiceError(f"Error adding attachment: {str(e)}") + - Returns: - None + def _save_email_to_blob(self, + html_content: str, + subject: str, + recipients: List[str], + attachment_path: Optional[str] = None) -> str: """ + Save the email content to a blob storage container + """ + EMAIL_CONTAINER_NAME = 'emails' + from azure.storage.blob import BlobServiceClient + from datetime import datetime, timezone + from azure.storage.blob import ContentSettings - msg: EmailMessage = EmailMessage() - msg['Subject'] = subject - msg['From'] = self.username - msg['Bcc'] = ','.join(recipients) - msg.add_alternative(html_content, subtype='html') - - if attachment_path: - try: - # convert to path object and resolve to absolute path - file_path = Path(attachment_path).resolve() - # validate file exists and is accessible - if not file_path.exists(): - raise EmailServiceError(f"File not found: {attachment_path}") - - with open(file_path, 'rb') as file: - file_data = file.read() - file_name = file_path.name - msg.add_attachment(file_data, - maintype='application', - subtype='octet-stream', - filename=file_name) - except (OSError, EmailServiceError) as e: - logger.error(f"Error adding attachment: {str(e)}") - raise EmailServiceError(f"Error adding attachment: {str(e)}") + blob_service_client = BlobServiceClient.from_connection_string(os.getenv('BLOB_CONNECTION_STRING')) + blob_container_client = blob_service_client.get_container_client(EMAIL_CONTAINER_NAME) + # get the blob storage container + from financial_doc_processor import BlobUploadError + import uuid + + + # create an id for the email + email_id = str(uuid.uuid4()) + + # create a blob name for the email + blob_name = f"{email_id}/content.html" + + # add metadata to the blob + metadata = { + "email_id": email_id, + "subject": subject, + "created_at": datetime.now(timezone.utc).isoformat(), + "recipients": ', '.join(recipients), + "has_attachment": str(bool(attachment_path)) + } + + # upload the email content to the blob try: - with smtplib.SMTP(self.smtp_server, self.smtp_port) as server: - server.starttls() - server.login(self.username, self.password) - server.send_message(msg) - logger.info(f'Email sent to {recipients}') - except Exception as e: - logger.error(f'Failed to send email: {str(e)}') - raise EmailServiceError(f'Failed to send email: {str(e)}') - \ No newline at end of file + blob_container_client.upload_blob(blob_name, html_content, metadata=metadata, content_settings=ContentSettings(content_type='text/html')) + except BlobUploadError as e: + logger.error(f"Error uploading email to blob: {str(e)}") + raise BlobUploadError(f"Error uploading email to blob: {str(e)}") + + # return the blob name + return blob_name From 030ad3895be315aab74774e5079c02b6aeb141e3 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 15:09:22 -0500 Subject: [PATCH 271/820] add logging for recipients --- backend/rp2email.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/rp2email.py b/backend/rp2email.py index 80c268d8..6f3c8ecb 100644 --- a/backend/rp2email.py +++ b/backend/rp2email.py @@ -371,6 +371,8 @@ def send_email( if response.status_code == 200: logger.info("Email sent successfully") + # log recipients + logger.info(f"Recipients: {recipients}") return True error_data = response.get_json() From ec1b650e6abd6e136a8c65cd03d02f3b5adfbcc4 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 15:23:14 -0500 Subject: [PATCH 272/820] add timestamps when emails sent successfully --- backend/rp2email.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/rp2email.py b/backend/rp2email.py index 6f3c8ecb..6dbf25b8 100644 --- a/backend/rp2email.py +++ b/backend/rp2email.py @@ -316,7 +316,7 @@ def cleanup(self) -> None: # Send Email #################################### from flask import current_app - +from datetime import datetime, timezone def send_email( email_data: Dict[str, Any], recipients: List[str], @@ -370,7 +370,7 @@ def send_email( response = client.post(EMAIL_ENDPOINT, json=payload) if response.status_code == 200: - logger.info("Email sent successfully") + logger.info(f"Email sent successfully at {datetime.now(timezone.utc)}") # log recipients logger.info(f"Recipients: {recipients}") return True From 3bb65989ba4638ff3884f5faa9752552addd706c Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Mon, 6 Jan 2025 22:22:24 -0500 Subject: [PATCH 273/820] Update start.sh add system dependencies for weasyprint --- start.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/start.sh b/start.sh index 96911451..c7293a25 100755 --- a/start.sh +++ b/start.sh @@ -5,6 +5,18 @@ echo "starting script" export $(grep -v '^#' .env | xargs) +# Install system dependencies for PDF processing +echo "Installing system dependencies" +sudo apt-get update +sudo apt-get install -y \ + libgobject-2.0-0 \ + libcairo2 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info + # echo "" # echo "Loading azd .env file from current environment" # echo "" @@ -67,4 +79,4 @@ xdg-open http://127.0.0.1:8000 if [ $? -ne 0 ]; then echo "Failed to start backend" exit $? -fi \ No newline at end of file +fi From b52a6ae85a89e64fdf2c8d0e7b7943b9827b8ac1 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 22:58:39 -0500 Subject: [PATCH 274/820] update start.sh --- start.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/start.sh b/start.sh index c7293a25..db35e60d 100755 --- a/start.sh +++ b/start.sh @@ -15,7 +15,9 @@ sudo apt-get install -y \ libpangocairo-1.0-0 \ libgdk-pixbuf2.0-0 \ libffi-dev \ - shared-mime-info + shared-mime-info \ + libharfbuzz0b \ + libpangoft2-1.0-0 # echo "" # echo "Loading azd .env file from current environment" From 7dd60fcb5c7de81b1e6887fb2be44d0346741400 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 23:11:51 -0500 Subject: [PATCH 275/820] dependencies for pdf processing --- .github/workflows/azure-dev.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 42516df8..a8fbf011 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -14,8 +14,19 @@ jobs: - name: Checkout Code uses: actions/checkout@v3 - - name: Install zip - run: sudo apt-get update && sudo apt-get install -y zip + - name: Install Dependencies + run: | + sudo apt-get update && sudo apt-get install -y \ + zip \ + libpango-1.0-0 \ + libharfbuzz0b \ + libpangoft2-1.0-0 \ + libgobject-2.0-0 \ + libcairo2 \ + libpangocairo-1.0-0 \ + libgdk-pixbuf2.0-0 \ + libffi-dev \ + shared-mime-info - name: Azure Login uses: azure/login@v2 From fac61ebb65f3d21cafe7e057c25d8acf0792405d Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 23:27:54 -0500 Subject: [PATCH 276/820] update weasyprint dependencies --- .github/workflows/azure-dev.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index a8fbf011..6583da48 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -21,12 +21,10 @@ jobs: libpango-1.0-0 \ libharfbuzz0b \ libpangoft2-1.0-0 \ - libgobject-2.0-0 \ - libcairo2 \ - libpangocairo-1.0-0 \ - libgdk-pixbuf2.0-0 \ + libharfbuzz-subset0 \ libffi-dev \ - shared-mime-info + libjpeg-dev \ + libopenjp2-7-dev - name: Azure Login uses: azure/login@v2 From d2a6f1e9c8e517d19225a06ec8e66927b069d39c Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 23:34:56 -0500 Subject: [PATCH 277/820] added libgobject-2.0-0 --- .github/workflows/azure-dev.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 6583da48..9ab3c430 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -24,7 +24,9 @@ jobs: libharfbuzz-subset0 \ libffi-dev \ libjpeg-dev \ - libopenjp2-7-dev + libopenjp2-7-dev \ + libgobject-2.0-0 \ + libglib2.0-0 - name: Azure Login uses: azure/login@v2 From 07aca92d931f96eb68dca421e48a3ea624a1adf8 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Mon, 6 Jan 2025 23:40:55 -0500 Subject: [PATCH 278/820] update start.sh --- start.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/start.sh b/start.sh index db35e60d..67ee087c 100755 --- a/start.sh +++ b/start.sh @@ -9,15 +9,15 @@ export $(grep -v '^#' .env | xargs) echo "Installing system dependencies" sudo apt-get update sudo apt-get install -y \ - libgobject-2.0-0 \ - libcairo2 \ libpango-1.0-0 \ - libpangocairo-1.0-0 \ - libgdk-pixbuf2.0-0 \ - libffi-dev \ - shared-mime-info \ libharfbuzz0b \ - libpangoft2-1.0-0 + libpangoft2-1.0-0 \ + libharfbuzz-subset0 \ + libffi-dev \ + libjpeg-dev \ + libopenjp2-7-dev \ + libgobject-2.0-0 \ + libglib2.0-0 # echo "" # echo "Loading azd .env file from current environment" From 6ac32e6b28a441a23d955ad252e56fc9d26f9471 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Tue, 7 Jan 2025 00:01:26 -0500 Subject: [PATCH 279/820] troubleshoot weasyprint --- .github/workflows/azure-dev.yml | 11 +++++++++-- start.sh | 12 +++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 9ab3c430..99baa04d 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -25,8 +25,15 @@ jobs: libffi-dev \ libjpeg-dev \ libopenjp2-7-dev \ - libgobject-2.0-0 \ - libglib2.0-0 + libglib2.0-0 \ + libglib2.0-dev \ + libcairo2 \ + libcairo2-dev \ + libpangocairo-1.0-0 \ + pkg-config \ + python3-dev \ + python3-cffi \ + libgobject-2.0-0 - name: Azure Login uses: azure/login@v2 diff --git a/start.sh b/start.sh index 67ee087c..1b64aa18 100755 --- a/start.sh +++ b/start.sh @@ -16,8 +16,18 @@ sudo apt-get install -y \ libffi-dev \ libjpeg-dev \ libopenjp2-7-dev \ + libglib2.0-0 \ + libglib2.0-dev \ + libcairo2 \ + libcairo2-dev \ + libpangocairo-1.0-0 \ + pkg-config \ + python3-dev \ + python3-cffi \ libgobject-2.0-0 \ - libglib2.0-0 + fonts-liberation \ + libgdk-pixbuf2.0-0 \ + shared-mime-info # echo "" # echo "Loading azd .env file from current environment" From d068fae22cb8996e05004073119391704bdee209 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Tue, 7 Jan 2025 12:52:41 -0500 Subject: [PATCH 280/820] fix bug for SECEdgar API --- backend/financial_doc_processor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 008e8c8c..3d9130e0 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -735,8 +735,13 @@ def download_filing(self, equity_id: str, filing_type: str, after_date: str = No # Convert to string format expected by SEC EDGAR formatted_date = utc_date.strftime('%Y-%m-%d') + # today + today = datetime.now(timezone.utc) + # Add one day - next_date = parsed_date + timedelta(days=1) + tomorrow = today + timedelta(days=1) + + tomorrow_str = tomorrow.strftime('%Y-%m-%d') logger.info(f"Downloading {filing_type} for {equity_id} after {formatted_date}") num_downloaded_file = self.dl.get( @@ -745,7 +750,7 @@ def download_filing(self, equity_id: str, filing_type: str, after_date: str = No limit=1, download_details=True, after=formatted_date, - before = next_date + before = tomorrow_str # avoid afterdate is greater than before date error ) if num_downloaded_file == 0: From 5f10e3ef6405100b3e69d78e60aa21da1a55043d Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 7 Jan 2025 15:17:25 -0400 Subject: [PATCH 281/820] update README with system dependencies for PDF processing and remove installation from start.sh --- README.md | 37 +++++++++++++++++++++++++++++++++++++ start.sh | 25 ------------------------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index f6957793..7ad1fe9b 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,43 @@ If you plan to customize the ingestion logic, create a new repo by clicking on t If you created a new repository please update the repository URL before running the git clone command +## Install system dependencies for PDF processing +If you are working on Windows you can skip this step. + +### Mac OS +``` +brew update +brew install weasyprint +``` +In case of errors with the instalation you can run +``` +export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_FALLBACK_LIBRARY_PATH +``` +### Linux (Devian-based) +``` +sudo apt-get update +sudo apt-get install -y \ + libpango-1.0-0 \ + libharfbuzz0b \ + libpangoft2-1.0-0 \ + libharfbuzz-subset0 \ + libffi-dev \ + libjpeg-dev \ + libopenjp2-7-dev \ + libglib2.0-0 \ + libglib2.0-dev \ + libcairo2 \ + libcairo2-dev \ + libpangocairo-1.0-0 \ + pkg-config \ + python3-dev \ + python3-cffi \ + libgobject-2.0-0 \ + fonts-liberation \ + libgdk-pixbuf2.0-0 \ + shared-mime-info +``` + **2) Build App** Everytime you change frontend code you need to build it before a new deployment, including in the first time: diff --git a/start.sh b/start.sh index 1b64aa18..3a4d60d8 100755 --- a/start.sh +++ b/start.sh @@ -4,31 +4,6 @@ echo "starting script" # Loading env variables export $(grep -v '^#' .env | xargs) - -# Install system dependencies for PDF processing -echo "Installing system dependencies" -sudo apt-get update -sudo apt-get install -y \ - libpango-1.0-0 \ - libharfbuzz0b \ - libpangoft2-1.0-0 \ - libharfbuzz-subset0 \ - libffi-dev \ - libjpeg-dev \ - libopenjp2-7-dev \ - libglib2.0-0 \ - libglib2.0-dev \ - libcairo2 \ - libcairo2-dev \ - libpangocairo-1.0-0 \ - pkg-config \ - python3-dev \ - python3-cffi \ - libgobject-2.0-0 \ - fonts-liberation \ - libgdk-pixbuf2.0-0 \ - shared-mime-info - # echo "" # echo "Loading azd .env file from current environment" # echo "" From 18104aac33cf0e8be0edaf3007222b26f3122ea9 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Wed, 8 Jan 2025 14:27:19 -0500 Subject: [PATCH 282/820] update readme and integrate the pdf converter azure function --- README.md | 37 --- backend/report2email.ipynb | 541 ------------------------------------- backend/rp2email.py | 73 ++--- 3 files changed, 40 insertions(+), 611 deletions(-) delete mode 100644 backend/report2email.ipynb diff --git a/README.md b/README.md index 7ad1fe9b..f6957793 100644 --- a/README.md +++ b/README.md @@ -21,43 +21,6 @@ If you plan to customize the ingestion logic, create a new repo by clicking on t If you created a new repository please update the repository URL before running the git clone command -## Install system dependencies for PDF processing -If you are working on Windows you can skip this step. - -### Mac OS -``` -brew update -brew install weasyprint -``` -In case of errors with the instalation you can run -``` -export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_FALLBACK_LIBRARY_PATH -``` -### Linux (Devian-based) -``` -sudo apt-get update -sudo apt-get install -y \ - libpango-1.0-0 \ - libharfbuzz0b \ - libpangoft2-1.0-0 \ - libharfbuzz-subset0 \ - libffi-dev \ - libjpeg-dev \ - libopenjp2-7-dev \ - libglib2.0-0 \ - libglib2.0-dev \ - libcairo2 \ - libcairo2-dev \ - libpangocairo-1.0-0 \ - pkg-config \ - python3-dev \ - python3-cffi \ - libgobject-2.0-0 \ - fonts-liberation \ - libgdk-pixbuf2.0-0 \ - shared-mime-info -``` - **2) Build App** Everytime you change frontend code you need to build it before a new deployment, including in the first time: diff --git a/backend/report2email.ipynb b/backend/report2email.ipynb deleted file mode 100644 index 856ea39a..00000000 --- a/backend/report2email.ipynb +++ /dev/null @@ -1,541 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 58, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:45:39,312 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 63\n", - "2024-12-31 08:45:39,324 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 64\n", - "2024-12-31 08:45:39,324 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 65\n", - "2024-12-31 08:45:39,324 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 66\n", - "2024-12-31 08:45:39,327 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 70\n", - "2024-12-31 08:45:39,327 - dotenv.main - WARNING - Python-dotenv could not parse statement starting at line 71\n" - ] - } - ], - "source": [ - "import os\n", - "import sys\n", - "import logging\n", - "import markdown\n", - "\n", - "from llm_config import LLMManager # Load pre-configured LLM\n", - "from financial_doc_processor import BlobStorageManager # Load Blob Storage Manager\n", - "\n", - "logger = logging.getLogger(__name__)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:42:30,812 - azure.core.pipeline.policies.http_logging_policy - INFO - Request URL: 'https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Monthly_Economics/December_2024.html'\n", - "Request method: 'GET'\n", - "Request headers:\n", - " 'x-ms-range': 'REDACTED'\n", - " 'x-ms-version': 'REDACTED'\n", - " 'Accept': 'application/xml'\n", - " 'User-Agent': 'azsdk-python-storage-blob/12.19.0 Python/3.11.9 (Windows-10-10.0.26100-SP0)'\n", - " 'x-ms-date': 'REDACTED'\n", - " 'x-ms-client-request-id': '14f36c90-c77d-11ef-904c-10f60a8f4b42'\n", - " 'Authorization': 'REDACTED'\n", - "No body was attached to the request\n", - "2024-12-31 08:42:31,271 - azure.core.pipeline.policies.http_logging_policy - INFO - Response status: 206\n", - "Response headers:\n", - " 'Content-Length': '13678'\n", - " 'Content-Type': 'text/html'\n", - " 'Content-Range': 'REDACTED'\n", - " 'Last-Modified': 'Mon, 30 Dec 2024 20:06:45 GMT'\n", - " 'Accept-Ranges': 'REDACTED'\n", - " 'ETag': '\"0x8DD290D7C488B92\"'\n", - " 'Server': 'Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0'\n", - " 'x-ms-request-id': '0aa6194a-001e-005f-3389-5b24c7000000'\n", - " 'x-ms-client-request-id': '14f36c90-c77d-11ef-904c-10f60a8f4b42'\n", - " 'x-ms-version': 'REDACTED'\n", - " 'x-ms-creation-time': 'REDACTED'\n", - " 'x-ms-blob-content-md5': 'REDACTED'\n", - " 'x-ms-lease-status': 'REDACTED'\n", - " 'x-ms-lease-state': 'REDACTED'\n", - " 'x-ms-blob-type': 'REDACTED'\n", - " 'x-ms-server-encrypted': 'REDACTED'\n", - " 'Access-Control-Expose-Headers': 'REDACTED'\n", - " 'Access-Control-Allow-Origin': 'REDACTED'\n", - " 'Date': 'Tue, 31 Dec 2024 13:42:31 GMT'\n", - "2024-12-31 08:42:31,274 - financial_doc_processor - INFO - Successfully downloaded blob to c:\\SF_Repo\\gpt-rag-frontend\\backend\\blob_downloads\\December_2024.html\n" - ] - } - ], - "source": [ - "\n", - "# initialize the blob manager \n", - "blob_manager = BlobStorageManager()\n", - "\n", - "# downlaod the blob\n", - "# todo: check if the status is success before processing this \n", - "#! this is is retrieved from the previous successful run the process_and_summarize endpoint\n", - "blob_link = 'https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Monthly_Economics/December_2024.html?sv=2022-11-02&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2025-01-06T13:29:33Z&st=2024-09-25T04:29:33Z&spr=https&sig=rmMDVy0aPztEj6A%2FMQHFbioHbLuiL3tn622D993%2Fvow%3D'\n", - "\n", - "file = blob_manager.download_blob_from_a_link(blob_link)\n", - "\n", - "if file:\n", - " # process the file \n", - " pass\n", - "else:\n", - " logger.error(\"Failed to download the file\")\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:42:31,298 - __main__ - INFO - Successfully imported the HTML file\n" - ] - } - ], - "source": [ - "# import the html file \n", - "from pathlib import Path\n", - "\n", - "# Get the file within blob_downloads\n", - "html_file_path = next(Path(os.getcwd()).glob('blob_downloads/*.html'))\n", - "\n", - "# Check if the file exists\n", - "if html_file_path.exists():\n", - " with open(html_file_path, 'r', encoding='utf-8') as file:\n", - " html_content = file.read()\n", - " logger.info(\"Successfully imported the HTML file\")\n", - "else:\n", - " logger.error(\"HTML file not found\")\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:42:36,144 - httpx - INFO - HTTP Request: POST https://oai0-vm2b2htvuuclm.openai.azure.com/openai/deployments/Agent/chat/completions?api-version=2024-05-01-preview \"HTTP/1.1 200 OK\"\n" - ] - } - ], - "source": [ - "# process llm \n", - "llm_manager = LLMManager()\n", - "llm = llm_manager.get_client(client_type='gpt4o', use_langchain=True)\n", - "\n", - "# get prompt \n", - "sys_prompt = llm_manager.get_prompt(prompt_type='email_template')\n", - "\n", - "# add the report content to the prompt \n", - "prompt = sys_prompt.format(report_content=html_content)\n", - "\n", - "#* summarize the report\n", - "\n", - "summary = llm.invoke(prompt)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "**December 2024: Key Economic Trends Shaping the Global Landscape**\n", - "\n", - "This report provides an analysis of the global economic environment in December 2024, highlighting the interplay of growth, inflation, and geopolitical tensions. Here are 3 key points of the report:\n", - "\n", - "1. **Global Economic Stability**: The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.\n", - "\n", - "2. **Regional Economic Dynamics**: North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.\n", - "\n", - "3. **Industry Transformations**: The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.\n", - "\n", - "**Why it matters:** Understanding these economic trends is crucial for businesses and investors to strategically position themselves in a complex global landscape, leveraging opportunities for growth while mitigating the risks associated with geopolitical and market uncertainties." - ], - "text/plain": [ - "" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from IPython.display import Markdown, HTML\n", - "Markdown(summary.content)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "

    December 2024: Key Economic Trends Shaping the Global Landscape

    \n", - "

    This report provides an analysis of the global economic environment in December 2024, highlighting the interplay of growth, inflation, and geopolitical tensions. Here are 3 key points of the report:

    \n", - "
      \n", - "
    1. \n", - "

      Global Economic Stability: The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.

      \n", - "
    2. \n", - "
    3. \n", - "

      Regional Economic Dynamics: North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.

      \n", - "
    4. \n", - "
    5. \n", - "

      Industry Transformations: The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.

      \n", - "
    6. \n", - "
    \n", - "

    Why it matters: Understanding these economic trends is crucial for businesses and investors to strategically position themselves in a complex global landscape, leveraging opportunities for growth while mitigating the risks associated with geopolitical and market uncertainties.

    " - ], - "text/plain": [ - "" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#* now convert this summarization HTML \n", - "\n", - "report_html: str = markdown.markdown(summary.content)\n", - "\n", - "HTML(report_html)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In order to parse these information to email, we need to parse it to proper field. " - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "metadata": {}, - "outputs": [], - "source": [ - "from utils import EmailSchema\n" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:42:39,982 - httpx - INFO - HTTP Request: POST https://oai0-vm2b2htvuuclm.openai.azure.com/openai/deployments/Agent/chat/completions?api-version=2024-05-01-preview \"HTTP/1.1 200 OK\"\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "title='Key Economic Trends Shaping the Global Landscape - December 2024' intro_text='This report provides an analysis of the global economic environment in December 2024, highlighting the interplay of growth, inflation, and geopolitical tensions.' keypoints=[KeyPoint(title='Global Economic Stability', content='The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.'), KeyPoint(title='Regional Economic Dynamics', content='North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.'), KeyPoint(title='Industry Transformations', content='The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.')] why_it_matters='Understanding these economic trends is crucial for businesses and investors to strategically position themselves in a complex global landscape, leveraging opportunities for growth while mitigating the risks associated with geopolitical and market uncertainties.' document_type='MonthlyMacroeconomics'\n" - ] - } - ], - "source": [ - "# Now that we've built the schema, we will parse the report to this schema\n", - "\n", - "llm_report_parser = llm.with_structured_output(EmailSchema)\n", - "\n", - "email_schema = llm_report_parser.invoke(summary.content)\n", - "\n", - "print(email_schema)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'title': 'Global Economic Stability',\n", - " 'content': 'The global GDP is projected to grow steadily at 3.2% in 2024, supported by strategic interest rate cuts that have helped ease inflationary pressures. However, geopolitical tensions, such as trade policy shifts and tariff threats, pose significant risks to this stability.'},\n", - " {'title': 'Regional Economic Dynamics',\n", - " 'content': 'North America shows stable GDP growth with managed inflation, while Europe faces slower growth due to geopolitical challenges. The Asia-Pacific region is advancing technologically but must navigate issues of protectionism, and Emerging Markets are dealing with economic disparities and financial instability.'},\n", - " {'title': 'Industry Transformations',\n", - " 'content': 'The technology sector is witnessing transformative growth driven by AI, necessitating sustainable infrastructure strategies. In energy, there is a shift toward renewable energy certificates, and the healthcare industry is focusing on ambient technology to address workforce challenges.'}]" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# since there are multiple key points, we should convert thme to dict \n", - "\n", - "keypoints_dict = [{'title': point.title, 'content': point.content} for point in email_schema.keypoints]\n", - "\n", - "\n", - "keypoints_dict\n" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "metadata": {}, - "outputs": [], - "source": [ - "# create a function to generate the html template\n", - "def generate_html_template(title, intro_text, key_points, why_it_matters, document_type):\n", - " \"\"\"\n", - " Generate HTML email template with customizable content.\n", - " \n", - " Parameters:\n", - " - title (str): The main title of the report\n", - " - intro_text (str): Introductory text below the title\n", - " - key_points (list): List of dictionaries containing 'title' and 'content' for each point\n", - " - why_it_matters (str): The \"Why it matters\" section content\n", - " \"\"\"\n", - " \n", - " # Generate the key points HTML\n", - " key_points_html = \"\"\n", - " for i, point in enumerate(key_points, 1):\n", - " key_points_html += f\"\"\"\n", - "

    {i}. {point['title']}: {point['content']}

    \"\"\"\n", - "\n", - " template = f\"\"\"\n", - "\n", - "\n", - " \n", - " \n", - " {title}\n", - "\n", - "\n", - "
    \n", - "
    \n", - "
    \n", - " \"Header\n", - "
    \n", - "\n", - "
    \n", - "

    {title}

    \n", - "

    {intro_text}

    \n", - "
    \n", - "\n", - "
    \n", - " {key_points_html}\n", - "

    Why it matters: {why_it_matters}

    \n", - "
    \n", - "\n", - "
    \n", - "

    Got questions?

    Click here

    to chat with our AI financial agent for tailored insights and real-time guidance.

    \n", - "
    \n", - "\n", - "
    \n", - "

    Sales Factory AI
    Unlocking Simplicity, Illuminating Insights

    \n", - "

    Connect with us on: LinkedIn

    \n", - "
    \n", - "
    \n", - "
    \n", - "\n", - "\"\"\"\n", - " \n", - " return template\n" - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [], - "source": [ - "# generate the html \n", - "email_html_content = generate_html_template(title = email_schema.title, \n", - " intro_text = email_schema.intro_text, \n", - " key_points = keypoints_dict, \n", - " why_it_matters = email_schema.why_it_matters, \n", - " document_type = email_schema.document_type)\n", - "\n", - "# we can now save this html to a file \n", - "with open('email_html_content.html', 'w') as file:\n", - " file.write(email_html_content)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "metadata": {}, - "outputs": [], - "source": [ - "# wondering if I should create a different endpoint to send email. \n", - "\n", - "# let's see the needed components \n", - "\n", - "import smtplib \n", - "\n", - "from email.message import EmailMessage\n", - "\n", - "EMAIL_CONFIG = {\n", - " 'smtp_server': 'smtp.gmail.com',\n", - " 'port': 587,\n", - " 'email': 'namtran6701@gmail.com',\n", - " 'password': 'elxr mdyx jzch wrns',\n", - "}\n", - "\n", - "EMAIL_RECIPIENTS = {\n", - " 'test': ['namtran6701@gmail.com', 'victorm@hamalsolutions.com'],\n", - " 'production': ['namtran6701@gmail.com', 'nam.tran@salesfactory.com', 'nam3864779@gmail.com']\n", - "} " - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "metadata": {}, - "outputs": [], - "source": [ - "#! put in utils library \n", - "from typing import Iterable, Optional\n", - "\n", - "def load_html_template(template_path: str) -> str:\n", - " with open(template_path, 'r') as file:\n", - " return file.read()\n", - " \n", - "def send_email(subject: str, \n", - " html_content: str, \n", - " recipients: Iterable[str], \n", - " attachment_path: Optional[str] = None) -> None:\n", - " \n", - " # create the email message\n", - " \n", - " msg = EmailMessage()\n", - "\n", - " msg['Subject'] = subject\n", - " msg['From'] = EMAIL_CONFIG['email']\n", - " msg['Bcc'] = ','.join(recipients)\n", - " msg.add_alternative(html_content, subtype='html')\n", - "\n", - " # Attach file if provided \n", - " if attachment_path:\n", - " with open(attachment_path, 'rb') as file:\n", - " file_data = file.read()\n", - " file_name = attachment_path.split('\\\\')[-1]\n", - " msg.add_attachment(file_data, \n", - " maintype='application', \n", - " subtype='octet-stream', \n", - " filename=file_name)\n", - "\n", - " # send the email \n", - " with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['port']) as server:\n", - " server.starttls()\n", - " server.login(EMAIL_CONFIG['email'], EMAIL_CONFIG['password'])\n", - " server.send_message(msg)\n", - " logger.info(f'Email sent to {recipients}')\n" - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:48:08,829 - __main__ - INFO - Email sent to ['namtran6701@gmail.com', 'victorm@hamalsolutions.com']\n" - ] - } - ], - "source": [ - "send_email(subject='Test Email', \n", - " html_content=email_html_content, \n", - " recipients=EMAIL_RECIPIENTS['test'])\n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-31 08:42:16,181 - __main__ - INFO - Cleaned up the blob downloads directory\n", - "2024-12-31 08:42:16,183 - __main__ - INFO - Cleaned up the email_html_content.html file\n" - ] - } - ], - "source": [ - "# clean up resources \n", - "import shutil \n", - "\n", - "# rempve the blob downloads directory and its contents \n", - "blob_downloads_path = Path(os.getcwd())/'blob_downloads'\n", - "\n", - "# remove the blob downloads directory and its contents \n", - "if blob_downloads_path.exists():\n", - " shutil.rmtree(blob_downloads_path)\n", - " logger.info('Cleaned up the blob downloads directory')\n", - "\n", - "# remove the email_html_content.html file \n", - "email_html_path = Path(os.getcwd())/'email_html_content.html'\n", - "\n", - "if email_html_path.exists():\n", - " email_html_path.unlink()\n", - " logger.info('Cleaned up the email_html_content.html file')\n", - "\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/backend/rp2email.py b/backend/rp2email.py index 6dbf25b8..461e323e 100644 --- a/backend/rp2email.py +++ b/backend/rp2email.py @@ -19,6 +19,7 @@ TEMP_DIR = "blob_downloads" EMAIL_ENDPOINT = '/api/reports/email' PDF_OUTPUT_NAME = "report.pdf" +AZURE_FUNCTION_URL = f"{os.getenv('AZURE_FUNCTION_URL')}{os.getenv('AZURE_FUNCTION_CODE')}" #################################### # Pydantic Models @@ -289,10 +290,45 @@ def _parse_report_to_email_schema(self, summary: str) -> EmailSchema: raise def html_to_pdf(self, html_content: str, output_path: str) -> Path: - """Convert the html content to a pdf file.""" - from weasyprint import HTML - HTML(string=html_content).write_pdf(output_path) - return Path(output_path) + """Convert the html content to a pdf file using the azure function""" + import requests + + # Format the payload exactly as shown in the working test + payload = { + "html": html_content + } + + try: + # Make the request with timeout + logger.info(f"Calling the pdf converter") + response = requests.post( + AZURE_FUNCTION_URL, + json=payload, # Use json parameter to match the raw JSON format + timeout=30 + ) + response.raise_for_status() + + # Create directory if it doesn't exist + output_dir = Path(output_path).parent + output_dir.mkdir(parents=True, exist_ok=True) + + # Save the PDF content to a file + logger.info(f"Saving the PDF to {output_path}") + with open(output_path, 'wb') as f: + f.write(response.content) + logger.info(f"PDF saved successfully") + + return Path(output_path) + + except requests.exceptions.HTTPError as e: + logger.error(f"HTTP error occurred: {e.response.text if e.response else str(e)}") + raise + except requests.exceptions.RequestException as e: + logger.error(f"Request error occurred: {str(e)}") + raise + except IOError as e: + logger.error(f"Error saving PDF file: {str(e)}") + raise def cleanup(self) -> None: """Clean up temporary files. """ @@ -417,32 +453,3 @@ def process_and_send_email(blob_link: str, except Exception as e: logger.exception(f"Error processing and sending email: {str(e)}") return False - - - -# """ - -# Requirements for this endpoint: -# - Blob link from the report -# - List of recipients -# - Optional: attachment path and email subject -# - email_subject: subject of the email - - - -# What does this endpoint do? - -# 1. Download the report from the blob link -> that's why we need the blob link -# 2. Open the report and extract the content from the local path -# 3. Summarize the report -> that's why we need to initialize the llm and the prompt -# 4. parse the summary into the email schema -> use llm to parse -# 5. render the email template -> use the email template manager to render the email template with jinja2 -# 6. pass the formatted email to the email service endpoint -# 7. send the email to the recipients -# --------------------------------------------------- -# What do I need for the email service endpoint? ? -# 1. List of recipients -# 2. Subject of the email -# 3. Link to the blob report content (original). Get the link where it is downloaded -# 4. html content for the email (formatted) -# """ \ No newline at end of file From 4880fc90558463ef40c26746ecb60b37d8ec733b Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Wed, 8 Jan 2025 18:03:38 -0500 Subject: [PATCH 283/820] endpoint to list blobs from a container with optional condition and metadata --- backend/app.py | 68 +++++++++++++++++++ backend/financial_doc_processor.py | 104 +++++++++++++++++++++++++++-- 2 files changed, 167 insertions(+), 5 deletions(-) diff --git a/backend/app.py b/backend/app.py index 0bef882e..e3a3b9cf 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3005,6 +3005,74 @@ def digest_report(): 'message': str(e) }), 500 +@app.route('/api/reports/storage/files', methods=['GET']) +def list_blobs(): + """ + List blobs i nteh container with optional filtering + + Query params: + - prefix(str): filter blobs by prefix + - include_metadata(str): include metadata in results + - max_results(int): maximum number of results to return + - container_name(str): name of the container to list blobs from + + Returns: + JSON response with list of blobs + + Example Payload: + { + "prefix": "Reports/Curation_Reports/Monthly_Economics/", + "include_metadata": "yes", + "max_results": 10, + "container_name": "documents" + } + """ + + try: + # get query params + data = request.get_json() + + container_name = data.get('container_name') + prefix = data.get('prefix') + + include_metadata = data.get('include_metadata', 'no').lower() == 'yes' + + # convert max_results to int + max_results = data.get('max_results', 10) + + if not container_name: + return jsonify({ + "status": "error", + "message": "Blob container name is required" + }), 400 + + blob_storage_manager = BlobStorageManager() + blobs = blob_storage_manager.list_blobs_in_container( + container_name = container_name, + prefix = prefix, + include_metadata = include_metadata, + max_results =max_results + ) + + return jsonify({ + "status": "success", + "data": blobs, + "count": len(blobs) + }), 200 + + except ValueError as e: + return jsonify({ + "status": "error", + "message": str(e) + }), 400 + + except Exception as e: + logger.exception("Unexpected error in list_blobs") + return jsonify({ + "status": "error", + "message": str(e) + }), 500 + if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 3d9130e0..2cb1ddcc 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -8,7 +8,7 @@ from pathlib import Path from collections import defaultdict import markdown2 -from typing import Dict, List +from typing import Dict, List, Any from datetime import datetime, timezone, timedelta import pandas as pd @@ -348,6 +348,10 @@ class BlobConnectionError(BlobStorageError): """Failed to connect to blob storage""" pass +class ContainerNotFoundError(BlobStorageError): + """Container not found in blob storage""" + pass + class BlobAuthenticationError(BlobStorageError): """Authentication failed for blob storage""" pass @@ -700,6 +704,93 @@ def upload_to_blob(self, document_paths: dict = None, metadata: dict = None, fil return upload_results + def list_blobs_in_container( + self, + container_name: str, + prefix: str = None, + include_metadata: str = 'no', + max_results: int = None + ) -> List[Dict[str, Any]]: + """ + List blobs in a container with filtering and metadat + + Args: + container_name(str): Name of the container to list blobs from + prefix(str, optional): Filter results to blob with this prefix + include_metadata(str, optional): Include metadata in results + max_results (int, optional): Maximum number of results to return + + Returns: + List[Dict[str, Any]]: List of blobs information dictionaries containing + - name: Blob name + - size: size in bytes + - created_on: Creation timestamp + - last_modified: Last modified timestamp + - content_type: MIME type of the blob + - metadata: Blob metadata if include_metadata is True + - url: Blob URL + + Raises: + ValueError: If container_name is empty or max_results is invalid + ContainerNotFoundError: if container doesn't exist + BlobAuthenticationError: if authentication fails + """ + if not container_name or not container_name.strip(): + raise ValueError("Container name is required and cannot be empty") + + if max_results is None and max_results <= 0: + raise ValueError("max_results must be greater than 0") + + try: + container_client = self.blob_service_client.get_container_client(container_name) + + # Verify container exists + if not container_client.exists(): + raise ContainerNotFoundError(f"Container not found: {container_name}") + + # build list params + list_params = { + "name_starts_with": prefix if prefix else None, + "results_per_page": max_results + } + + # list blobs with params + blob_list = [] + blobs = container_client.list_blobs(**{k: v for k, v in list_params.items() if v is not None}) + + for blob in blobs: + blob_info = { + "name": blob.name, + "size": blob.size, + "created_on": blob.creation_time.isoformat(), + "last_modified": blob.last_modified.isoformat(), + "content_type": blob.content_settings.content_type, + "url": f"{self.blob_service_client.url}{container_name}/{blob.name}" + } + if include_metadata == 'yes': + try: + blob_client = container_client.get_blob_client(blob.name) + properties = blob_client.get_blob_properties() + blob_info['metadata'] = properties.metadata + except Exception as e: + logger.warning(f"Failed to retrieve metadata for {blob.name}: {str(e)}") + blob_info['metadata'] = None + + blob_list.append(blob_info) + + if max_results and len(blob_list) >= max_results: + break + + return blob_list + + except Exception as e: + if "AuthenticationFailed" in str(e): + raise BlobAuthenticationError(f"Error authenticating with blob storage: {str(e)}") + logger.error(f"Error listing blobs in container: {str(e)}") + raise + + + from sec_edgar_downloader import Downloader from utils import cleanup_resources class FinancialDocumentProcessor: @@ -838,7 +929,10 @@ def process_and_upload(self, equity_id: str, filing_type: str) -> dict: # example usage for get_document_metadata if __name__ == "__main__": doc_processor = BlobStorageManager() - content = doc_processor.get_rpcontent_from_blob_path('/Reports/Curation_Reports/Monthly_Economics/December_2024.html') - print(content) - - + blobs = doc_processor.list_blobs_in_container( + container_name=os.getenv("documentss"), + prefix="Reports/Curation_Reports/Ecommerce", + include_metadata='yes', + max_results=10 + ) + print(blobs) From ef268dd36968b36f5cf8d2576927c9e3eaf74109 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Wed, 8 Jan 2025 18:05:39 -0500 Subject: [PATCH 284/820] change prefix to optional None --- backend/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index e3a3b9cf..5f4c631a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3033,7 +3033,7 @@ def list_blobs(): data = request.get_json() container_name = data.get('container_name') - prefix = data.get('prefix') + prefix = data.get('prefix', None) include_metadata = data.get('include_metadata', 'no').lower() == 'yes' From 1cb516fc694a8b98eefe4843154b0cab1cdc5bb2 Mon Sep 17 00:00:00 2001 From: namtran6701 Date: Wed, 8 Jan 2025 18:21:18 -0500 Subject: [PATCH 285/820] fix metadata --- backend/app.py | 2 +- backend/financial_doc_processor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index 5f4c631a..591a7fd3 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3035,7 +3035,7 @@ def list_blobs(): container_name = data.get('container_name') prefix = data.get('prefix', None) - include_metadata = data.get('include_metadata', 'no').lower() == 'yes' + include_metadata = data.get('include_metadata', 'no').lower() # convert max_results to int max_results = data.get('max_results', 10) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 2cb1ddcc..ea2b1256 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -930,7 +930,7 @@ def process_and_upload(self, equity_id: str, filing_type: str) -> dict: if __name__ == "__main__": doc_processor = BlobStorageManager() blobs = doc_processor.list_blobs_in_container( - container_name=os.getenv("documentss"), + container_name='documents', prefix="Reports/Curation_Reports/Ecommerce", include_metadata='yes', max_results=10 From 6fea175026c065639b174e4d4ee3970a6d3bb614 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Sat, 11 Jan 2025 21:40:49 -0400 Subject: [PATCH 286/820] Security has been added to endpoints and fix bug with date (#246) --- backend/app.py | 33 ++++++++++++++++++++++----------- backend/shared/cosmo_db.py | 8 ++++---- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/backend/app.py b/backend/app.py index 591a7fd3..e63814c6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -651,7 +651,8 @@ def deleteChatConversation(chat_id): #get report by id argument from Container Reports @app.route("/api/reports/", methods=["GET"]) -def getReport(report_id): +@auth.login_required() +def getReport(*, context, report_id): """ Endpoint to get a report by ID. """ @@ -667,7 +668,8 @@ def getReport(report_id): #create Reports curation and companySummarization container Reports @app.route("/api/reports", methods=["POST"]) -def createReport(): +@auth.login_required() +def createReport(*, context): """ Endpoint to create a new report. """ @@ -731,7 +733,8 @@ def createReport(): #update Reports curation and companySummarization container Reports @app.route("/api/reports/", methods=["PUT"]) -def updateReport(report_id): +@auth.login_required() +def updateReport(*, context, report_id): """ Endpoint to update a report by ID. """ @@ -754,7 +757,8 @@ def updateReport(report_id): #delete report from Container Reports @app.route("/api/reports/", methods=["DELETE"]) -def deleteReport(report_id): +@auth.login_required() +def deleteReport(*, context, report_id): """ Endpoint to delete a report by ID. """ @@ -775,7 +779,8 @@ def deleteReport(report_id): #Get User for email receivers @app.route("/api/user/", methods=["GET"]) -def getUserid(user_id): +@auth.login_required() +def getUserid(*, context, user_id): """ Endpoint to get a user by ID. """ @@ -791,7 +796,8 @@ def getUserid(user_id): #Update Users @app.route("/api/user/", methods=["PUT"]) -def updateUser(user_id): +@auth.login_required() +def updateUser(*, context, user_id): """ Endpoint to update a user """ @@ -813,7 +819,8 @@ def updateUser(user_id): return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 @app.route("/api/reports", methods=["GET"]) -def getFilteredType(): +@auth.login_required() +def getFilteredType(*, context): """ Endpoint to obtain reports by type or retrieve all reports if no type is specified. """ @@ -836,7 +843,8 @@ def getFilteredType(): return jsonify({"error": "Internal Server Error"}), 500 @app.route("/api/reports/summarization/templates", methods=["POST"]) -def addSummarizationReport(): +@auth.login_required() +def addSummarizationReport(*, context): """ Endpoint to add a summarization report template. @@ -880,7 +888,8 @@ def addSummarizationReport(): @app.route('/api/reports/summarization/templates/', methods=['DELETE']) -def removeSummarizationReport(template_id): +@auth.login_required() +def removeSummarizationReport(*, context, template_id): """ Endpoint to remove a summarization report template by ID. @@ -909,7 +918,8 @@ def removeSummarizationReport(template_id): return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) @app.route('/api/reports/summarization/templates/', methods=['GET']) -def getSummarizationReports(): +@auth.login_required() +def getSummarizationReports(*, context): try: result = get_templates() return create_success_response(result) @@ -917,7 +927,8 @@ def getSummarizationReports(): return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) @app.route('/api/reports/summarization/templates/', methods=['GET']) -def getSummarizationReport(template_id): +@auth.login_required() +def getSummarizationReport(*, context, template_id): try: result = get_template_by_ID(template_id) return create_success_response(result) diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index 5eaebca8..b5da21d2 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -39,8 +39,8 @@ def create_report(data): try: container = get_cosmos_container("reports") data["id"] = str(uuid.uuid4()) - data["createAt"] = datetime.now(timezone.utc).isoformat() + "Z" - data["updatedAt"] = datetime.now(timezone.utc).isoformat() + "Z" + data["createAt"] = datetime.now(timezone.utc).isoformat() + data["updatedAt"] = datetime.now(timezone.utc).isoformat() container.upsert_item(data) logging.info(f"Document created: {data}") return data @@ -191,8 +191,8 @@ def create_template(data): try: container = get_cosmos_container("templates") data["id"] = str(uuid.uuid4()) - data["createAt"] = datetime.now(timezone.utc).isoformat() + "Z" - data["updatedAt"] = datetime.now(timezone.utc).isoformat() + "Z" + data["createAt"] = datetime.now(timezone.utc).isoformat() + data["updatedAt"] = datetime.now(timezone.utc).isoformat() container.upsert_item(data) logging.info(f"Document created: {data}") return data From 0a63d59ccd3ab2f4346c16fa5d74c39cf4d0a129 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Sat, 11 Jan 2025 22:55:14 -0400 Subject: [PATCH 287/820] HOTFIX get blob storage conexion strin g from a key vault secret (#257) --- backend/financial_doc_processor.py | 686 +++++++++++++++++------------ 1 file changed, 393 insertions(+), 293 deletions(-) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index ea2b1256..cfd67071 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -14,6 +14,8 @@ import pandas as pd import fitz from dotenv import load_dotenv +from azure.keyvault.secrets import SecretClient +from azure.identity import DefaultAzureCredential from azure.storage.blob import BlobServiceClient, ContentSettings from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter @@ -29,177 +31,195 @@ load_dotenv() -BLOB_CONNECTION_STRING = os.getenv('BLOB_CONNECTION_STRING') -BLOB_CONTAINER_NAME = os.getenv('BLOB_CONTAINER_NAME') +def get_secret(secretName): + keyVaultName = os.environ["AZURE_KEY_VAULT_NAME"] + KVUri = f"https://{keyVaultName}.vault.azure.net" + credential = DefaultAzureCredential() + client = SecretClient(vault_url=KVUri, credential=credential) + logging.info(f"[webbackend] retrieving {secretName} secret from {keyVaultName}.") + retrieved_secret = client.get_secret(secretName) + return retrieved_secret.value + + +BLOB_CONNECTION_STRING = get_secret("storageConnectionString") +BLOB_CONTAINER_NAME = os.getenv("BLOB_CONTAINER_NAME") # configure logging logging.basicConfig( - level = logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger(__name__) + def get_downloaded_files(equity_id: str, filing_type: str): filings_dir = os.path.join(os.getcwd(), "sec-edgar-filings", equity_id, filing_type) # reformat the directory path to be a valid path if not os.path.exists(filings_dir): logger.warning(f"The directory {filings_dir} does not exist") - return None + return None # Walk through all subdirectories for root, dirs, files in os.walk(filings_dir): - # Find the primary-document.html file - for file in files: + # Find the primary-document.html file + for file in files: # Look specifically for primary-document.html - if file == "primary-document.html": - return os.path.join(root, file) - logger.warning(f"No primary-document.html file found for {equity_id} {filing_type}") + if file == "primary-document.html": + return os.path.join(root, file) + logger.warning(f"No primary-document.html file found for {equity_id} {filing_type}") return None + def collect_filing_documents( - EQUITY_IDS: List[str], - FILING_TYPES: List[str], - get_downloaded_files: callable + EQUITY_IDS: List[str], FILING_TYPES: List[str], get_downloaded_files: callable ) -> Dict[str, Dict[str, str]]: """ Collect filing documents for multiple equities and filing types and convert to PDF. - """ + """ if not EQUITY_IDS: raise ValueError("EQUITY_IDS list cannot be empty") if not FILING_TYPES: raise ValueError("FILING_TYPES list cannot be empty") - + document_paths: Dict[str, Dict[str, str]] = defaultdict(dict) - + try: for equity in EQUITY_IDS: logger.info(f"Processing equity: {equity}") - + for filing_type in FILING_TYPES: try: logger.debug(f"Fetching {filing_type} for {equity}") html_path = get_downloaded_files(equity, filing_type) - + if html_path: # Convert HTML path to PDF path - pdf_path = Path(html_path).with_suffix('.pdf') - + pdf_path = Path(html_path).with_suffix(".pdf") + # Convert HTML to PDF success = convert_html_to_pdf( - input_path=html_path, - output_path=pdf_path + input_path=html_path, output_path=pdf_path ) - + if success: document_paths[equity][filing_type] = str(pdf_path) - logger.debug(f"Converted and stored PDF for {equity} {filing_type}: {pdf_path}") + logger.debug( + f"Converted and stored PDF for {equity} {filing_type}: {pdf_path}" + ) else: - logger.warning(f"Failed to convert {filing_type} for {equity}") + logger.warning( + f"Failed to convert {filing_type} for {equity}" + ) else: logger.warning(f"No {filing_type} document found for {equity}") - + except Exception as e: - logger.error(f"Error processing {filing_type} for {equity}: {str(e)}") + logger.error( + f"Error processing {filing_type} for {equity}: {str(e)}" + ) continue - + if not document_paths[equity]: logger.warning(f"No documents found for equity: {equity}") - + except Exception as e: logger.error(f"Unexpected error during document collection: {str(e)}") raise - + return dict(document_paths) -blob_connection_string = os.getenv("BLOB_CONNECTION_STRING") -if not blob_connection_string: +BLOB_CONNECTION_STRING = os.getenv("BLOB_CONNECTION_STRING") +if not BLOB_CONNECTION_STRING: raise ValueError("BLOB_CONNECTION_STRING environment variable is not set") blob_container_name = BLOB_CONTAINER_NAME -blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string) +blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONNECTION_STRING) container_client = blob_service_client.get_container_client(blob_container_name) def validate_document_paths(document_paths: Dict[str, Dict[str, str]]) -> bool: """ Validate the collected document paths. - + Args: document_paths (Dict[str, Dict[str, str]]): Collected document paths - + Returns: bool: True if validation passes, False otherwise """ - + try: # Check if any documents were collected if not document_paths: logger.error("No documents were collected") return False - + # Validate each path exists OR path does not end with .pdf for equity, filings in document_paths.items(): if not filings: logger.warning(f"No filings found for equity {equity}") continue - + for filing_type, path in filings.items(): logger.info(f"Checking PDF requirements for {equity} {filing_type} ") - if not str(path).lower().endswith('.pdf'): - logger.error(f"file for {equity} {filing_type} is not a PDF: {path}") + if not str(path).lower().endswith(".pdf"): + logger.error( + f"file for {equity} {filing_type} is not a PDF: {path}" + ) logger.info(f"PDF found for {equity}") if not Path(path).exists(): logger.error(f"File not found for {equity} {filing_type}: {path}") return False return True - + except Exception as e: logger.error(f"Error during validation: {str(e)}") return False - # Create directory if it does not exist -def ensure_directory_exists(directory_path): - path = Path(directory_path) - if not path.exists(): - path.mkdir(parents=True, exist_ok=True) - print(f"Directory created: {directory_path}") - else: - print(f"Directory already exists: {directory_path}") - +def ensure_directory_exists(directory_path): + path = Path(directory_path) + if not path.exists(): + path.mkdir(parents=True, exist_ok=True) + print(f"Directory created: {directory_path}") + else: + print(f"Directory already exists: {directory_path}") + # Convert pages from PDF to images def extract_pdf_pages_to_images(pdf_path, image_dir): # Validate image_out directory exists doc_id = str(uuid.uuid4()) image_out_dir = os.path.join(image_dir, doc_id) - ensure_directory_exists(image_out_dir) + ensure_directory_exists(image_out_dir) # Open the PDF file and iterate pages - print ('Extracting images from PDF...') + print("Extracting images from PDF...") try: pdf_document = fitz.open(pdf_path) except Exception as e: logger.error(f"Error opening PDF: {str(e)}") return None - # get the file name without extension + # get the file name without extension file_name = os.path.splitext(os.path.basename(pdf_path))[0] - for page_number in range(len(pdf_document)): - page = pdf_document.load_page(page_number) - image = page.get_pixmap() - image_out_file = os.path.join(image_out_dir, f'{file_name}_{page_number + 1}.png') - image.save(image_out_file) + for page_number in range(len(pdf_document)): + page = pdf_document.load_page(page_number) + image = page.get_pixmap() + image_out_file = os.path.join( + image_out_dir, f"{file_name}_{page_number + 1}.png" + ) + image.save(image_out_file) pdf_document.close() return doc_id -# save the summary to pdf to upload to blob later + +# save the summary to pdf to upload to blob later def save_str_to_pdf(text: str, output_path: str) -> None: """ Save a given text string to a PDF file with full Unicode support using ReportLab. @@ -219,57 +239,61 @@ def save_str_to_pdf(text: str, output_path: str) -> None: rightMargin=72, leftMargin=72, topMargin=72, - bottomMargin=72 + bottomMargin=72, ) - + # Create the story (content) styles = getSampleStyleSheet() story = [] - + # Add the text as a paragraph - para = Paragraph(text, styles['Normal']) + para = Paragraph(text, styles["Normal"]) story.append(para) - + # Build the PDF doc.build(story) - + logger.info(f"PDF saved successfully to {output_path}") - + except Exception as e: logger.error(f"Error saving PDF: {str(e)}") raise -def remove_directory(directory_path): - try: - if os.path.exists(directory_path): - shutil.rmtree(directory_path) - print(f"Directory '{directory_path}' has been removed successfully.") - else: - print(f"Directory '{directory_path}' does not exist.") - except Exception as e: - print(f"An error occurred while removing the directory: {e}") +def remove_directory(directory_path): + try: + if os.path.exists(directory_path): + shutil.rmtree(directory_path) + print(f"Directory '{directory_path}' has been removed successfully.") + else: + print(f"Directory '{directory_path}' does not exist.") + except Exception as e: + print(f"An error occurred while removing the directory: {e}") + def reset_local_dirs(): - if os.path.exists('json'): - remove_directory('json') - if os.path.exists('images'): - remove_directory('images') - if os.path.exists('pdf'): - remove_directory('pdf') - -def create_document_paths(output_path: str, equity_name: str, financial_type: str) -> dict: + if os.path.exists("json"): + remove_directory("json") + if os.path.exists("images"): + remove_directory("images") + if os.path.exists("pdf"): + remove_directory("pdf") + + +def create_document_paths( + output_path: str, equity_name: str, financial_type: str +) -> dict: """ Create a document paths dictionary structure compatible with upload_to_blob function. - + Args: output_path (str): Path to the document (e.g., 'pdf/10-K_AAPL_summary.pdf') equity_name (str): Name of the equity (e.g., 'AAPL') financial_type (str): Type of financial document (e.g., '10-K') - + Returns: dict: Nested dictionary structure for upload_to_blob function - + Example: >>> path = 'pdf/10-K_AAPL_summary.pdf' >>> create_document_paths(path, 'AAPL', '10-K') @@ -279,16 +303,13 @@ def create_document_paths(output_path: str, equity_name: str, financial_type: st } } """ - return { - equity_name: { - financial_type: output_path - } - } + return {equity_name: {financial_type: output_path}} + def markdown_to_html(markdown_text: str, output_file: str): - """Convert markdown to HTML using markdown2""" - # Define CSS styles - css_styles = """ + """Convert markdown to HTML using markdown2""" + # Define CSS styles + css_styles = """ """ - - html_content = markdown2.markdown(markdown_text, extras=["tables"]) - - # Combine CSS with HTML content - final_html = f""" + + html_content = markdown2.markdown(markdown_text, extras=["tables"]) + + # Combine CSS with HTML content + final_html = f""" @@ -334,95 +355,124 @@ def markdown_to_html(markdown_text: str, output_file: str): """ - - # Create output directory if it doesn't exist - Path(output_file).parent.mkdir(parents=True, exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - f.write(final_html) + + # Create output directory if it doesn't exist + Path(output_file).parent.mkdir(parents=True, exist_ok=True) + with open(output_file, "w", encoding="utf-8") as f: + f.write(final_html) + class BlobStorageError(Exception): """Base exception for blob storage operations""" + pass + class BlobConnectionError(BlobStorageError): """Failed to connect to blob storage""" + pass + class ContainerNotFoundError(BlobStorageError): """Container not found in blob storage""" + pass + class BlobAuthenticationError(BlobStorageError): """Authentication failed for blob storage""" + pass + class BlobNotFoundError(BlobStorageError): """Blob not found in storage""" + pass + class BlobUploadError(BlobStorageError): """Failed to upload blob""" + pass + class BlobDownloadError(BlobStorageError): """Failed to download blob""" + pass -# class to catch metadata error + + +# class to catch metadata error class BlobMetadataError(BlobStorageError): """Failed to retrieve metadata""" + pass + class ReportGenerationError(Exception): """Base exception for report generation errors""" + pass + class InvalidReportTypeError(ReportGenerationError): """Raised when report type is invalid""" + pass + class StorageError(ReportGenerationError): """Raised when storage operations fail""" + pass + + class BlobStorageManager: def __init__(self): try: - self.blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONNECTION_STRING) - self.container_client = self.blob_service_client.get_container_client(BLOB_CONTAINER_NAME) - self.blob_base_folder = 'financial' + self.blob_service_client = BlobServiceClient.from_connection_string( + BLOB_CONNECTION_STRING + ) + self.container_client = self.blob_service_client.get_container_client( + BLOB_CONTAINER_NAME + ) + self.blob_base_folder = "financial" except ValueError as e: raise BlobConnectionError(f"Invalid connection string: {str(e)}") except Exception as e: raise BlobConnectionError(f"Failed to initialize blob storage: {str(e)}") - - #todo: add report blob path to the click here link in the html template like this: https://webgpt0-vm2b2htvuuclm.azurewebsites.net/?agent=financial&document=Ecommerce&blobpath=%22https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Ecommerce/De[%E2%80%A6]KdAaWXqNHsjhB5c3cWaAT3rWymLUB3YuZQdOc%2F6FYG8%3D%22 - #todo: then retrieve the document in init and load the report content to the llm - + + # todo: add report blob path to the click here link in the html template like this: https://webgpt0-vm2b2htvuuclm.azurewebsites.net/?agent=financial&document=Ecommerce&blobpath=%22https://strag0vm2b2htvuuclm.blob.core.windows.net/documents/Reports/Curation_Reports/Ecommerce/De[%E2%80%A6]KdAaWXqNHsjhB5c3cWaAT3rWymLUB3YuZQdOc%2F6FYG8%3D%22 + # todo: then retrieve the document in init and load the report content to the llm + def get_rpcontent_from_blob_path(self, blob_path: str) -> str: """ Get report content from blob path. - + Args: blob_path (str): Path to the blob, e.g. 'Reports/Curation_Reports/Ecommerce/December_2024.html' """ try: # Remove any leading/trailing slashes - clean_path = blob_path.strip('/') - + clean_path = blob_path.strip("/") + logger.info(f"Attempting to access blob at path: {clean_path}") - + blob_client = self.container_client.get_blob_client(clean_path) - + if not blob_client.exists(): logger.error(f"Blob not found: {clean_path}") raise BlobDownloadError(f"Blob not found at path: {clean_path}") - + downloaded_blob = blob_client.download_blob() return downloaded_blob.content_as_text() - + except Exception as e: logger.exception(f"Error accessing blob at {blob_path}") raise BlobDownloadError(f"Failed to download blob: {str(e)}") - + # todo: double check this function def _get_blob_path_parts_from_url(self, url: str) -> List[str]: """ @@ -431,62 +481,65 @@ def _get_blob_path_parts_from_url(self, url: str) -> List[str]: from urllib.parse import urlparse parsed_url = urlparse(url) - return parsed_url.path.lstrip('/').split('/') - + return parsed_url.path.lstrip("/").split("/") + def download_blob_from_a_link(self, url: str, filename: str = None) -> bool: """ Download a document from a given blob URL and save it to the downloads directo ry. - + Args: url (str): The full Azure blob storage URL - filename (str, optional): Name for the downloaded file. If not provided, + filename (str, optional): Name for the downloaded file. If not provided, will be extracted from the URL - + Returns: None """ try: # Parse the URL to get the container and blob path from urllib.parse import urlparse + parsed_url = urlparse(url) - + # Split the path into parts - path_parts = parsed_url.path.lstrip('/').split('/') - + path_parts = parsed_url.path.lstrip("/").split("/") + # Get blob path - blob_path = '/'.join(path_parts[1:]) - + blob_path = "/".join(path_parts[1:]) + # If filename not provided, use the last part of the blob path if not filename: filename = os.path.basename(blob_path) - + # Create downloads directory in project root - downloads_dir = os.path.join(os.getcwd(), 'blob_downloads') + downloads_dir = os.path.join(os.getcwd(), "blob_downloads") os.makedirs(downloads_dir, exist_ok=True) - + # Construct the full local path local_data_path = os.path.join(downloads_dir, filename) - + # Get the blob client blob_client = self.container_client.get_blob_client(blob_path) - + # Download the blob - with open(local_data_path, 'wb') as file: + with open(local_data_path, "wb") as file: download_stream = blob_client.download_blob() file.write(download_stream.readall()) - + logger.info(f"Successfully downloaded blob to {local_data_path}") return True - + except Exception as e: logger.error(f"Failed to download blob: {str(e)}") return False - - def download_documents(self, equity_name: str, - financial_type: str, - exclude_summary: bool = True, - local_data_path: str = PDF_PATH) -> List[str]: + def download_documents( + self, + equity_name: str, + financial_type: str, + exclude_summary: bool = True, + local_data_path: str = PDF_PATH, + ) -> List[str]: """ Download documents from blob storage. @@ -514,41 +567,49 @@ def download_documents(self, equity_name: str, raise OSError(f"Failed to create local directory: {str(e)}") base_path = f"{self.blob_base_folder}/{financial_type}" - + try: # List all blobs - all_blobs = list(self.container_client.list_blobs(name_starts_with=base_path)) + all_blobs = list( + self.container_client.list_blobs(name_starts_with=base_path) + ) except Exception as e: raise BlobNotFoundError(f"Failed to list blobs: {str(e)}") # Filter for exact equity name matches import re + equity_pattern = re.compile( f"{re.escape(base_path)}/{re.escape(equity_name)}(_summary)?\.pdf$" ) - + filtered_blobs = [ - blob for blob in all_blobs - if equity_pattern.match(blob.name) and - (not exclude_summary or "_summary" not in blob.name) + blob + for blob in all_blobs + if equity_pattern.match(blob.name) + and (not exclude_summary or "_summary" not in blob.name) ] if not filtered_blobs: - raise BlobNotFoundError(f"No matching documents found for {equity_name}") + raise BlobNotFoundError( + f"No matching documents found for {equity_name}" + ) + + logger.info( + f"Found {len(filtered_blobs)} matching documents for {equity_name}" + ) - logger.info(f"Found {len(filtered_blobs)} matching documents for {equity_name}") - for blob in filtered_blobs: try: - logger.info(f'Downloading {blob.name}') + logger.info(f"Downloading {blob.name}") blob_client = self.container_client.get_blob_client(blob.name) - file_name = f'{financial_type}_{os.path.basename(blob.name)}' + file_name = f"{financial_type}_{os.path.basename(blob.name)}" local_file_path = os.path.join(local_data_path, file_name) - + with open(local_file_path, "wb") as file: data = blob_client.download_blob() file.write(data.readall()) - + downloaded_files.append(local_file_path) logger.info(f"Successfully downloaded: {file_name}") except OSError as e: @@ -561,49 +622,57 @@ def download_documents(self, equity_name: str, except Exception as e: logger.error(f"Error in blob storage operations: {str(e)}") raise - + return downloaded_files + def get_document_metadata(self, remote_file_path: str) -> dict: """Retrieve metadata for a specific blob from defined container in env - + Args: remote_file_path (str): Path to the blob in blob storage - + Returns: dict: Metadata of the blob - + Raises: BlobMetadataError: If there is an error retrieving metadata Example: metadata = doc_processor.get_document_metadata('financial/10-K/AAPL.pdf') print(metadata) """ - - try: + + try: blob_client = self.container_client.get_blob_client(remote_file_path) blob_properties = blob_client.get_blob_properties() return blob_properties.metadata except Exception as e: - raise BlobMetadataError(f"Error retrieving metadata for {remote_file_path}: {str(e)}") - + raise BlobMetadataError( + f"Error retrieving metadata for {remote_file_path}: {str(e)}" + ) # make sure the document_paths is a dict with the structure of create_document_paths - def upload_to_blob(self, document_paths: dict = None, metadata: dict = None, file_path: str = None, blob_folder: str = None) -> Dict: + def upload_to_blob( + self, + document_paths: dict = None, + metadata: dict = None, + file_path: str = None, + blob_folder: str = None, + ) -> Dict: """ - Upload files to Azure Blob Storage. Can handle either a document_paths dictionary + Upload files to Azure Blob Storage. Can handle either a document_paths dictionary or a single file path. - + Args: document_paths (dict, optional): Nested dictionary with equity IDs and their filing types file_path (str, optional): Direct path to a file to upload blob_folder (str, optional): Custom folder path in blob storage (defaults to self.blob_base_folder) - + Returns: dict: Dictionary of upload results """ if not document_paths and not file_path: raise ValueError("Either document_paths or file_path must be provided") - + if document_paths and file_path: raise ValueError("Cannot provide both document_paths and file_path") @@ -611,52 +680,52 @@ def upload_to_blob(self, document_paths: dict = None, metadata: dict = None, fil if file_path: if not os.path.exists(file_path): raise FileNotFoundError(f"File not found: {file_path}") - + try: # Use provided blob folder or default to base folder base_folder = blob_folder if blob_folder else self.blob_base_folder blob_path = f"{base_folder}/{os.path.basename(file_path)}" # set the content type based on the file extension - if blob_path.endswith('.pdf'): - content_type = 'application/pdf' - elif blob_path.endswith('.html'): - content_type = 'text/html' - elif blob_path.endswith('.txt'): - content_type = 'text/plain' + if blob_path.endswith(".pdf"): + content_type = "application/pdf" + elif blob_path.endswith(".html"): + content_type = "text/html" + elif blob_path.endswith(".txt"): + content_type = "text/plain" else: - content_type = 'application/octet-stream' + content_type = "application/octet-stream" with open(file_path, "rb") as data: try: self.container_client.upload_blob( - name=blob_path, - data=data, - overwrite=True, + name=blob_path, + data=data, + overwrite=True, content_settings=ContentSettings(content_type=content_type), - metadata=metadata + metadata=metadata, ) except Exception as e: raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") - + # get the blob url for the uploaded file blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" - + result = { "status": "success", "blob_path": blob_path, - "blob_url": blob_url + "blob_url": blob_url, } - logger.info(f'Document has been uploaded to {blob_path}') + logger.info(f"Document has been uploaded to {blob_path}") return result - + except Exception as e: result = {"status": "failed", "error": str(e)} logger.error(f"Failed to upload file {file_path}: {str(e)}") return result - + # Handle document_paths dictionary upload (original functionality) if not isinstance(document_paths, dict): raise ValueError("document_paths must be a dictionary") - + upload_results = {} for equity, filings in document_paths.items(): upload_results[equity] = {} @@ -664,152 +733,173 @@ def upload_to_blob(self, document_paths: dict = None, metadata: dict = None, fil try: if not os.path.exists(document_path): raise FileNotFoundError(f"File not found: {document_path}") - - blob_path = f"{self.blob_base_folder}/{filing_type}/{equity}_summary.pdf" if "summary" in document_path else f"{self.blob_base_folder}/{filing_type}/{equity}.pdf" - + + blob_path = ( + f"{self.blob_base_folder}/{filing_type}/{equity}_summary.pdf" + if "summary" in document_path + else f"{self.blob_base_folder}/{filing_type}/{equity}.pdf" + ) + # set the content type based on the file extension - if blob_path.endswith('.pdf'): - content_type = 'application/pdf' - elif blob_path.endswith('.html'): - content_type = 'text/html' - elif blob_path.endswith('.txt'): - content_type = 'text/plain' + if blob_path.endswith(".pdf"): + content_type = "application/pdf" + elif blob_path.endswith(".html"): + content_type = "text/html" + elif blob_path.endswith(".txt"): + content_type = "text/plain" else: - content_type = 'application/octet-stream' - + content_type = "application/octet-stream" + with open(document_path, "rb") as data: try: self.container_client.upload_blob( - name=blob_path, - data=data, - overwrite=True, - content_settings=ContentSettings(content_type=content_type), - metadata=metadata + name=blob_path, + data=data, + overwrite=True, + content_settings=ContentSettings( + content_type=content_type + ), + metadata=metadata, ) except Exception as e: - raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") - + raise BlobUploadError( + f"Failed to upload {blob_path}: {str(e)}" + ) + # get the blob url for the uploaded file blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" upload_results[equity][filing_type] = { "status": "success", "blob_path": blob_path, "blob_url": blob_url, - "metadata": metadata + "metadata": metadata, } - logger.info(f'Document has been uploaded to {blob_path}') + logger.info(f"Document has been uploaded to {blob_path}") except Exception as e: - upload_results[equity][filing_type] = {"status": "failed", "error": str(e)} + upload_results[equity][filing_type] = { + "status": "failed", + "error": str(e), + } logger.error(f"Failed to upload {equity} {filing_type}: {str(e)}") return upload_results - def list_blobs_in_container( - self, - container_name: str, - prefix: str = None, - include_metadata: str = 'no', - max_results: int = None + self, + container_name: str, + prefix: str = None, + include_metadata: str = "no", + max_results: int = None, ) -> List[Dict[str, Any]]: - """ - List blobs in a container with filtering and metadat + """ + List blobs in a container with filtering and metadat - Args: - container_name(str): Name of the container to list blobs from + Args: + container_name(str): Name of the container to list blobs from prefix(str, optional): Filter results to blob with this prefix - include_metadata(str, optional): Include metadata in results - max_results (int, optional): Maximum number of results to return + include_metadata(str, optional): Include metadata in results + max_results (int, optional): Maximum number of results to return Returns: - List[Dict[str, Any]]: List of blobs information dictionaries containing - - name: Blob name + List[Dict[str, Any]]: List of blobs information dictionaries containing + - name: Blob name - size: size in bytes - - created_on: Creation timestamp - - last_modified: Last modified timestamp - - content_type: MIME type of the blob - - metadata: Blob metadata if include_metadata is True - - url: Blob URL - - Raises: + - created_on: Creation timestamp + - last_modified: Last modified timestamp + - content_type: MIME type of the blob + - metadata: Blob metadata if include_metadata is True + - url: Blob URL + + Raises: ValueError: If container_name is empty or max_results is invalid ContainerNotFoundError: if container doesn't exist BlobAuthenticationError: if authentication fails """ if not container_name or not container_name.strip(): raise ValueError("Container name is required and cannot be empty") - + if max_results is None and max_results <= 0: raise ValueError("max_results must be greater than 0") - - try: - container_client = self.blob_service_client.get_container_client(container_name) + + try: + container_client = self.blob_service_client.get_container_client( + container_name + ) # Verify container exists if not container_client.exists(): raise ContainerNotFoundError(f"Container not found: {container_name}") - - # build list params + + # build list params list_params = { - "name_starts_with": prefix if prefix else None, - "results_per_page": max_results + "name_starts_with": prefix if prefix else None, + "results_per_page": max_results, } - # list blobs with params + # list blobs with params blob_list = [] - blobs = container_client.list_blobs(**{k: v for k, v in list_params.items() if v is not None}) - + blobs = container_client.list_blobs( + **{k: v for k, v in list_params.items() if v is not None} + ) + for blob in blobs: blob_info = { "name": blob.name, - "size": blob.size, + "size": blob.size, "created_on": blob.creation_time.isoformat(), "last_modified": blob.last_modified.isoformat(), "content_type": blob.content_settings.content_type, - "url": f"{self.blob_service_client.url}{container_name}/{blob.name}" + "url": f"{self.blob_service_client.url}{container_name}/{blob.name}", } - if include_metadata == 'yes': - try: + if include_metadata == "yes": + try: blob_client = container_client.get_blob_client(blob.name) properties = blob_client.get_blob_properties() - blob_info['metadata'] = properties.metadata + blob_info["metadata"] = properties.metadata except Exception as e: - logger.warning(f"Failed to retrieve metadata for {blob.name}: {str(e)}") - blob_info['metadata'] = None + logger.warning( + f"Failed to retrieve metadata for {blob.name}: {str(e)}" + ) + blob_info["metadata"] = None blob_list.append(blob_info) - if max_results and len(blob_list) >= max_results: - break + if max_results and len(blob_list) >= max_results: + break return blob_list - except Exception as e: + except Exception as e: if "AuthenticationFailed" in str(e): - raise BlobAuthenticationError(f"Error authenticating with blob storage: {str(e)}") + raise BlobAuthenticationError( + f"Error authenticating with blob storage: {str(e)}" + ) logger.error(f"Error listing blobs in container: {str(e)}") - raise - + raise from sec_edgar_downloader import Downloader from utils import cleanup_resources + + class FinancialDocumentProcessor: def __init__(self): self.dl = Downloader( os.getenv("USER_AGENT_NAME", "SalesFactory"), - os.getenv("USER_AGENT_EMAIL", "nam.tran@salesfactory.com") + os.getenv("USER_AGENT_EMAIL", "nam.tran@salesfactory.com"), ) self.blob_manager = BlobStorageManager() - def download_filing(self, equity_id: str, filing_type: str, after_date: str = None) -> dict: + def download_filing( + self, equity_id: str, filing_type: str, after_date: str = None + ) -> dict: """ Download a single SEC filing. - + Args: equity_id (str): The equity identifier (e.g., 'AAPL') filing_type (str): The type of filing (e.g., '10-K') after_date (str): Date string in 'YYYY-MM-DD' format - + Returns: dict: Status of the download operation """ @@ -818,43 +908,45 @@ def download_filing(self, equity_id: str, filing_type: str, after_date: str = No # Validate date format try: # Parse the input date - parsed_date = datetime.strptime(after_date, '%Y-%m-%d') - + parsed_date = datetime.strptime(after_date, "%Y-%m-%d") + # Ensure date is in UTC timezone utc_date = parsed_date.replace(tzinfo=timezone.utc) - + # Convert to string format expected by SEC EDGAR - formatted_date = utc_date.strftime('%Y-%m-%d') - + formatted_date = utc_date.strftime("%Y-%m-%d") + # today today = datetime.now(timezone.utc) - + # Add one day tomorrow = today + timedelta(days=1) - - tomorrow_str = tomorrow.strftime('%Y-%m-%d') - - logger.info(f"Downloading {filing_type} for {equity_id} after {formatted_date}") + + tomorrow_str = tomorrow.strftime("%Y-%m-%d") + + logger.info( + f"Downloading {filing_type} for {equity_id} after {formatted_date}" + ) num_downloaded_file = self.dl.get( - filing_type, - equity_id, - limit=1, - download_details=True, + filing_type, + equity_id, + limit=1, + download_details=True, after=formatted_date, - before = tomorrow_str # avoid afterdate is greater than before date error + before=tomorrow_str, # avoid afterdate is greater than before date error ) - + if num_downloaded_file == 0: return { "status": "not_found", "message": f"No {filing_type} found after {formatted_date} for {equity_id}", - "code": 404 + "code": 404, } except ValueError as e: return { "status": "error", "message": f"Error: {str(e)}", - "code": 400 + "code": 400, } else: logger.info(f"Downloading most recent {filing_type} for {equity_id}") @@ -863,14 +955,14 @@ def download_filing(self, equity_id: str, filing_type: str, after_date: str = No return { "status": "success", "message": f"Successfully downloaded {filing_type} for {equity_id}", - "code": 200 + "code": 200, } except Exception as e: logger.error(f"Download failed: {str(e)}") return { "status": "error", "message": f"Failed to download {filing_type} for {equity_id}: {str(e)}", - "code": 500 + "code": 500, } def process_and_upload(self, equity_id: str, filing_type: str) -> dict: @@ -879,26 +971,29 @@ def process_and_upload(self, equity_id: str, filing_type: str) -> dict: document_paths = collect_filing_documents( EQUITY_IDS=[equity_id], FILING_TYPES=[filing_type], - get_downloaded_files=get_downloaded_files + get_downloaded_files=get_downloaded_files, ) if not validate_document_paths(document_paths): return { "status": "error", "message": "Document collection validation failed", - "code": 400 + "code": 400, } - - # add metadata to uploaded document + + # add metadata to uploaded document from datetime import datetime + metadata = { - 'equity_id': equity_id, - 'filing_type': filing_type, - 'uploaded_date': datetime.now().strftime('%Y-%m-%d'), - 'source': 'SEC EDGAR', + "equity_id": equity_id, + "filing_type": filing_type, + "uploaded_date": datetime.now().strftime("%Y-%m-%d"), + "source": "SEC EDGAR", } - results = self.blob_manager.upload_to_blob(document_paths, metadata=metadata) + results = self.blob_manager.upload_to_blob( + document_paths, metadata=metadata + ) equity_result = results.get(equity_id, {}) filing_result = equity_result.get(filing_type, {}) @@ -914,25 +1009,30 @@ def process_and_upload(self, equity_id: str, filing_type: str) -> dict: return { "status": "success" if upload_successful else "error", - "message": "Document processed successfully" if upload_successful else "Upload failed", + "message": ( + "Document processed successfully" + if upload_successful + else "Upload failed" + ), "results": results, - "code": 200 if upload_successful else 500 + "code": 200 if upload_successful else 500, } except Exception as e: logger.error(f"Processing failed: {str(e)}") return { "status": "error", "message": f"Processing failed: {str(e)}", - "code": 500 + "code": 500, } + # example usage for get_document_metadata if __name__ == "__main__": doc_processor = BlobStorageManager() blobs = doc_processor.list_blobs_in_container( - container_name='documents', + container_name="documents", prefix="Reports/Curation_Reports/Ecommerce", - include_metadata='yes', - max_results=10 + include_metadata="yes", + max_results=10, ) print(blobs) From 5f836fe487f2062eb3460fbfb1e64c91ce7d2b78 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Sun, 12 Jan 2025 06:49:49 -0400 Subject: [PATCH 288/820] FA Use secret for Azure Blob Storage connection string and enhance error handling (#258) --- backend/financial_doc_processor.py | 32 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index cfd67071..58646934 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -41,9 +41,32 @@ def get_secret(secretName): return retrieved_secret.value +# Retrieve the connection string for Azure Blob Storage from secrets BLOB_CONNECTION_STRING = get_secret("storageConnectionString") +# Retrieve the Blob container name from environment variables BLOB_CONTAINER_NAME = os.getenv("BLOB_CONTAINER_NAME") +# Validate that the connection string is available +if not BLOB_CONNECTION_STRING: + raise ValueError( + "The connection string for Azure Blob Storage (BLOB_CONNECTION_STRING) is not set. Please ensure it is correctly configured." + ) + +# Validate that the container name is provided +if not BLOB_CONTAINER_NAME: + raise ValueError( + "The Blob container name (BLOB_CONTAINER_NAME) is not set. Please ensure it is correctly configured." + ) + +# Assign the Blob container name to a variable for clarity +blob_container_name = BLOB_CONTAINER_NAME + +# Initialize the Blob service client using the provided connection string +blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONNECTION_STRING) + +# Get the container client for the specified Blob container +container_client = blob_service_client.get_container_client(blob_container_name) + # configure logging logging.basicConfig( @@ -129,15 +152,6 @@ def collect_filing_documents( return dict(document_paths) -BLOB_CONNECTION_STRING = os.getenv("BLOB_CONNECTION_STRING") -if not BLOB_CONNECTION_STRING: - raise ValueError("BLOB_CONNECTION_STRING environment variable is not set") -blob_container_name = BLOB_CONTAINER_NAME - -blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONNECTION_STRING) -container_client = blob_service_client.get_container_client(blob_container_name) - - def validate_document_paths(document_paths: Dict[str, Dict[str, str]]) -> bool: """ Validate the collected document paths. From 7937aabc0af77fad04a00391144e4d56dd068f6e Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Sun, 12 Jan 2025 07:34:13 -0400 Subject: [PATCH 289/820] FA349 Improve error handling, logging, and security for Azure Blob Storage connection string retrieval (#259) --- backend/financial_doc_processor.py | 82 +++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 58646934..1d6901da 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -31,41 +31,73 @@ load_dotenv() -def get_secret(secretName): - keyVaultName = os.environ["AZURE_KEY_VAULT_NAME"] - KVUri = f"https://{keyVaultName}.vault.azure.net" - credential = DefaultAzureCredential() - client = SecretClient(vault_url=KVUri, credential=credential) - logging.info(f"[webbackend] retrieving {secretName} secret from {keyVaultName}.") - retrieved_secret = client.get_secret(secretName) - return retrieved_secret.value +def get_secret(secret_name): + """ + Retrieve a secret value from Azure Key Vault. + + Args: + secret_name (str): The name of the secret to retrieve. + + Returns: + str: The value of the secret. + + Raises: + Exception: If the secret cannot be retrieved. + """ + try: + keyVaultName = os.getenv("AZURE_KEY_VAULT_NAME") + if not keyVaultName: + raise ValueError("Environment variable 'AZURE_KEY_VAULT_NAME' is not set.") + + KVUri = f"https://{keyVaultName}.vault.azure.net" + credential = DefaultAzureCredential() + client = SecretClient(vault_url=KVUri, credential=credential) + logging.info( + f"[webbackend] retrieving {secret_name} secret from {keyVaultName}." + ) + retrieved_secret = client.get_secret(secret_name) + return retrieved_secret.value + except Exception as e: + logging.error(f"Failed to retrieve secret '{secret_name}': {e}") + raise # Retrieve the connection string for Azure Blob Storage from secrets -BLOB_CONNECTION_STRING = get_secret("storageConnectionString") -# Retrieve the Blob container name from environment variables -BLOB_CONTAINER_NAME = os.getenv("BLOB_CONTAINER_NAME") +try: + BLOB_CONNECTION_STRING = get_secret("storageConnectionString") + if not BLOB_CONNECTION_STRING: + raise ValueError( + "The connection string for Azure Blob Storage (BLOB_CONNECTION_STRING): '{BLOB_CONNECTION_STRING}' is not set. Please ensure it is correctly configured." + ) -# Validate that the connection string is available -if not BLOB_CONNECTION_STRING: - raise ValueError( - "The connection string for Azure Blob Storage (BLOB_CONNECTION_STRING) is not set. Please ensure it is correctly configured." - ) + logging.info("Successfully retrieved Blob connection string.") + # Validate that the connection string is available + +except Exception as e: + logging.error("Error retrieving the connection string for Azure Blob Storage.") + logging.debug(f"Detailed error: {e}") # Log detailed errors at the debug level + raise -# Validate that the container name is provided +# Retrieve the Blob container name from environment variables +BLOB_CONTAINER_NAME = os.getenv("BLOB_CONTAINER_NAME") if not BLOB_CONTAINER_NAME: raise ValueError( "The Blob container name (BLOB_CONTAINER_NAME) is not set. Please ensure it is correctly configured." ) -# Assign the Blob container name to a variable for clarity -blob_container_name = BLOB_CONTAINER_NAME - -# Initialize the Blob service client using the provided connection string -blob_service_client = BlobServiceClient.from_connection_string(BLOB_CONNECTION_STRING) - -# Get the container client for the specified Blob container -container_client = blob_service_client.get_container_client(blob_container_name) +# Initialize the Blob service client +try: + blob_service_client = BlobServiceClient.from_connection_string( + BLOB_CONNECTION_STRING + ) + container_client = blob_service_client.get_container_client(BLOB_CONTAINER_NAME) + if not container_client.exists(): + logging.warning(f"Blob container '{BLOB_CONTAINER_NAME}' does not exist.") + # Uncomment below to create the container dynamically: + # container_client.create_container() +except Exception as e: + logging.error(f"Failed to initialize Blob service client or access container: {e}") + raise # configure logging From 7bf6c87aaa205a4bcbe3acd7f895235a8e6cdf84 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Sun, 12 Jan 2025 12:17:34 -0400 Subject: [PATCH 290/820] FA 349 get secrets and rename enviroments vars (#260) --- backend/app.py | 1247 ++++++++++++++++++---------- backend/app_config.py | 10 +- backend/financial_doc_processor.py | 34 +- backend/llm_config.py | 45 +- 4 files changed, 848 insertions(+), 488 deletions(-) diff --git a/backend/app.py b/backend/app.py index e63814c6..f8299216 100644 --- a/backend/app.py +++ b/backend/app.py @@ -51,7 +51,7 @@ ) import stripe.error -from shared.cosmo_db import( +from shared.cosmo_db import ( create_report, get_report, get_user_container, @@ -62,7 +62,7 @@ delete_template, get_templates, get_template_by_ID, - update_user + update_user, ) load_dotenv(override=True) @@ -111,9 +111,26 @@ def get_secret(secretName): SPEECH_RECOGNITION_LANGUAGE = os.getenv("SPEECH_RECOGNITION_LANGUAGE") SPEECH_SYNTHESIS_LANGUAGE = os.getenv("SPEECH_SYNTHESIS_LANGUAGE") SPEECH_SYNTHESIS_VOICE_NAME = os.getenv("SPEECH_SYNTHESIS_VOICE_NAME") -AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING") AZURE_CSV_STORAGE_NAME = os.getenv("AZURE_CSV_STORAGE_CONTAINER", "files") + +# Retrieve the connection string for Azure Blob Storage from secrets +try: + AZURE_STORAGE_CONNECTION_STRING = get_secret("storageConnectionString") + if not AZURE_STORAGE_CONNECTION_STRING: + raise ValueError( + "The connection string for Azure Blob Storage (AZURE_STORAGE_CONNECTION_STRING): is not set. Please ensure it is correctly configured." + ) + + logging.info("Successfully retrieved Blob connection string.") + # Validate that the connection string is available + +except Exception as e: + logging.error("Error retrieving the connection string for Azure Blob Storage.") + logging.debug(f"Detailed error: {e}") # Log detailed errors at the debug level + raise + + logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) @@ -649,7 +666,8 @@ def deleteChatConversation(chat_id): logging.exception("[webbackend] exception in /delete-chat-conversation") return jsonify({"error": str(e)}), 500 -#get report by id argument from Container Reports + +# get report by id argument from Container Reports @app.route("/api/reports/", methods=["GET"]) @auth.login_required() def getReport(*, context, report_id): @@ -663,10 +681,13 @@ def getReport(*, context, report_id): logging.warning(f"Report with id {report_id} not found.") return jsonify({"error": f"Report with this id {report_id} not found"}), 404 except Exception as e: - logging.exception(f"An error occurred retrieving the report with id {report_id}") + logging.exception( + f"An error occurred retrieving the report with id {report_id}" + ) return jsonify({"error": "Internal Server Error"}), 500 -#create Reports curation and companySummarization container Reports + +# create Reports curation and companySummarization container Reports @app.route("/api/reports", methods=["POST"]) @auth.login_required() def createReport(*, context): @@ -678,17 +699,24 @@ def createReport(*, context): if not data: return jsonify({"error": "Invalid or missing JSON payload"}), 400 - + # Validate the 'name' field if "name" not in data: return jsonify({"error": "Field 'name' is required"}), 400 - + # Validate the 'type' field if "type" not in data: return jsonify({"error": "Field 'type' is required"}), 400 if data["type"] not in ["curation", "companySummarization"]: - return jsonify({"error": "Invalid 'type'. Must be 'curation' or 'companySummarization'"}), 400 + return ( + jsonify( + { + "error": "Invalid 'type'. Must be 'curation' or 'companySummarization'" + } + ), + 400, + ) # Validate fields according to type if data["type"] == "companySummarization": @@ -696,24 +724,52 @@ def createReport(*, context): missing_fields = [field for field in required_fields if field not in data] if missing_fields: - return jsonify({"error": f"Missing required fields: {', '.join(missing_fields)}"}), 400 + return ( + jsonify( + { + "error": f"Missing required fields: {', '.join(missing_fields)}" + } + ), + 400, + ) # Validate 'reportTemplate' valid_templates = ["10-K", "10-Q", "8-K", "DEF 14A"] if data["reportTemplate"] not in valid_templates: - return jsonify({"error": f"'reportTemplate' must be one of: {', '.join(valid_templates)}"}), 400 + return ( + jsonify( + { + "error": f"'reportTemplate' must be one of: {', '.join(valid_templates)}" + } + ), + 400, + ) elif data["type"] == "curation": required_fields = ["category"] missing_fields = [field for field in required_fields if field not in data] if missing_fields: - return jsonify({"error": f"Missing required fields: {', '.join(missing_fields)}"}), 400 + return ( + jsonify( + { + "error": f"Missing required fields: {', '.join(missing_fields)}" + } + ), + 400, + ) # Validate 'category' valid_categories = ["Ecommerce", "Weekly Economic", "Monthly Economic"] if data["category"] not in valid_categories: - return jsonify({"error": f"'category' must be one of: {', '.join(valid_categories)}"}), 400 + return ( + jsonify( + { + "error": f"'category' must be one of: {', '.join(valid_categories)}" + } + ), + 400, + ) # Validar the 'status' field if "status" not in data: @@ -721,7 +777,12 @@ def createReport(*, context): valid_statuses = ["active", "archived"] if data["status"] not in valid_statuses: - return jsonify({"error": f"'status' must be one of: {', '.join(valid_statuses)}"}), 400 + return ( + jsonify( + {"error": f"'status' must be one of: {', '.join(valid_statuses)}"} + ), + 400, + ) # Delegate report creation new_report = create_report(data) @@ -729,9 +790,13 @@ def createReport(*, context): except Exception as e: logging.exception("Error creating report") - return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + return ( + jsonify({"error": "An unexpected error occurred. Please try again later."}), + 500, + ) + -#update Reports curation and companySummarization container Reports +# update Reports curation and companySummarization container Reports @app.route("/api/reports/", methods=["PUT"]) @auth.login_required() def updateReport(*, context, report_id): @@ -743,19 +808,32 @@ def updateReport(*, context, report_id): if updated_data is None: return jsonify({"error": "Invalid or missing JSON payload"}), 400 - + updated_report = update_report(report_id, updated_data) return "", 204 - + except NotFound as e: logging.warning(f"Tried to update a report that doesn't exist: {report_id}") - return jsonify({"error": f"Tried to update a report with this id {report_id} that does not exist"}), 404 + return ( + jsonify( + { + "error": f"Tried to update a report with this id {report_id} that does not exist" + } + ), + 404, + ) except Exception as e: - logging.exception(f"Error updating report with ID {report_id}") # Logs the full exception - return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 - -#delete report from Container Reports + logging.exception( + f"Error updating report with ID {report_id}" + ) # Logs the full exception + return ( + jsonify({"error": "An unexpected error occurred. Please try again later."}), + 500, + ) + + +# delete report from Container Reports @app.route("/api/reports/", methods=["DELETE"]) @auth.login_required() def deleteReport(*, context, report_id): @@ -764,20 +842,23 @@ def deleteReport(*, context, report_id): """ try: delete_report(report_id) - - return "",204 - + + return "", 204 + except NotFound as e: # If the report does not exist, return 404 Not Found logging.warning(f"Report with id {report_id} not found.") return jsonify({"error": f"Report with id {report_id} not found."}), 404 - + except Exception as e: logging.exception(f"Error deleting report with id {report_id}") - return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + return ( + jsonify({"error": "An unexpected error occurred. Please try again later."}), + 500, + ) -#Get User for email receivers +# Get User for email receivers @app.route("/api/user/", methods=["GET"]) @auth.login_required() def getUserid(*, context, user_id): @@ -794,7 +875,8 @@ def getUserid(*, context, user_id): logging.exception(f"An error occurred retrieving the report with id {user_id}") return jsonify({"error": "Internal Server Error"}), 500 -#Update Users + +# Update Users @app.route("/api/user/", methods=["PUT"]) @auth.login_required() def updateUser(*, context, user_id): @@ -806,17 +888,30 @@ def updateUser(*, context, user_id): if updated_data is None: return jsonify({"error": "Invalid or missing JSON payload"}), 400 - + updated_data = update_user(user_id, updated_data) return "", 204 - + except NotFound as e: logging.warning(f"Tried to update a user that doesn't exist: {user_id}") - return jsonify({"error": f"Tried to update a user with this id {user_id} that does not exist"}), 404 + return ( + jsonify( + { + "error": f"Tried to update a user with this id {user_id} that does not exist" + } + ), + 404, + ) except Exception as e: - logging.exception(f"Error updating user with ID {user_id}") # Logs the full exception - return jsonify({"error": "An unexpected error occurred. Please try again later."}), 500 + logging.exception( + f"Error updating user with ID {user_id}" + ) # Logs the full exception + return ( + jsonify({"error": "An unexpected error occurred. Please try again later."}), + 500, + ) + @app.route("/api/reports", methods=["GET"]) @auth.login_required() @@ -842,6 +937,7 @@ def getFilteredType(*, context): logging.exception(f"Error retrieving reports.") return jsonify({"error": "Internal Server Error"}), 500 + @app.route("/api/reports/summarization/templates", methods=["POST"]) @auth.login_required() def addSummarizationReport(*, context): @@ -859,35 +955,50 @@ def addSummarizationReport(*, context): JSON response with the created report template if successful. JSON error response with appropriate HTTP status code if an error occurs. """ - try: + try: data = request.get_json() if not data: - raise MissingJSONPayloadError('Missing JSON payload') + raise MissingJSONPayloadError("Missing JSON payload") if not "name" in data: - raise MissingRequiredFieldError('name') + raise MissingRequiredFieldError("name") if not "description" in data: - raise MissingRequiredFieldError('description') + raise MissingRequiredFieldError("description") if not "companyTickers" in data: - raise MissingRequiredFieldError('companyTickers') - valid_names=["10-K", "10-Q", "8-K", "DEF 14A"] + raise MissingRequiredFieldError("companyTickers") + valid_names = ["10-K", "10-Q", "8-K", "DEF 14A"] if not data["name"] in valid_names: - raise InvalidParameterError('name', f"Must be one of: {', '.join(valid_names)}") - new_template = {'name': data['name'], 'companyTickers': data['companyTickers'], 'description': data['description'], 'status': 'active', 'type': 'summarization'} + raise InvalidParameterError( + "name", f"Must be one of: {', '.join(valid_names)}" + ) + new_template = { + "name": data["name"], + "companyTickers": data["companyTickers"], + "description": data["description"], + "status": "active", + "type": "summarization", + } # add to cosmosDB container result = create_template(new_template) return create_success_response(result) except MissingJSONPayloadError as e: - return create_error_response("Invalid or Missing JSON payload", HTTPStatus.BAD_REQUEST) + return create_error_response( + "Invalid or Missing JSON payload", HTTPStatus.BAD_REQUEST + ) except MissingRequiredFieldError as field: - return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) + return create_error_response( + f"Field '{field}' is required", HTTPStatus.BAD_REQUEST + ) except InvalidParameterError as e: return create_error_response(str(e), HTTPStatus.BAD_REQUEST) except Exception as e: logging.exception(e) - return jsonify({"error": "An unexpected error occurred. Please try again later."}), HTTPStatus.INTERNAL_SERVER_ERROR + return ( + jsonify({"error": "An unexpected error occurred. Please try again later."}), + HTTPStatus.INTERNAL_SERVER_ERROR, + ) -@app.route('/api/reports/summarization/templates/', methods=['DELETE']) +@app.route("/api/reports/summarization/templates/", methods=["DELETE"]) @auth.login_required() def removeSummarizationReport(*, context, template_id): """ @@ -906,37 +1017,53 @@ def removeSummarizationReport(*, context, template_id): """ try: if not template_id: - raise MissingRequiredFieldError('template_id') - #delete from cosmosDB container + raise MissingRequiredFieldError("template_id") + # delete from cosmosDB container result = delete_template(template_id) return create_success_response(result) except NotFound as e: - return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) + return create_error_response( + f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND + ) except MissingRequiredFieldError as field: - return create_error_response(f"Field '{field}' is required", HTTPStatus.BAD_REQUEST) + return create_error_response( + f"Field '{field}' is required", HTTPStatus.BAD_REQUEST + ) except Exception as e: - return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + return create_error_response( + "An unexpected error occurred. Please try again later.", + HTTPStatus.INTERNAL_SERVER_ERROR, + ) + -@app.route('/api/reports/summarization/templates/', methods=['GET']) +@app.route("/api/reports/summarization/templates/", methods=["GET"]) @auth.login_required() def getSummarizationReports(*, context): try: result = get_templates() return create_success_response(result) except Exception as e: - return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) + return create_error_response( + "An unexpected error occurred. Please try again later.", + HTTPStatus.INTERNAL_SERVER_ERROR, + ) + -@app.route('/api/reports/summarization/templates/', methods=['GET']) +@app.route("/api/reports/summarization/templates/", methods=["GET"]) @auth.login_required() def getSummarizationReport(*, context, template_id): try: result = get_template_by_ID(template_id) return create_success_response(result) except NotFound as e: - return create_error_response(f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND) + return create_error_response( + f"Template with id '{template_id}' not found", HTTPStatus.NOT_FOUND + ) except Exception as e: - return create_error_response("An unexpected error occurred. Please try again later.", HTTPStatus.INTERNAL_SERVER_ERROR) - + return create_error_response( + "An unexpected error occurred. Please try again later.", + HTTPStatus.INTERNAL_SERVER_ERROR, + ) # methods to provide access to speech services and blob storage account blobs @@ -2280,20 +2407,22 @@ def determine_subscription_tiers(subscription): from app_config import FILING_TYPES, BASE_FOLDER -doc_processor = FinancialDocumentProcessor() # from financial_doc_processor -@app.route('/api/SECEdgar/financialdocuments', methods=['GET']) +doc_processor = FinancialDocumentProcessor() # from financial_doc_processor + + +@app.route("/api/SECEdgar/financialdocuments", methods=["GET"]) def process_edgar_document(): """ Process a single financial document from SEC EDGAR. - + Args for payload: equity_id (str): Stock symbol/ticker (e.g., 'AAPL') filing_type (str): SEC filing type (e.g., '10-K') after_date (str, optional): Filter for filings after this date (YYYY-MM-DD) - + Returns: JSON Response with processing status and results - + Raises: 400: Invalid request parameters 404: Document not found @@ -2302,42 +2431,60 @@ def process_edgar_document(): try: # Validate request and setup if not check_and_install_wkhtmltopdf(): - return jsonify({ - "status": "error", - "message": "Failed to install required dependency wkhtmltopdf", - "code": 500 - }), 500 + return ( + jsonify( + { + "status": "error", + "message": "Failed to install required dependency wkhtmltopdf", + "code": 500, + } + ), + 500, + ) # Get and validate parameters data = request.get_json() if not data: - return jsonify({ - "status": "error", - "message": "No data provided", - "code": 400 - }), 400 + return ( + jsonify( + {"status": "error", "message": "No data provided", "code": 400} + ), + 400, + ) # Extract and validate parameters - equity_id = data.get('equity_id') - filing_type = data.get('filing_type') - after_date = data.get('after_date', None) + equity_id = data.get("equity_id") + filing_type = data.get("filing_type") + after_date = data.get("after_date", None) if not equity_id or not filing_type: - return jsonify({ - "status": "error", - "message": "Both equity_id and filing_type are required", - "code": 400 - }), 400 + return ( + jsonify( + { + "status": "error", + "message": "Both equity_id and filing_type are required", + "code": 400, + } + ), + 400, + ) if filing_type not in FILING_TYPES: - return jsonify({ - "status": "error", - "message": f"Invalid filing type. Must be one of: {FILING_TYPES}", - "code": 400 - }), 400 + return ( + jsonify( + { + "status": "error", + "message": f"Invalid filing type. Must be one of: {FILING_TYPES}", + "code": 400, + } + ), + 400, + ) # Download filing - download_result = doc_processor.download_filing(equity_id, filing_type, after_date) + download_result = doc_processor.download_filing( + equity_id, filing_type, after_date + ) if download_result.get("status") != "success": return jsonify(download_result), download_result.get("code", 500) @@ -2348,17 +2495,15 @@ def process_edgar_document(): except Exception as e: logger.error(f"API execution failed: {str(e)}") - return jsonify({ - "status": "error", - "message": str(e), - "code": 500 - }), 500 + return jsonify({"status": "error", "message": str(e), "code": 500}), 500 from tavily_tool import TavilySearch -@app.route('/api/web-search', methods = ['POST']) + + +@app.route("/api/web-search", methods=["POST"]) def web_search(): - """ + """ Endpoint for multiple web search Expected Json body: @@ -2372,74 +2517,83 @@ def web_search(): """ try: data = request.get_json() - + # validate required fields: - if not data or 'query' not in data: + if not data or "query" not in data: logger.error("Missing required field: 'query'") - return jsonify({ - 'error': "Missing required field: 'query'" - }), 400 - - # get optional parameters - mode = data.get('mode', 'news') - max_results = data.get('max_results', 2) + return jsonify({"error": "Missing required field: 'query'"}), 400 + + # get optional parameters + mode = data.get("mode", "news") + max_results = data.get("max_results", 2) if not isinstance(max_results, int) or max_results < 1: logger.error(f"Invalid max_results: {max_results}") - return jsonify({ - 'error': "Invalid max_results. Please provide a positive integer." - }), 400 - include_domains = data.get('include_domains', None) + return ( + jsonify( + {"error": "Invalid max_results. Please provide a positive integer."} + ), + 400, + ) + include_domains = data.get("include_domains", None) if include_domains is not None and not isinstance(include_domains, list): logger.error(f"Invalid include_domains: {include_domains}") - return jsonify({ - 'error': "Invalid include_domains. Please provide a list of strings." - }), 400 - - search_days = data.get('search_days', 30) - + return ( + jsonify( + { + "error": "Invalid include_domains. Please provide a list of strings." + } + ), + 400, + ) + + search_days = data.get("search_days", 30) # initialize searcher logger.info("Initializing TavilySearch") try: - searcher = TavilySearch(max_results=max_results, - include_domains=include_domains, - search_days = search_days) + searcher = TavilySearch( + max_results=max_results, + include_domains=include_domains, + search_days=search_days, + ) except ValueError as e: logger.error(f"Error initializing TavilySearch: {e}") - return jsonify({ - 'error': f"Invalid configuration: {str(e)}" - }), 400 + return jsonify({"error": f"Invalid configuration: {str(e)}"}), 400 # perform search based on mode. If mode is not provided, default to news - if mode.lower() == 'news': + if mode.lower() == "news": logger.info("Performing news search") - results = searcher.search_news(data['query']) - elif mode.lower() == 'general': + results = searcher.search_news(data["query"]) + elif mode.lower() == "general": logger.info("Performing general search") - results = searcher.search_general(data['query']) + results = searcher.search_general(data["query"]) else: logger.error("Invalid mode. Please use 'news' or 'general'.") - return jsonify({ - 'error': "Invalid mode. Please use 'news' or 'general'." - }), 400 - + return ( + jsonify({"error": "Invalid mode. Please use 'news' or 'general'."}), + 400, + ) + # format results logger.info("Formatting search results") formatted_results = searcher.format_result(results) - + logger.info("Search completed successfully") return jsonify(formatted_results) except Exception as e: logger.error(f"An error occurred: {e}") - return jsonify({ - 'error': "An error occurred while processing the request." - }), 500 + return ( + jsonify({"error": "An error occurred while processing the request."}), + 500, + ) from app_config import IMAGE_PATH from summarization import DocumentSummarizer -@app.route('/api/SECEdgar/financialdocuments/summary', methods=['POST']) + + +@app.route("/api/SECEdgar/financialdocuments/summary", methods=["POST"]) def generate_summary(): """ Endpoint to generate a summary of financial documents from SEC Edgar. @@ -2457,39 +2611,64 @@ def generate_summary(): Both fields must be non-empty strings. """ try: - try: + try: data = request.get_json() - if not data: - return jsonify({ - 'error': 'Invalid request', - 'details': 'Request body is requred and must be a valid JSON object' - }), 400 - equity_name = data.get('equity_name') - financial_type = data.get('financial_type') + if not data: + return ( + jsonify( + { + "error": "Invalid request", + "details": "Request body is requred and must be a valid JSON object", + } + ), + 400, + ) + equity_name = data.get("equity_name") + financial_type = data.get("financial_type") if not all([equity_name, financial_type]): - return jsonify({ - 'error': 'Missing required fields', - 'details': 'equity_name and financial_type are required' - }), 400 - + return ( + jsonify( + { + "error": "Missing required fields", + "details": "equity_name and financial_type are required", + } + ), + 400, + ) + if not isinstance(equity_name, str) or not isinstance(financial_type, str): - return jsonify({ - 'error': 'Invalid input type', - 'details': 'equity_name and financial_type must be strings' - }), 400 - + return ( + jsonify( + { + "error": "Invalid input type", + "details": "equity_name and financial_type must be strings", + } + ), + 400, + ) + if not equity_name.strip() or not financial_type.strip(): - return jsonify({ - 'error': 'Empty input', - 'details': 'equity_name and financial_type cannot be empty' - }), 400 - + return ( + jsonify( + { + "error": "Empty input", + "details": "equity_name and financial_type cannot be empty", + } + ), + 400, + ) + except ValueError as e: - return jsonify({ - 'error': 'Invalid input', - 'details': f"Failed to parse request body: {str(e)}" - }), 400 + return ( + jsonify( + { + "error": "Invalid input", + "details": f"Failed to parse request body: {str(e)}", + } + ), + 400, + ) # Initialize components with error handling try: @@ -2497,86 +2676,111 @@ def generate_summary(): summarizer = DocumentSummarizer() except ConnectionError as e: logging.error(f"Failed to connect to blob storage: {e}") - return jsonify({ - 'error': 'Connection error', - 'details': 'Failed to connect to storage service' - }), 503 + return ( + jsonify( + { + "error": "Connection error", + "details": "Failed to connect to storage service", + } + ), + 503, + ) except Exception as e: logging.error(f"Failed to initialize components: {e}") - return jsonify({ - 'error': 'Service initialization failed', - 'details': str(e) - }), 500 + return ( + jsonify({"error": "Service initialization failed", "details": str(e)}), + 500, + ) # Reset directories try: reset_local_dirs() except PermissionError as e: logging.error(f"Permission error while cleaning up directories: {str(e)}") - return jsonify({ - 'error': 'Permission error', - 'details': 'Failed to clean up directories due to permission issues' - }), 500 + return ( + jsonify( + { + "error": "Permission error", + "details": "Failed to clean up directories due to permission issues", + } + ), + 500, + ) except OSError as e: logging.error(f"OS error while reseting directories: {str(e)}") - return jsonify({ - 'error': 'System error', - 'details': 'Failed to prepare working directories' - }), 500 + return ( + jsonify( + { + "error": "System error", + "details": "Failed to prepare working directories", + } + ), + 500, + ) except Exception as e: logging.error(f"Failed to clean up directories: {e}") - return jsonify({ - 'error': 'Cleanup failed', - 'details': 'Failed to clean up directories to prepare for processing' - }), 500 - + return ( + jsonify( + { + "error": "Cleanup failed", + "details": "Failed to clean up directories to prepare for processing", + } + ), + 500, + ) + # Download documents - downloaded_files = blob_manager.download_documents(equity_name=equity_name, - financial_type=financial_type) + downloaded_files = blob_manager.download_documents( + equity_name=equity_name, financial_type=financial_type + ) # Process documents for file_path in downloaded_files: doc_id = extract_pdf_pages_to_images(file_path, IMAGE_PATH) - + # Generate summaries all_summaries = summarizer.process_document_images(IMAGE_PATH) final_summary = summarizer.generate_final_summary(all_summaries) - + # Save the summary locally and upload to blob - local_output_path = f'pdf/{financial_type}_{equity_name}_summary.pdf' + local_output_path = f"pdf/{financial_type}_{equity_name}_summary.pdf" save_str_to_pdf(final_summary, local_output_path) - + # Upload summary to blob - document_paths = create_document_paths(local_output_path, equity_name, financial_type) + document_paths = create_document_paths( + local_output_path, equity_name, financial_type + ) # upload to blob and get the blob path/remote links upload_results = blob_manager.upload_to_blob(document_paths) - blob_path = upload_results[equity_name][financial_type]['blob_path'] - blob_url = upload_results[equity_name][financial_type]['blob_url'] + blob_path = upload_results[equity_name][financial_type]["blob_path"] + blob_url = upload_results[equity_name][financial_type]["blob_url"] # Clean up local directories try: reset_local_dirs() except Exception as e: logging.error(f"Failed to clean up directories: {e}") - - return jsonify({ - 'status': 'success', - 'equity_name': equity_name, - 'financial_type': financial_type, - 'blob_path': blob_path, - 'remote_blob_url': blob_url, - 'summary': final_summary, - }), 200 + + return ( + jsonify( + { + "status": "success", + "equity_name": equity_name, + "financial_type": financial_type, + "blob_path": blob_path, + "remote_blob_url": blob_url, + "summary": final_summary, + } + ), + 200, + ) except Exception as e: logging.error(f"Unexpected error: {e}", exc_info=True) - return jsonify({ - 'error': 'Internal server error', - 'details': str(e) - }), 500 + return jsonify({"error": "Internal server error", "details": str(e)}), 500 finally: # Ensure cleanup happens try: @@ -2588,17 +2792,20 @@ def generate_summary(): except Exception as e: logging.error(f"Failed to clean up: {e}") + from utils import _extract_response_data -@app.route('/api/SECEdgar/financialdocuments/process-and-summarize', methods=['POST']) + + +@app.route("/api/SECEdgar/financialdocuments/process-and-summarize", methods=["POST"]) def process_and_summarize_document(): """ Process and summarize a financial document in sequence. - + Args: equity_id (str): Stock symbol/ticker (e.g., 'AAPL') filing_type (str): SEC filing type (e.g., '10-K') after_date (str, optional): Filter for filings after this date (YYYY-MM-DD) - + Returns: JSON Response with structure: { @@ -2606,7 +2813,7 @@ def process_and_summarize_document(): "edgar_data_process": {...}, "summary_process": {...} } - + Raises: 400: Invalid request parameters 404: Document not found @@ -2616,177 +2823,245 @@ def process_and_summarize_document(): try: data = request.get_json() if not data: - return jsonify({ - 'status': 'error', - 'error': 'Invalid request', - 'details': 'Request body is requred and must be a valid JSON object', - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 400 + return ( + jsonify( + { + "status": "error", + "error": "Invalid request", + "details": "Request body is requred and must be a valid JSON object", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 400, + ) # Validate required fields - required_fields = ['equity_id', 'filing_type'] + required_fields = ["equity_id", "filing_type"] if not all(field in data for field in required_fields): - return jsonify({ - 'status': 'error', - 'error': 'Missing required fields', - 'details': f"Missing required fields: {', '.join(required_fields)}", - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 400 + return ( + jsonify( + { + "status": "error", + "error": "Missing required fields", + "details": f"Missing required fields: {', '.join(required_fields)}", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 400, + ) # Validate filing type - if data['filing_type'] not in FILING_TYPES: - return jsonify({ - 'status': 'error', - 'error': 'Invalid filing type', - 'details': f"Invalid filing type. Must be one of: {', '.join(FILING_TYPES)}", - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 400 + if data["filing_type"] not in FILING_TYPES: + return ( + jsonify( + { + "status": "error", + "error": "Invalid filing type", + "details": f"Invalid filing type. Must be one of: {', '.join(FILING_TYPES)}", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 400, + ) # Validate date format if provided - if 'after_date' in data: + if "after_date" in data: try: - datetime.strptime(data['after_date'], '%Y-%m-%d') + datetime.strptime(data["after_date"], "%Y-%m-%d") except ValueError: - return jsonify({ - 'status': 'error', - 'error': 'Invalid date format', - 'details': 'Use YYYY-MM-DD', - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 400 + return ( + jsonify( + { + "status": "error", + "error": "Invalid date format", + "details": "Use YYYY-MM-DD", + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 400, + ) except ValueError as e: logger.error(f"Invalid request data: {str(e)}") - return jsonify({ - 'status': 'error', - 'error': 'Invalid request data', - 'details': str(e), - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 400 + return ( + jsonify( + { + "status": "error", + "error": "Invalid request data", + "details": str(e), + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 400, + ) try: # Step 1: Process document - logger.info(f"Starting document processing for {data['equity_id']} {data['filing_type']}") + logger.info( + f"Starting document processing for {data['equity_id']} {data['filing_type']}" + ) with app.test_request_context( - '/api/SECEdgar/financialdocuments', - method='GET', - json=data + "/api/SECEdgar/financialdocuments", method="GET", json=data ) as ctx: process_result = process_edgar_document() process_data = _extract_response_data(process_result) - if process_data.get('status') != 'success': - logger.error(f"Document processing failed: {process_data.get('message')}") - if process_data.get('code') == 404: - return jsonify({ - 'status': 'not_found', - 'error': process_data.get('message'), - 'code': process_data.get('code'), - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 404 + if process_data.get("status") != "success": + logger.error( + f"Document processing failed: {process_data.get('message')}" + ) + if process_data.get("code") == 404: + return ( + jsonify( + { + "status": "not_found", + "error": process_data.get("message"), + "code": process_data.get("code"), + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 404, + ) else: - return jsonify({ - 'status': 'error', - 'error': process_data.get('message'), - 'code': process_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 500 + return ( + jsonify( + { + "status": "error", + "error": process_data.get("message"), + "code": process_data.get( + "code", HTTPStatus.INTERNAL_SERVER_ERROR + ), + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 500, + ) # Step 2: Generate summary - logger.info(f"Starting summary generation for {data['equity_id']} {data['filing_type']}") + logger.info( + f"Starting summary generation for {data['equity_id']} {data['filing_type']}" + ) summary_payload = { "equity_name": data["equity_id"], - "financial_type": data["filing_type"] + "financial_type": data["filing_type"], } with app.test_request_context( - '/api/SECEdgar/financialdocuments/summary', - method='POST', - json=summary_payload + "/api/SECEdgar/financialdocuments/summary", + method="POST", + json=summary_payload, ) as ctx: summary_result = generate_summary() summary_data = _extract_response_data(summary_result) - if summary_data.get('status') != 'success': - logger.error(f"Summary generation failed: {summary_data.get('message')}") - return jsonify({ - 'status': 'error', - 'error': summary_data.get('message'), - 'details': summary_data.get('code', HTTPStatus.INTERNAL_SERVER_ERROR), - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 500 + if summary_data.get("status") != "success": + logger.error( + f"Summary generation failed: {summary_data.get('message')}" + ) + return ( + jsonify( + { + "status": "error", + "error": summary_data.get("message"), + "details": summary_data.get( + "code", HTTPStatus.INTERNAL_SERVER_ERROR + ), + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 500, + ) # Return combined results response_data = { - 'status': 'success', - 'edgar_data_process': process_data, - 'summary_process': summary_data + "status": "success", + "edgar_data_process": process_data, + "summary_process": summary_data, } - - logger.info(f"Successfully processed and summarized document for {data['equity_id']}") + + logger.info( + f"Successfully processed and summarized document for {data['equity_id']}" + ) return jsonify(response_data), 200 except Exception as e: - logger.exception(f"Unexpected error in process_and_summarize_document: {str(e)}") - return jsonify({ - 'status': 'error', - 'error': 'An unexpected error occurred while processing the document', - 'details': str(e), - 'timestamp': datetime.now(timezone.utc).isoformat() - }), 500 + logger.exception( + f"Unexpected error in process_and_summarize_document: {str(e)}" + ) + return ( + jsonify( + { + "status": "error", + "error": "An unexpected error occurred while processing the document", + "details": str(e), + "timestamp": datetime.now(timezone.utc).isoformat(), + } + ), + 500, + ) from pathlib import Path from curation_report_generator import graph from financial_doc_processor import markdown_to_html, BlobStorageManager from financial_agent_utils.curation_report_utils import ( - REPORT_TOPIC_PROMPT_DICT, - InvalidReportTypeError, - ReportGenerationError, - StorageError + REPORT_TOPIC_PROMPT_DICT, + InvalidReportTypeError, + ReportGenerationError, + StorageError, ) from financial_agent_utils.curation_report_config import ( - WEEKLY_CURATION_REPORT, - ALLOWED_CURATION_REPORTS, - NUM_OF_QUERIES + WEEKLY_CURATION_REPORT, + ALLOWED_CURATION_REPORTS, + NUM_OF_QUERIES, ) -@app.route('/api/reports/generate/curation', methods=['POST']) + +@app.route("/api/reports/generate/curation", methods=["POST"]) def generate_report(): try: data = request.get_json() - report_topic_rqst = data['report_topic'] # Will raise KeyError if missing - + report_topic_rqst = data["report_topic"] # Will raise KeyError if missing + # Validate report type if report_topic_rqst not in ALLOWED_CURATION_REPORTS: - raise InvalidReportTypeError(f"Invalid report type. Please choose from: {ALLOWED_CURATION_REPORTS}") + raise InvalidReportTypeError( + f"Invalid report type. Please choose from: {ALLOWED_CURATION_REPORTS}" + ) # Get report configuration report_topic_prompt = REPORT_TOPIC_PROMPT_DICT[report_topic_rqst] search_days = 10 if report_topic_rqst in WEEKLY_CURATION_REPORT else 30 - + # Generate report logger.info(f"Generating report for {report_topic_rqst}") - report = graph.invoke({ - "topic": report_topic_prompt, # this is the prompt to to trigger the agent - "report_type": report_topic_rqst, # this is user request - "number_of_queries": NUM_OF_QUERIES, - "search_mode": "news", - "search_days": search_days - }) + report = graph.invoke( + { + "topic": report_topic_prompt, # this is the prompt to to trigger the agent + "report_type": report_topic_rqst, # this is user request + "number_of_queries": NUM_OF_QUERIES, + "search_mode": "news", + "search_days": search_days, + } + ) # Generate file path current_date = datetime.now(timezone.utc) week_of_month = (current_date.day - 1) // 7 + 1 if report_topic_rqst in WEEKLY_CURATION_REPORT: - file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}/Week_{week_of_month}.html") + file_path = Path( + f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}/Week_{week_of_month}.html" + ) else: - file_path = Path(f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}.html") + file_path = Path( + f"Reports/Curation_Reports/{report_topic_rqst}/{current_date.strftime('%B_%Y')}.html" + ) file_path.parent.mkdir(parents=True, exist_ok=True) # Convert and save report logger.info("Converting markdown to html") - markdown_to_html(report['final_report'], str(file_path)) + markdown_to_html(report["final_report"], str(file_path)) logger.info("Uploading to blob storage") blob_storage_manager = BlobStorageManager() @@ -2796,8 +3071,7 @@ def generate_report(): blob_folder = f"Reports/Curation_Reports/{report_topic_rqst}" upload_result = blob_storage_manager.upload_to_blob( - file_path=str(file_path), - blob_folder=blob_folder + file_path=str(file_path), blob_folder=blob_folder ) # Cleanup files @@ -2805,44 +3079,59 @@ def generate_report(): try: # Use shutil.rmtree to recursively remove directory and all contents import shutil + if file_path.exists(): shutil.rmtree(file_path.parent, ignore_errors=True) logger.info(f"Successfully removed directory: {file_path.parent}") except Exception as e: - logger.warning(f"Error while cleaning up directory {file_path.parent}: {str(e)}") + logger.warning( + f"Error while cleaning up directory {file_path.parent}: {str(e)}" + ) # Continue execution even if cleanup fails pass - return jsonify({ - 'status': 'success', - 'message': f'Report generated for {report_topic_rqst}', - 'report_url': upload_result['blob_url'] - }) + return jsonify( + { + "status": "success", + "message": f"Report generated for {report_topic_rqst}", + "report_url": upload_result["blob_url"], + } + ) except KeyError: logger.error("Missing report_topic in request") - return jsonify({'error': 'report_topic is required'}), 400 - + return jsonify({"error": "report_topic is required"}), 400 + except InvalidReportTypeError as e: logger.error(f"Invalid report topic: {str(e)}") - return jsonify({'error': str(e)}), 400 - + return jsonify({"error": str(e)}), 400 + except Exception as e: - logger.error(f"Unexpected error during report generation: {str(e)}", exc_info=True) - return jsonify({'error': 'An unexpected error occurred while generating the report'}), 500 + logger.error( + f"Unexpected error during report generation: {str(e)}", exc_info=True + ) + return ( + jsonify( + {"error": "An unexpected error occurred while generating the report"} + ), + 500, + ) + from utils import EmailServiceError, EmailService -@app.route('/api/reports/email', methods=['POST']) + + +@app.route("/api/reports/email", methods=["POST"]) def send_email_endpoint(): """Send an email with optional attachments. - Note: currently attachment path has to be in the same directory as the app.py file. - + Note: currently attachment path has to be in the same directory as the app.py file. + Expected JSON payload: { "subject": "Email subject", - "html_content": "HTML formatted content", + "html_content": "HTML formatted content", "recipients": ["email1@domain.com", "email2@domain.com"], - "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. + "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. "save_email": "yes" # Optional, default is "no" } @@ -2853,121 +3142,157 @@ def send_email_endpoint(): # Get and validate request data data = request.get_json() if not data: - return jsonify({ - 'status': 'error', - 'message': 'No JSON data provided' - }), 400 + return jsonify({"status": "error", "message": "No JSON data provided"}), 400 # Validate required fields - required_fields = {'subject', 'html_content', 'recipients'} + required_fields = {"subject", "html_content", "recipients"} missing_fields = required_fields - set(data.keys()) if missing_fields: - return jsonify({ - 'status': 'error', - 'message': f'Missing required fields: {", ".join(missing_fields)}' - }), 400 + return ( + jsonify( + { + "status": "error", + "message": f'Missing required fields: {", ".join(missing_fields)}', + } + ), + 400, + ) # Validate recipients format - if not isinstance(data['recipients'], list): - return jsonify({ - 'status': 'error', - 'message': 'Recipients must be provided as a list' - }), 400 - - if not data['recipients']: - return jsonify({ - 'status': 'error', - 'message': 'At least one recipient is required' - }), 400 + if not isinstance(data["recipients"], list): + return ( + jsonify( + { + "status": "error", + "message": "Recipients must be provided as a list", + } + ), + 400, + ) + + if not data["recipients"]: + return ( + jsonify( + {"status": "error", "message": "At least one recipient is required"} + ), + 400, + ) # Validate attachment path if provided - attachment_path = data.get('attachment_path') + attachment_path = data.get("attachment_path") if attachment_path: # Convert Windows path to proper format - attachment_path = Path(attachment_path.replace('\\', '/')).resolve() + attachment_path = Path(attachment_path.replace("\\", "/")).resolve() if not attachment_path.exists(): - return jsonify({ - 'status': 'error', - 'message': f'Attachment file not found: {attachment_path}' - }), 400 - + return ( + jsonify( + { + "status": "error", + "message": f"Attachment file not found: {attachment_path}", + } + ), + 400, + ) + # Update the attachment_path in data - data['attachment_path'] = str(attachment_path) + data["attachment_path"] = str(attachment_path) - # Validate email configuration + # Validate email configuration email_config = { - 'smtp_server': os.getenv('EMAIL_HOST'), - 'smtp_port': os.getenv('EMAIL_PORT'), - 'username': os.getenv('EMAIL_USER'), - 'password': os.getenv('EMAIL_PASS') + "smtp_server": os.getenv("EMAIL_HOST"), + "smtp_port": os.getenv("EMAIL_PORT"), + "username": os.getenv("EMAIL_USER"), + "password": os.getenv("EMAIL_PASS"), } if not all(email_config.values()): logger.error("Missing email configuration environment variables") - return jsonify({ - 'status': 'error', - 'message': 'Email service configuration error' - }), 500 + return ( + jsonify( + {"status": "error", "message": "Email service configuration error"} + ), + 500, + ) # Initialize and send email email_service = EmailService(**email_config) email_params = { - "subject": data['subject'], - "html_content": data['html_content'], - "recipients": data['recipients'], - "attachment_path": data.get('attachment_path') + "subject": data["subject"], + "html_content": data["html_content"], + "recipients": data["recipients"], + "attachment_path": data.get("attachment_path"), } - # send the email + # send the email email_service.send_email(**email_params) # save the email to blob storage - if data.get('save_email', 'no').lower() == 'yes': + if data.get("save_email", "no").lower() == "yes": blob_name = email_service._save_email_to_blob(**email_params) logger.info(f"Email has been saved to blob storage: {blob_name}") else: - logger.info("Email has not been saved to blob storage because save_email is set to no") + logger.info( + "Email has not been saved to blob storage because save_email is set to no" + ) blob_name = None - return jsonify({ - 'status': 'success', - 'message': 'Email sent successfully', - 'blob_name': blob_name - }), 200 + return ( + jsonify( + { + "status": "success", + "message": "Email sent successfully", + "blob_name": blob_name, + } + ), + 200, + ) except EmailServiceError as e: logger.error(f"Email service error: {str(e)}") - return jsonify({ - 'status': 'error', - 'message': f'Failed to send email: {str(e)}' - }), 500 - + return ( + jsonify({"status": "error", "message": f"Failed to send email: {str(e)}"}), + 500, + ) + except BlobUploadError as e: logger.error(f"Blob upload error: {str(e)}") - return jsonify({ - 'status': 'error', - 'message': f'Email has been sent, but failed to upload to blob storage: {str(e)}' - }), 500 - + return ( + jsonify( + { + "status": "error", + "message": f"Email has been sent, but failed to upload to blob storage: {str(e)}", + } + ), + 500, + ) + except Exception as e: logger.exception("Unexpected error in send_email_endpoint") - return jsonify({ - 'status': 'error', - 'message': f'An unexpected error occurred: {str(e)}' - }), 500 + return ( + jsonify( + { + "status": "error", + "message": f"An unexpected error occurred: {str(e)}", + } + ), + 500, + ) + from rp2email import process_and_send_email -@app.route('/api/reports/digest', methods=['POST']) + + +@app.route("/api/reports/digest", methods=["POST"]) def digest_report(): """ Process report and send email . - + Expected payload: { "blob_link": "https://...", "recipients": ["email1@domain.com"], - "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. + "attachment_path": "path/to/attachment.pdf" # Optional, use forward slashes. By default, it will automatically attach the document from the blob link (PDF converted). Select "no" to disable this feature. "email_subject": "Custom email subject" # Optional "save_email": "yes" # Optional, default is "yes" @@ -2977,58 +3302,68 @@ def digest_report(): # Validate request data data = request.get_json() if not data: - return jsonify({ - 'status': 'error', - 'message': 'No JSON data provided' - }), 400 - + return jsonify({"status": "error", "message": "No JSON data provided"}), 400 + # Validate required fields - if 'blob_link' not in data or 'recipients' not in data: - return jsonify({ - 'status': 'error', - 'message': 'Missing required fields: blob_link and recipients' - }), 400 - + if "blob_link" not in data or "recipients" not in data: + return ( + jsonify( + { + "status": "error", + "message": "Missing required fields: blob_link and recipients", + } + ), + 400, + ) + # Process report and send email success = process_and_send_email( - blob_link=data['blob_link'], - recipients=data['recipients'], - attachment_path=data.get('attachment_path', None), - email_subject=data.get('email_subject', None), - save_email=data.get('save_email', 'yes') + blob_link=data["blob_link"], + recipients=data["recipients"], + attachment_path=data.get("attachment_path", None), + email_subject=data.get("email_subject", None), + save_email=data.get("save_email", "yes"), ) - + if success: - return jsonify({ - 'status': 'success', - 'message': 'Report processed and email sent successfully' - }), 200 + return ( + jsonify( + { + "status": "success", + "message": "Report processed and email sent successfully", + } + ), + 200, + ) else: - return jsonify({ - 'status': 'error', - 'message': 'Failed to process report and send email' - }), 500 - + return ( + jsonify( + { + "status": "error", + "message": "Failed to process report and send email", + } + ), + 500, + ) + except Exception as e: logger.exception("Error processing report and sending email") - return jsonify({ - 'status': 'error', - 'message': str(e) - }), 500 + return jsonify({"status": "error", "message": str(e)}), 500 + -@app.route('/api/reports/storage/files', methods=['GET']) +@app.route("/api/reports/storage/files", methods=["GET"]) def list_blobs(): - """ + """ List blobs i nteh container with optional filtering - Query params: - - prefix(str): filter blobs by prefix - - include_metadata(str): include metadata in results - - max_results(int): maximum number of results to return - - container_name(str): name of the container to list blobs from + Query params: + - prefix(str): filter blobs by prefix + - include_metadata(str): include metadata in results + - max_results(int): maximum number of results to return + - container_name(str): name of the container to list blobs from - Returns: - JSON response with list of blobs + Returns: + JSON response with list of blobs Example Payload: { @@ -3039,50 +3374,42 @@ def list_blobs(): } """ - try: - # get query params + try: + # get query params data = request.get_json() - container_name = data.get('container_name') - prefix = data.get('prefix', None) + container_name = data.get("container_name") + prefix = data.get("prefix", None) - include_metadata = data.get('include_metadata', 'no').lower() + include_metadata = data.get("include_metadata", "no").lower() # convert max_results to int - max_results = data.get('max_results', 10) + max_results = data.get("max_results", 10) if not container_name: - return jsonify({ - "status": "error", - "message": "Blob container name is required" - }), 400 - + return ( + jsonify( + {"status": "error", "message": "Blob container name is required"} + ), + 400, + ) + blob_storage_manager = BlobStorageManager() blobs = blob_storage_manager.list_blobs_in_container( - container_name = container_name, - prefix = prefix, - include_metadata = include_metadata, - max_results =max_results + container_name=container_name, + prefix=prefix, + include_metadata=include_metadata, + max_results=max_results, ) - return jsonify({ - "status": "success", - "data": blobs, - "count": len(blobs) - }), 200 - + return jsonify({"status": "success", "data": blobs, "count": len(blobs)}), 200 + except ValueError as e: - return jsonify({ - "status": "error", - "message": str(e) - }), 400 - + return jsonify({"status": "error", "message": str(e)}), 400 + except Exception as e: logger.exception("Unexpected error in list_blobs") - return jsonify({ - "status": "error", - "message": str(e) - }), 500 + return jsonify({"status": "error", "message": str(e)}), 500 if __name__ == "__main__": diff --git a/backend/app_config.py b/backend/app_config.py index 0d99de88..328633df 100644 --- a/backend/app_config.py +++ b/backend/app_config.py @@ -11,8 +11,8 @@ EDITPROFILE_USER_FLOW = os.getenv( "EDITPROFILE_USER_FLOW" ) # e.g. "B2C_1_profileediting1" -RESETPASSWORD_USER_FLOW = os.getenv( - "RESETPASSWORD_USER_FLOW" +RESETPASSSWORD_USER_FLOW = os.getenv( + "RESETPASSSWORD_USER_FLOW" ) # e.g. "B2C_1_passwordreset1" # Application (client) registration details @@ -33,7 +33,5 @@ BASE_FOLDER = "financial" # Paths in financial summarization -IMAGE_PATH = 'images' -PDF_PATH = './pdf' - - +IMAGE_PATH = "images" +PDF_PATH = "./pdf" diff --git a/backend/financial_doc_processor.py b/backend/financial_doc_processor.py index 1d6901da..553ffbca 100644 --- a/backend/financial_doc_processor.py +++ b/backend/financial_doc_processor.py @@ -721,7 +721,22 @@ def upload_to_blob( if document_paths and file_path: raise ValueError("Cannot provide both document_paths and file_path") + try: + blob_sas_token = get_secret("blobSasToken") + if not blob_sas_token: + raise ValueError( + "The SAS token for Azure Blob Storage (blob_sas_token) is not set. Please ensure it is correctly configured." + ) + + logging.info("Successfully retrieved Blob SAS token.") + # Validate that the SAS token is available + except Exception as e: + logging.error("Error retrieving the SAS token for Azure Blob Storage.") + logging.debug( + f"Detailed error: {e}" + ) # Log detailed errors at the debug level + raise # Handle single file upload if file_path: if not os.path.exists(file_path): @@ -753,7 +768,7 @@ def upload_to_blob( raise BlobUploadError(f"Failed to upload {blob_path}: {str(e)}") # get the blob url for the uploaded file - blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" + blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{blob_sas_token}" result = { "status": "success", @@ -771,7 +786,22 @@ def upload_to_blob( # Handle document_paths dictionary upload (original functionality) if not isinstance(document_paths, dict): raise ValueError("document_paths must be a dictionary") + try: + blob_sas_token = get_secret("blobSasToken") + if not blob_sas_token: + raise ValueError( + "The SAS token for Azure Blob Storage (blob_sas_token) is not set. Please ensure it is correctly configured." + ) + + logging.info("Successfully retrieved Blob SAS token.") + # Validate that the SAS token is available + except Exception as e: + logging.error("Error retrieving the SAS token for Azure Blob Storage.") + logging.debug( + f"Detailed error: {e}" + ) # Log detailed errors at the debug level + raise upload_results = {} for equity, filings in document_paths.items(): upload_results[equity] = {} @@ -813,7 +843,7 @@ def upload_to_blob( ) # get the blob url for the uploaded file - blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{os.getenv('BLOB_SAS_TOKEN')}" + blob_url = f"{self.blob_service_client.url}{os.getenv('BLOB_CONTAINER_NAME')}/{blob_path}?{blob_sas_token}" upload_results[equity][filing_type] = { "status": "success", "blob_path": blob_path, diff --git a/backend/llm_config.py b/backend/llm_config.py index e7238e86..e35924ac 100644 --- a/backend/llm_config.py +++ b/backend/llm_config.py @@ -9,15 +9,17 @@ load_dotenv() + class LLMConfig(BaseModel): - api_base: str = Field(default=os.getenv('OPENAI_API_BASE')) - api_key: str = Field(default=os.getenv('OPENAI_API_KEY')) - api_version: str = Field(default=os.getenv('OPENAI_API_VERSION')) - model_name: str = Field(default=os.getenv('OPENAI_GPT_MODEL')) - + api_base: str = Field(default=os.getenv("AZURE_OPENAI_ENDPOINT")) + api_key: str = Field(default=os.getenv("AZURE_OPENAI_API_KEY")) + api_version: str = Field(default=os.getenv("AZURE_OPENAI_API_VERSION")) + model_name: str = Field(default=os.getenv("OPENAI_GPT_MODEL")) + class Config: frozen = True # Makes the config immutable + class PromptTemplate(BaseModel): image_analysis: str = Field( default=""" @@ -80,34 +82,37 @@ class PromptTemplate(BaseModel): class Config: frozen = True + class LLMManager: def __init__(self): self.prompts = PromptTemplate() self._clients: Dict[str, AzureOpenAI | AzureChatOpenAI] = {} self.config: Dict[str, LLMConfig] = { "gpt4o": LLMConfig( - api_base=os.getenv('OPENAI_API_BASE'), - api_key=os.getenv('OPENAI_API_KEY'), - api_version=os.getenv('OPENAI_API_VERSION'), - model_name=os.getenv('OPENAI_GPT_MODEL') + api_base=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_API_KEY"), + api_version=os.getenv("AZURE_OPENAI_API_VERSION"), + model_name=os.getenv("OPENAI_GPT_MODEL"), ), "embedding": LLMConfig( - api_base=os.getenv('OPENAI_API_BASE'), - api_key=os.getenv('OPENAI_API_KEY'), - api_version=os.getenv('OPENAI_API_VERSION'), - model_name=os.getenv('OPENAI_EMBEDDING_MODEL') - ) + api_base=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_API_KEY"), + api_version=os.getenv("AZURE_OPENAI_API_VERSION"), + model_name=os.getenv("AZURE_OPENAI_EMBEDDING_MODEL"), + ), } - def get_client(self, client_type: str = "gpt4o", use_langchain: bool = False) -> AzureOpenAI | AzureChatOpenAI: + def get_client( + self, client_type: str = "gpt4o", use_langchain: bool = False + ) -> AzureOpenAI | AzureChatOpenAI: """Get or create an Azure OpenAI client - + Args: client_type: Type of client to create ("gpt4o" or "embedding") use_langchain: If True, returns a LangChain AzureChatOpenAI client instead of regular AzureOpenAI """ client_key = f"{client_type}_langchain" if use_langchain else client_type - + if client_key not in self._clients: config = self.config[client_type] if use_langchain: @@ -115,16 +120,16 @@ def get_client(self, client_type: str = "gpt4o", use_langchain: bool = False) -> openai_api_key=config.api_key, openai_api_version=config.api_version, azure_endpoint=config.api_base, - deployment_name=config.model_name + deployment_name=config.model_name, ) else: self._clients[client_key] = AzureOpenAI( api_key=config.api_key, api_version=config.api_version, - base_url=f"{config.api_base}/openai/deployments/{config.model_name}" + base_url=f"{config.api_base}/openai/deployments/{config.model_name}", ) return self._clients[client_key] def get_prompt(self, prompt_type: str) -> str: """Get a prompt template by type""" - return getattr(self.prompts, prompt_type) \ No newline at end of file + return getattr(self.prompts, prompt_type) From 7efc98a2a2bb7bc8b8bca4241ac972964946d97c Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Sun, 12 Jan 2025 22:21:09 -0400 Subject: [PATCH 291/820] FA 340 add missing pah /api to orchestrator uri (#261) --- backend/app.py | 14 +++++++------- backend/llm_config.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/app.py b/backend/app.py index f8299216..edf38402 100644 --- a/backend/app.py +++ b/backend/app.py @@ -70,14 +70,14 @@ SPEECH_REGION = os.getenv("SPEECH_REGION") ORCHESTRATOR_ENDPOINT = os.getenv("ORCHESTRATOR_ENDPOINT") ORCHESTRATOR_URI = os.getenv("ORCHESTRATOR_URI", default="") -SETTINGS_ENDPOINT = ORCHESTRATOR_URI + "/settings" -FEEDBACK_ENDPOINT = ORCHESTRATOR_URI + "/feedback" -HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/conversations" -CHECK_USER_ENDPOINT = ORCHESTRATOR_URI + "/checkUser" -SUBSCRIPTION_ENDPOINT = ORCHESTRATOR_URI + "/subscriptions" -INVITATIONS_ENDPOINT = ORCHESTRATOR_URI + "/invitations" +SETTINGS_ENDPOINT = ORCHESTRATOR_URI + "/api/settings" +FEEDBACK_ENDPOINT = ORCHESTRATOR_URI + "/api/feedback" +HISTORY_ENDPOINT = ORCHESTRATOR_URI + "/api/conversations" +CHECK_USER_ENDPOINT = ORCHESTRATOR_URI + "/api/checkUser" +SUBSCRIPTION_ENDPOINT = ORCHESTRATOR_URI + "/api/subscriptions" +INVITATIONS_ENDPOINT = ORCHESTRATOR_URI + "/api/invitations" STORAGE_ACCOUNT = os.getenv("STORAGE_ACCOUNT") -FINANCIAL_ASSISTANT_ENDPOINT = ORCHESTRATOR_URI + "/financial-orc" +FINANCIAL_ASSISTANT_ENDPOINT = ORCHESTRATOR_URI + "/api/financial-orc" PRODUCT_ID_DEFAULT = os.getenv("STRIPE_PRODUCT_ID") # email diff --git a/backend/llm_config.py b/backend/llm_config.py index e35924ac..6f87876a 100644 --- a/backend/llm_config.py +++ b/backend/llm_config.py @@ -16,6 +16,21 @@ class LLMConfig(BaseModel): api_version: str = Field(default=os.getenv("AZURE_OPENAI_API_VERSION")) model_name: str = Field(default=os.getenv("OPENAI_GPT_MODEL")) + def __init__(self, **data): + super().__init__(**data) + if not self.api_base: + raise ValueError( + "Environment variable 'AZURE_OPENAI_ENDPOINT' is required." + ) + if not self.api_key: + raise ValueError("Environment variable 'AZURE_OPENAI_API_KEY' is required.") + if not self.api_version: + raise ValueError( + "Environment variable 'AZURE_OPENAI_API_VERSION' is required." + ) + if not self.model_name: + raise ValueError("Environment variable 'OPENAI_GPT_MODEL' is required.") + class Config: frozen = True # Makes the config immutable From 2fd4e22988a568ae41d7ef1cd0ea9cef37184816 Mon Sep 17 00:00:00 2001 From: Victor Maldonado <67714035+vmlcode@users.noreply.github.com> Date: Mon, 13 Jan 2025 08:47:34 -0400 Subject: [PATCH 292/820] FA-311 implement UI for managing templates summarization reports (#248) * FEATURE: Add report templates and creation routes to the application * Add Report Templates page and associated styles * Refactor API responses to return data objects for summarization templates and reports * Enhance Report Templates page with delete functionality and loading state management * Add Report Template Creation component and associated styles * Refactor addSummarizationReport function to update required fields for report templates. Changed 'name' to 'templateType' and 'companyTickers' to 'companyTicker' and 'companyName' for improved clarity and consistency in API payloads. * Fix addSummarizationReport function to validate 'templateType' instead of 'name' for report templates. This change enhances error handling by ensuring only valid template types are accepted, improving API payload consistency. * Fix typo in deleteSummarizationReportTemplate function name and update SummarizationReportProps type to include 'templateType', 'companyTicker', and 'companyName' fields for improved API consistency and clarity. * Enhance Report Templates functionality by adding modal popup for report creation confirmation, updating state management for report templates, and refining API integration. Updated styles for modal popup and improved error handling in the ReportTemplateCreation component. * Update ReportTemplates component to reflect new terminology --------- Co-authored-by: Manuel Castro --- frontend/src/App.tsx | 10 +- frontend/src/api/api.ts | 6 +- frontend/src/api/models.ts | 5 +- .../ReportTemplateCreation.module.css | 216 ++++++++++++++++ .../ReportCreation/ReportTemplateCreation.tsx | 136 ++++++++++ .../pages/reports/ReportTemplates.module.css | 239 ++++++++++++++++++ .../src/pages/reports/ReportTemplates.tsx | 168 ++++++++++++ 7 files changed, 772 insertions(+), 8 deletions(-) create mode 100644 frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.module.css create mode 100644 frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx create mode 100644 frontend/src/pages/reports/ReportTemplates.module.css create mode 100644 frontend/src/pages/reports/ReportTemplates.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 99917378..b2b4bc45 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -24,6 +24,8 @@ import CurationReports from "./pages/reports/CurationReports"; import CurationCreation from "./pages/reports/ReportCreation/CurationCreation"; import SummarizationReports from "./pages/reports/SummarizationReports"; import SummarizationCreation from "./pages/reports/ReportCreation/SummarizationCreation"; +import { TemplateCreation } from "./pages/reports/ReportCreation/ReportTemplateCreation"; +import { TemplateReports } from "./pages/reports/ReportTemplates"; export default function App() { return ( @@ -124,9 +126,11 @@ export default function App() { > }> } /> - } /> - } /> - } /> + }/> + }/> + }/> + }/> + }/> diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index c14e1e0d..43dac0bf 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -628,13 +628,13 @@ export async function getSummarizationTemplates() { throw Error('Error getting summarization templates'); } const reports = await response.json(); - return reports; + return reports.data; } export async function getSummarizationReportTemplateByID(templateID: string) { const response = await fetch(`/api/reports/summarization/templates/${templateID}`, {method: 'GET', headers: {'Content-Type': 'application/json'}}); const report = await response.json(); - return report; + return report.data; } export async function createSummarizationReport(templateData: SummarizationReportProps) { @@ -652,7 +652,7 @@ export async function createSummarizationReport(templateData: SummarizationRepor return newReport; } -export async function deleteSumarizationReportTemplate(templateID: string) { +export async function deleteSummarizationReportTemplate(templateID: string) { const response = await fetch(`/api/reports/summarization/templates/${templateID}`, { method: 'DELETE', headers: {'Content-Type': 'application/json'}, diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts index 420d88c5..1c6e0c60 100644 --- a/frontend/src/api/models.ts +++ b/frontend/src/api/models.ts @@ -107,7 +107,8 @@ export type PostSettingsProps = { }; export type SummarizationReportProps = { - name: string; + templateType: string; description: string; - companyTickers: string[]; + companyTicker: string; + companyName: string; } \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.module.css b/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.module.css new file mode 100644 index 00000000..50e0527c --- /dev/null +++ b/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.module.css @@ -0,0 +1,216 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.labelContainer{ + display: flex; + justify-content: flex-start; + width: 100%; +} + +.card { + display: flex; + flex-direction: column; + align-items: flex-start; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 50px; + margin-top: 15px; + flex-grow: 2; +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.textButton{ + text-align: center; + cursor: pointer; + margin-left: 10px; +} + +.button:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: flex-start; + display: flex; + gap: 40px; +} + +.iconColor{ + color: #a7c444; +} + +.input{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: 25em; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.input:focus{ + border: 2px solid #8eb11a; + outline: none; + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +@media (max-width: 900px) { + .input{ + width: 16em; + } +} + +.error { + text-align: center; + font-weight: 400; + font-style: italic; + color: #ff3333; +} + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +.modalPopup{ + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + padding: 100px; + background-color: #cef1a5; + border: 1px solid #87f083; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +@media (max-width: 900px){ + .modal{ + width: 20em; + height: 10em; + padding: 20px; + } + .modalPopup{ + width: 20em; + } +} + +.buttonModalContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +.text{ + text-align: center; +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx b/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx new file mode 100644 index 00000000..cb99ae22 --- /dev/null +++ b/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import styles from './ReportTemplateCreation.module.css'; +import { IconArrowBack, IconX } from '@tabler/icons-react'; +import { Label, Dropdown, IDropdownOption } from '@fluentui/react'; +import { useNavigate } from 'react-router-dom'; +import { createSummarizationReport } from '../../../api'; + +export const TemplateCreation: React.FC = () => { + + const [templateType, setTemplateType] = useState('') + const [company, setCompany] = useState('') + const [companyTicker, setCompanyTicker] = useState('') + const [description, setDescription] = useState('') + const [errorMessage, setErrorMessage] = useState("") + const [isConfirm, setIsConfirm] = useState(false) + + + const templateTypeOptions = [ + { key: '1', text: '10-K' }, + { key: '2', text: '10-Q' }, + { key: '3', text: '8-K' }, + { key: '4', text: 'DEF 14A' }, + ] + const companyOptions = [ + { key: '1', text: "The Home Depot", value: 'LOW' }, + { key: '2', text: "Lowe's Home Improvement", value: 'HD' }, + { key: '3', text: "Walmart", value: 'WMT' }, + { key: '4', text: "Ace Hardware", value: 'ACE' }, + { key: '5', text: "Amazon", value: 'AMZN' }, + { key: '7', text: "Costco", value: 'COST' }, + { key: '8', text: "Target", value: 'TGT' }, + ] + + const handleDropdownTemplate = (event: any, selectedOption: any) => { + setTemplateType(selectedOption.text) + } + + const handleDropdownCompany = (event: any, selectedOption: any) => { + setCompany(selectedOption.text) + setCompanyTicker(selectedOption.value) + } + + const handleInputDescription = (event: React.ChangeEvent) => { + setDescription(event.target.value) + } + + const handleConfirmButton = () => { + if (templateType == '') { + setErrorMessage('Please select a template type') + return + } + if (company == '') { + setErrorMessage('Please select a company') + return + } + if (description == '') { + setErrorMessage('Please enter a description') + return + } + setIsConfirm(!isConfirm) + } + + const handleCancelButton = () => { + setTemplateType('') + setCompany('') + setCompanyTicker('') + setDescription('') + setIsConfirm(!isConfirm) + } + + const handleCreateReport = async () => { + setIsConfirm(false) + let timer: NodeJS.Timeout; + try{ + await createSummarizationReport({ + companyTicker: companyTicker, + templateType: templateType, + description: description, + companyName: company + }) + timer = setTimeout(() => { + navigate('/report-templates', {state: {popUp: true}}) + }, 10); + } catch (error) { + console.error("Error trying to create the report: ", error); + } + } + + const navigate = useNavigate() + return ( +
    +
    + +
    +
    +

    Summarization Report Template Creation

    +
    +
    +
    + + + + + + + + {errorMessage !== null &&

    {errorMessage}

    } +
    + + +
    +
    + {isConfirm && ( +
    + + +
    + + +
    +
    + )} +
    + ); +}; \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportTemplates.module.css b/frontend/src/pages/reports/ReportTemplates.module.css new file mode 100644 index 00000000..794371a7 --- /dev/null +++ b/frontend/src/pages/reports/ReportTemplates.module.css @@ -0,0 +1,239 @@ +.page_container { + display: flex; + flex-direction: column; + width: 100%; + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 20px; + margin-top: 15px; + flex-grow: 2; +} + +.iconColor{ + color: #a7c444; +} + +.labelContainer{ + display: flex; + justify-content: flex-start; + width: 100%; +} + +.text{ + text-align: center; +} + +.textButton{ + text-align: center; + cursor: pointer; + margin-left: 10px; +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #9fc51d; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #ffffff; +} + +.button:hover{ + box-shadow: 0 0 8px rgba(139, 195, 74, 0.5); +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +@media (max-width: 900px){ + .buttonContainer{ + gap: 10px; + } +} + +.table { + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +@media (max-width: 900px){ + .table{ + font-size: 0.7em; + } +} + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +@media (max-width: 900px) { + .modal{ + width: 20em; + } + .info{ + width: 10em; + } +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.thead{ + background-color: #a7c444; + color: white; +} + +.tableName{ + padding: 10px; +} + +.tableText{ + text-align: justify; +} + +.tableBackground{ + background-color: white; +} + +.tableBackgroundAlt{ + background-color: #e9e9e9; +} + +.tableStatusContainer{ + width: 100%; + justify-content: flex-start; + justify-items: center; + text-align: center; + display: flex; + text-transform: capitalize; +} + +.tableStatusActive{ + width: 100px; + background-color: #d2ffd2; + padding: 5px; + color: #a0df2c; + border-radius: 15px; +} + +.tableStatusArchived{ + width: 100px; + background-color: #c6f0ee; + padding: 5px; + color: #2ca6df; + border-radius: 15px; +} + +.modalPopup{ + position: fixed; + top: 10%; + left: 60%; + transform: translateX(-50%); + width: auto; + height: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + padding: 100px; + background-color: #cef1a5; + border: 1px solid #87f083; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportTemplates.tsx b/frontend/src/pages/reports/ReportTemplates.tsx new file mode 100644 index 00000000..406109ea --- /dev/null +++ b/frontend/src/pages/reports/ReportTemplates.tsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from "react"; +import styles from "./ReportTemplates.module.css"; +import { Label, Spinner } from "@fluentui/react"; +import { IconArrowBack, IconEdit, IconFilePlus, IconTrash, IconX } from "@tabler/icons-react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { getSummarizationTemplates, deleteSummarizationReportTemplate } from "../../api"; +export const TemplateReports: React.FC = () => { + const [dataLoad, setDataLoad] = useState(false); + const [loading, setLoading] = useState(false); + const [data, setFilteredData] = useState([]); + const [isDeleteActive, setIsDeleteActive] = useState(false); + const [deletedReportID, setdeletedReportID] = useState(""); + const [deletedReportName, setdeletedReportName] = useState(""); + const [popUp, setPopUp] = useState(false); + + const location = useLocation(); + useEffect(() => { + const getReportsTemplatesList = async () => { + if (location.state && location.state.popUp) { + setPopUp(true); + // Clear the popUp state after handling it + navigate(location.pathname, { replace: true, state: {} }); + setTimeout(() => { + setPopUp(false); + }, 3000); + } + setLoading(true); + try { + let templateList = await getSummarizationTemplates(); + console.log(templateList); + if (!Array.isArray(templateList)) { + templateList = []; + } + setFilteredData(templateList); + } catch (error) { + console.error("Error fetching user list:", error); + setFilteredData([]); + } finally { + setLoading(false); + } + }; + getReportsTemplatesList(); + }, [dataLoad]); + + const handleDeleteButton = (id: string, templateType: string, companyName: string) => { + setIsDeleteActive(true); + setdeletedReportID(id); + setdeletedReportName(templateType + " - " + companyName); + } + + const handleCancelDelete = () => { + setIsDeleteActive(false); + } + + const handleConfirmDelete = async() => { + setIsDeleteActive(false); + try { + await deleteSummarizationReportTemplate(deletedReportID); + setDataLoad(!dataLoad); + } catch (error) { + console.error("Error trying to delete the report: ", error); + } finally { + setLoading(false); + } + } + + const navigate = useNavigate(); + return ( +
    +
    + +
    +
    +

    Summarization Report Templates

    +
    +
    +
    + +
    + {loading ? ( + + ) : ( + + + + + + + + + + + + + {data.length > 0 ? (data.map((report: any, index: number) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
    Summarization ReportCompany NameCreated AtDescriptionStatusActions
    {report.companyTicker} {report.templateType}{report.companyName}{new Date(report.createdAt).toLocaleString()}{report.description} +
    +
    + {report.status} +
    +
    +
    +
    + +
    +
    + +
    + )} +
    + {isDeleteActive && ( +
    + + +
    + + +
    +
    + )} +
    +
    + {popUp && ( +
    + +
    + )} +
    + ); +}; From a8049c6f1f1f35449f070eb3250603f021364b86 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 13 Jan 2025 08:56:32 -0400 Subject: [PATCH 293/820] FA-344 Create the subscription management page UI (#249) * FA-344 Create the Subscription Management Page UI * FA-344 Deleting the console.log from the code --- frontend/src/App.tsx | 6 +- frontend/src/components/Sidebar/Sidebar.tsx | 7 - .../src/pages/organization/Organization.tsx | 3 +- .../SubscriptionManagement.module.css | 334 +++++++++++++++++- .../SubscriptionManagement.tsx | 280 ++++++++++++++- frontend/src/providers/AppProviders.tsx | 6 +- 6 files changed, 617 insertions(+), 19 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b2b4bc45..9bd0cdc8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,7 +8,6 @@ import Admin from "./pages/admin/Admin"; import Onboarding from "./pages/onboarding/Onboarding"; import Invitations from "./pages/invitations/Invitations"; import Organization from "./pages/organization/Organization"; -import FinancialAssistant from "./pages/financialassistant/FinancialAssistant"; import HelpCenter from "./pages/helpcenter/HelpCenter"; import UploadResources from "./pages/resources/UploadResources"; import RequestStudies from "./pages/studies/RequestStudies"; @@ -80,9 +79,8 @@ export default function App() { } /> } /> } /> - } /> - } /> - } /> + }/> + }/> diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 8dd69dfc..ab51ff7d 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -112,13 +112,6 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { to: "/organization", tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], roles: ["admin", "platformAdmin"] - }, - { - title: "Financial Assistant", - icon: , - to: "/financialassistant", - tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "platformAdmin"] } ] }, diff --git a/frontend/src/pages/organization/Organization.tsx b/frontend/src/pages/organization/Organization.tsx index 03821623..f7948c4d 100644 --- a/frontend/src/pages/organization/Organization.tsx +++ b/frontend/src/pages/organization/Organization.tsx @@ -7,6 +7,7 @@ import styles from "./Organization.module.css"; const Organization = () => { const { organization } = useAppContext(); + const expirationDate = new Date((organization?.subscriptionExpirationDate || 0) * 1000).toLocaleDateString(); if (!organization) { return ( @@ -53,7 +54,7 @@ const Organization = () => {
    - {organization?.subscriptionExpirationDate} + {expirationDate}
    diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css index 97f3e4ec..b92de5f5 100644 --- a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css @@ -2,5 +2,335 @@ display: flex; flex-direction: column; width: 100%; - padding: 100px 100px 100px 150px; -} \ No newline at end of file + padding: 40px 100px 100px 150px; + gap: 30px; +} + +@media (max-width: 900px) { + .page_container{ + padding: 100px 10px 100px 50px; + } +} + +.row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + justify-items: center; + border-bottom: 2px solid #d9d9d9; + margin-bottom: 10px; +} + +.title { + margin-bottom: 20px; + margin-left: 20px; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); + padding: 20px 20px 30px 20px; + gap: 20px; + margin-top: 15px; + flex-grow: 2; +} + +.table { + margin-top: 2em; + text-align: left; + background-color: white; + border-collapse: collapse; + width: 100%; +} + +.tableText{ + text-align: justify; +} + +@media (max-width: 900px){ + .table{ + font-size: 0.7em; + } +} + +@media (max-width: 768px) { + .title{ + margin-bottom: 10px; + margin-left: 0px; + } + + .center { + margin-left: 20px; + } + + .info{ + overflow-x: auto; + } +} + +.thead{ + background-color: #a7c444; + color: white; +} + +.group{ + margin-top: 2em; + display: flex; + align-items: flex-start; + gap: 1rem; +} + +.tableName{ + padding: 10px; +} + +.financialToggleText{ + font-size: 0.9rem; + font-weight: 410; + color: #000000; + white-space: nowrap; +} + +.financialToggle[type="checkbox"]{ + padding-top: 13%; + border-color: #85a717; +} + +.financialToggle[type="checkbox"]:checked { + background-color: #85a717; + border-color: #85a717; +} + +.financialToggle[type="checkbox"]:focus { + outline: none; + box-shadow: 0 0 0 0.2rem #aad12c; +} + +.messageBarText { + font-size: 16px; + flex-grow: auto; +} + +.button{ + display: flex; + padding: 5px 10px; + border: 2px solid #ffffff; + border-radius: 6px; + height: auto; + width: auto; + align-items: center; + justify-content: center; + background-color: #9fc51d; + color: white; + transition: background-color 0.3s; +} + + +.button:hover{ + background-color: #788a10; + border: 2px solid #88ae0a; +} + +.button:active{ + background-color: rgba(139, 195, 74, 0.5); +} + +.modal{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30em; + height: 15em; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 30px; +} + +.modalSubscription{ + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: auto; + height: auto; + display: flex; + flex-direction: flex; + justify-content: center; + align-items: center; + z-index: 500; + padding: 100px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 6px; + box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); + gap: 40px; +} + +@media (max-width: 1024px) { + .modal{ + width: 20em; + } + .modalSubscription{ + width: 20em; + gap: 20px; + flex-direction: column; + padding: 50px; + } +} + +.modalTitle{ + text-align: center; +} + +.modalText{ + text-align: justify; + font-weight: 400; +} + +.buttonContainer{ + text-align: left; + border-collapse: collapse; + width: 100%; + justify-content: center; + display: flex; + gap: 40px; +} + +.closeButton { + border: none; + font-size: 20px; + color: #726c6c; + background-color: transparent; + cursor: pointer; + position: absolute; + top: 0; + right: 0; + transform: translate(-10%, 10%); + margin-bottom: 10px; + padding-top: 6px; +} + +.closeButtonContainer:active{ + background-color: rgba(214, 214, 214, 0.315); + border-radius: 5px; +} + +.plan { + border: 2px solid #7fb600; + border-radius: 8px; + padding: 20px; + width: 300px; + height: 300px; + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); + background: white; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + align-items: center; +} + +.activePlan{ + border: 2px solid #b7ff0f; + border-radius: 8px; + padding: 20px; + width: 300px; + height: 300px; + box-shadow: 0px 8px 25px rgba(150, 230, 3, 0.14), 0px 0px 25px rgba(150, 230, 3, 0.14); + background: white; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + align-items: center; +} + +@media (max-width: 1024px) { + .plan { + width: 200px; + height: 150px; + padding: 10px; + } + .activePlan{ + width: 200px; + height: 150px; + padding: 10px; + } +} + +.planName { + margin: 0; + font-size: 1.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 600; + color: #333; +} + +.planPrice { + font-size: 1em; + color: #666; + margin: 20px 0; +} + + +.planButton { + background-color: #9fc51d; + color: white; + border: 1px solid #9fc51d; + padding: 12px 25px; + border-radius: 6px; + cursor: pointer; + font-size: 1em; + transition: background-color 0.3s; +} + +.planIcon { + margin: 10px 0; +} + + +@media (max-width: 1024px) { + .planPrice{ + margin: 5px 0; + } + .planIcon{ + width: 0; + height: 0; + } + .planName { + font-size: 1em; + } + + .planPrice{ + font-size: 0.7em; + } + .planButton{ + font-size: 0.6em; + height: 40px; + } +} + +.planButton:hover { + background-color: #788a10; +} + +.planButton:focus { + background-color: #9fc51d; +} diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx index c41d9161..8b27d5cf 100644 --- a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx @@ -1,12 +1,286 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import styles from "./SubscriptionManagement.module.css" +import { DefaultButton, Label, MessageBar, MessageBarType, PrimaryButton, Spinner } from "@fluentui/react"; +import { useAppContext } from "../../providers/AppProviders"; +import { getFinancialAssistant, getProductPrices, removeFinancialAssistant, upgradeSubscription } from "../../api"; +import { IconX } from "@tabler/icons-react"; +import { ChartPerson48Regular } from "@fluentui/react-icons"; const SubscriptionManagement: React.FC = () => { + const { user, organization, subscriptionTiers } = useAppContext(); + const [subscriptionStatus, setSubscriptionStatus] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [isErrorModal, setIsErrorModal] = useState(false) + const [isSubscriptionModal, setIsSubscriptionModal] = useState(false) + const [isUnsubscriptionModal, setIsUnsubscriptionModal] = useState(false) + const [isViewModal, setIsViewModal] = useState(false) + const subscriptionName = subscriptionTiers[0] || ''; + const [prices, setPrices] = useState([]); + const [isConfirmationModal, setIsConfirmationModal] = useState(false) + const [selectedSubscriptionName, setSelectedSubscriptionName] = useState('') + const [selectedSubscriptionID, setSelectedSubscriptionID] = useState('') + + const expirationDate = new Date((organization?.subscriptionExpirationDate || 0) * 1000).toLocaleDateString(); + + + + useEffect(() => { + const fetchStatus = async () => { + setLoading(true); + try { + if (!user?.organizationId) { + throw new Error("Organization ID is required"); + } + const { financial_assistant_active } = await getFinancialAssistant({ + user: { + ...user, + organizationId: user.organizationId + }, + subscriptionId: organization?.subscriptionId ?? "default-org-id" + }); + setSubscriptionStatus(financial_assistant_active); + } catch (error: any) { + console.log(error); + if (error.status === false) { + setSubscriptionStatus(false); + setError("Financial Assistant feature is not present in this subscription."); + setIsErrorModal(true) + } else if (error.status === null) { + setError("Bad request: unable to retrieve subscription status."); + setIsErrorModal(true) + } else { + setError("An error occurred while fetching subscription status."); + setIsErrorModal(true) + } + } finally { + setLoading(false); + } + }; + + fetchStatus(); + }, [user, organization]); + + useEffect(() => { + + async function fetchPrices() { + try { + const data = await getProductPrices({ user }); + setPrices(data.prices); + } catch (err) { + console.error("Failed to fetch product prices:", err); + setError("Unable to fetch product prices. Please try again later."); + setIsErrorModal(true) + } + } + + fetchPrices(); + }, [user]); + + + const handleSubscribe = async () => { + try { + setLoading(true); + const userObj = user ? { id: user.id, name: user.name, organizationId: user.organizationId ?? "default-org-id" } : undefined; + await upgradeSubscription({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); + setSubscriptionStatus(true); + setIsSubscriptionModal(false); + //This reloads the page so the financial assistant toggle appears after click + window.location.reload(); + } catch { + setError("An error occurred while subscribing to the Financial Assistant feature."); + setIsSubscriptionModal(false); + setIsErrorModal(true) + } finally { + setLoading(false); + } + }; + + const handleUnsubscribe = async () => { + try { + setLoading(true); + const userObj = user ? { id: user.id, name: user.name, organizationId: user.organizationId ?? "default-org-id" } : undefined; + await removeFinancialAssistant({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); + setSubscriptionStatus(false); + setIsUnsubscriptionModal(false); + //This reloads the page so the financial assistant toggle disappears after click + window.location.reload(); + } catch { + setError("An error occurred while unsubscribing from the Financial Assistant feature."); + setIsUnsubscriptionModal(false); + setIsErrorModal(true) + } finally { + setLoading(false); + } + }; + + const handleFinancialAssistantToggle = async () => { + if(subscriptionStatus == true){ + setIsUnsubscriptionModal(true) + }else{ + setIsSubscriptionModal(true) + } + } + + const handleViewSubscription = () =>{ + setIsViewModal(true) + } + + const handleSelectedSubscription = (priceNickname: string, priceID: string) =>{ + setSelectedSubscriptionName(priceNickname) + setSelectedSubscriptionID(priceID) + setIsConfirmationModal(true) + } + + const handleCheckout = async (priceId: string) => { + + } + + const FinancialAssistantText = subscriptionStatus ? "You are subscribed to the Financial Assistant feature." : + "Subscribe to Financial Assistant" + + return (
    -

    Subscription Management

    -

    Welcome to the Subscription Management page!

    +
    +

    Subscription Management

    +
    +
    + {loading ? ( + + ) : ( + + + + + + + + + + + + + + + +
    Subscription NameExpiration DateActions
    + {subscriptionName} + + {expirationDate} + +
    + +
    +
    + )} +
    + + {FinancialAssistantText} + +
    + + Financial Assistant +
    +
    + {isViewModal && ( +
    + + {prices.map((price, index) => ( +
    + +

    {price.nickname}

    +

    {price.description} +

    + +

    + ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval} +

    + +
    + ))} +
    + )} + {isConfirmationModal && ( +
    + + {selectedSubscriptionName === subscriptionName ? ( +
    + + +
    + setIsConfirmationModal(false)} text="Cancel" /> + handleCheckout(selectedSubscriptionID)} text="Confirm change" /> +
    +
    + ) : ( +
    + + +
    + setIsConfirmationModal(false)} text="Cancel" /> + handleCheckout(selectedSubscriptionID)} text="Confirm Subscription" /> +
    +
    + )} + +
    + )} + {isSubscriptionModal && ( +
    + + + +
    + setSubscriptionStatus(false)} text="Cancel" /> + +
    +
    + )} + {isUnsubscriptionModal && ( +
    + + + +
    + setIsUnsubscriptionModal(false)} text="Cancel" /> + +
    +
    + )} + {isErrorModal && ( +
    + + + +
    + )} +
    +
    + ); }; diff --git a/frontend/src/providers/AppProviders.tsx b/frontend/src/providers/AppProviders.tsx index f30ee3dd..795b13fd 100644 --- a/frontend/src/providers/AppProviders.tsx +++ b/frontend/src/providers/AppProviders.tsx @@ -217,9 +217,11 @@ export const AppProvider: React.FC<{ children: ReactNode }> = ({ children }) => name: organization.name, owner: organization.owner, subscriptionId: organization.subscriptionId, - subscriptionTier: organization.subscriptionTier as SubscriptionTier // Type assertion + subscriptionTier: organization.subscriptionTier as SubscriptionTier, // Type assertion + subscriptionExpirationDate: organization.subscriptionExpirationDate, + subscriptionStatus: organization.subscriptionStatus }); - + if (organization.subscriptionId) { await fetchSubscriptionTiers(organization.subscriptionId, userId); } From fc9147e471ffaf1ea87facac4cd06f37f3e47449 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Mon, 13 Jan 2025 09:02:09 -0400 Subject: [PATCH 294/820] =?UTF-8?q?FA-339=20Add=20financial=20assistant=20?= =?UTF-8?q?option=20to=20subscription=20plans=20and=20update=20check?= =?UTF-8?q?=E2=80=A6=20(#251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add financial assistant option to subscription plans and update checkout session * Remove financial assistant option from checkout session and related UI elements --- backend/app.py | 4 +- .../PaymentGateway/PaymentGateway.tsx | 56 ++++++++++--------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/backend/app.py b/backend/app.py index edf38402..aaa33d21 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1115,9 +1115,7 @@ def create_checkout_session(): organizationId = request.json["organizationId"] try: checkout_session = stripe.checkout.Session.create( - line_items=[ - {"price": price, "quantity": 1}, - ], + line_items=[{"price": price, "quantity": 1}], mode="subscription", client_reference_id=userId, metadata={"userId": userId, "organizationId": organizationId}, diff --git a/frontend/src/components/PaymentGateway/PaymentGateway.tsx b/frontend/src/components/PaymentGateway/PaymentGateway.tsx index 25dfd4cd..3984b735 100644 --- a/frontend/src/components/PaymentGateway/PaymentGateway.tsx +++ b/frontend/src/components/PaymentGateway/PaymentGateway.tsx @@ -23,7 +23,7 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise async function fetchPrices() { try { const data = await getProductPrices({ user }); - setPrices(data.prices); + setPrices(data.prices.sort((a: any, b: any) => a.unit_amount - b.unit_amount)); } catch (err) { console.error("Failed to fetch product prices:", err); setError("Unable to fetch product prices. Please try again later."); @@ -62,32 +62,34 @@ export const SubscriptionPlans: React.FC<{ stripePromise: Promise

    Subscription Plans

    - {prices.map((price, index) => ( - <> -
    - -

    {price.nickname}

    -

    - ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval} -

    -

    {price.description}

    - {price.id !== "free_plan" && ( - - )} -
    - - ))} + {prices.map((price, index) => { + return ( + <> +
    + +

    {price.nickname}

    +

    + ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval} +

    +

    {price.description}

    + {price.id !== "free_plan" && ( + + )} +
    + + ); + })}
    ); From 9cc32fb512842286a8f2387ab42de4a5d5469385 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Mon, 13 Jan 2025 08:07:23 -0500 Subject: [PATCH 295/820] FA-329 manage and download organization reports (#254) --- frontend/src/App.tsx | 6 +- frontend/src/api/api.ts | 38 +++ frontend/src/components/Sidebar/Sidebar.tsx | 11 +- frontend/src/pages/reports/Reports.tsx | 251 ++++++++++++++++++++ 4 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/reports/Reports.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9bd0cdc8..b1d85ee9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import HelpCenter from "./pages/helpcenter/HelpCenter"; import UploadResources from "./pages/resources/UploadResources"; import RequestStudies from "./pages/studies/RequestStudies"; import ReportManagement from "./pages/reports/ReportManagement"; +import Reports from "./pages/reports/Reports"; import DistributionLists from "./pages/reports/DistributionLists"; import Logout from "./pages/logout/Logout"; import Notifications from "./pages/notifications/Notifications"; @@ -103,12 +104,13 @@ export default function App() { } > }> + } /> } /> } /> @@ -117,7 +119,7 @@ export default function App() { } diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 43dac0bf..0c464e1b 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -599,6 +599,44 @@ export async function createReport(reportData: object) { return newReport; } +export async function getReportBlobs({ + container_name, + prefix, + include_metadata, + max_results, +}: { + container_name: string; + prefix: string; + include_metadata: string; + max_results: string; +}) { + const params = new URLSearchParams({ + container_name, + prefix, + include_metadata, + max_results, + }); + + try { + const response = await fetch(`/api/reports/storage/files?${params}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.status > 299 || !response.ok) { + throw Error("Error getting report blobs"); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Error getting report blobs", error); + return { data: [] }; + } +} + //This function, if sent with the "type" parameter, receives a request with the required report. If nothing is sent, it will receive all the reports from the container. export async function getFilteredReports(type?: string) { const url = type diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index ab51ff7d..e1c9cb82 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -172,19 +172,26 @@ const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { //It is only visible to platform administrators section: "Reports", items: [ + { + title: "Reports", + icon: , + to: "/view-reports", + tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], + roles: ["admin", "platformAdmin"] + }, { title: "Report Management", icon: , to: "/view-manage-reports", tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["platformAdmin"] + roles: ["admin", "platformAdmin"] }, { title: "Distribution Lists", icon: , to: "/details-settings", tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["platformAdmin"] + roles: ["admin", "platformAdmin"] } ] }, diff --git a/frontend/src/pages/reports/Reports.tsx b/frontend/src/pages/reports/Reports.tsx new file mode 100644 index 00000000..aeab0e56 --- /dev/null +++ b/frontend/src/pages/reports/Reports.tsx @@ -0,0 +1,251 @@ +import React, { useEffect, useState } from "react"; +import { + SearchBox, + DetailsList, + IColumn, + SelectionMode, + Text, + Stack, + IStackTokens, + DefaultButton, + mergeStyles, + mergeStyleSets, + FontWeights, + IDetailsListStyles +} from "@fluentui/react"; +import { getReportBlobs } from "../../api"; +import { useNavigate } from "react-router-dom"; + +import { ArrowDownload20Regular } from "@fluentui/react-icons"; + +interface IReport { + id: string; + title: string; + creationDate: string; + type: string; + downloadUrl: string; +} + +const stackTokens: IStackTokens = { + childrenGap: 20, + padding: 20 +}; + +const styles = mergeStyleSets({ + container: { + maxWidth: "100%", + backgroundColor: "#F8F8FC", + minHeight: "100vh", + + display: "flex", + flexDirection: "column", + width: "100%", + padding: "40px 100px 100px 150px", + gap: "30px" + }, + header: { + marginBottom: "8px", + display: "flex", + flexDirection: "column" + }, + title: { + fontSize: "24px", + fontWeight: FontWeights.semibold, + color: "#333" + }, + subtitle: { + fontSize: "14px", + color: "#666", + marginTop: "4px" + }, + searchContainer: { + display: "flex", + gap: "10px", + marginBottom: "20px" + }, + searchBox: { + width: "100%", + maxWidth: "800px" + }, + statusBadge: { + padding: "4px 12px", + borderRadius: "16px", + fontSize: "12px", + fontWeight: FontWeights.semibold, + display: "inline-block" + }, + complete: { + backgroundColor: "#E7F7EE", + color: "#1B873B" + }, + inProcess: { + backgroundColor: "#FFF7D1", + color: "#D97706" + }, + pending: { + backgroundColor: "#FFE2E5", + color: "#E11D48" + }, + downloadIcon: { + color: "#666", + cursor: "pointer", + ":hover": { + color: "#333" + } + }, + listContainer: { + backgroundColor: "white", + borderRadius: "8px", + padding: "20px", + boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.1)" + } +}); + +const detailsListStyles: Partial = { + root: { + selectors: { + ".ms-DetailsHeader": { + paddingTop: "12px" + }, + ".ms-DetailsRow": { + borderBottom: "1px solid #f0f0f0" + } + } + } +}; + +export default function Reports() { + const [reports, setReports] = useState([]); + const [allReports, setAllReports] = useState([]); + const [searchText, setSearchText] = useState(""); + const [loading, setLoading] = useState(true); + + const columns: IColumn[] = [ + { + key: "id", + name: "Report ID", + fieldName: "id", + minWidth: 70, + maxWidth: 90 + }, + { + key: "title", + name: "Title", + fieldName: "title", + minWidth: 250, + maxWidth: 300 + }, + { + key: "type", + name: "Type", + fieldName: "type", + minWidth: 100, + maxWidth: 120 + }, + { + key: "creationDate", + name: "Creation Date", + fieldName: "creationDate", + minWidth: 200, + maxWidth: 240, + onRender: (item: IReport) => {item.creationDate ? new Date(item.creationDate).toDateString() : null} + }, + { + key: "download", + name: "Download", + fieldName: "download", + minWidth: 70, + maxWidth: 70, + onRender: (item: IReport) => + item.downloadUrl ? ( + + + + ) : null + } + ]; + + useEffect(() => { + setLoading(true); + const getData = async () => { + const reports = await getReportBlobs({ + container_name: "documents", + prefix: "Reports/Curation_Reports", + include_metadata: "yes", + max_results: "10" + }); + + const cleanedReports = reports.data.map((report: any, index: number) => { + const name = report.name.split("/"); + return { + id: index + 1, + title: name[name.length - 1].split(".")[0], + type: name.length > 2 ? name[2] : "", + creationDate: report.created_on, + downloadUrl: report.url + }; + }); + + setReports(cleanedReports); + setAllReports(cleanedReports); + setLoading(false); + }; + getData(); + }, []); + + const handleSearch = (searchText: string) => { + if (searchText) { + const filteredReports = allReports.filter(report => report.title.toLowerCase().includes(searchText.toLowerCase())); + setReports(filteredReports); + } else { + setReports(allReports); + } + } + + return ( +
    + +
    + Report Management +
    + Accessing stored reports +
    + +
    + setSearchText(newValue || "")} + onSearch={() => handleSearch(searchText)} + /> + handleSearch(searchText)} + /> +
    + +
    + + Stored Reports + + {loading ? ( + Loading... + ) : ( + + )} +
    +
    +
    + ); +} From 8783cb9591cf9b8cae89333dbe2c5ee461f91429 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Mon, 13 Jan 2025 09:12:42 -0400 Subject: [PATCH 296/820] FA-338 Change or cancel webapp subscription (#253) --- backend/app.py | 68 ++++++++++++++++++++++++++++++++++++++++- frontend/src/api/api.ts | 59 ++++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/backend/app.py b/backend/app.py index aaa33d21..e7c6faad 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1960,7 +1960,6 @@ def get_product_prices(product_id): logging.error(f"Error fetching prices: {e}") raise - @app.route("/api/prices", methods=["GET"]) def get_product_prices_endpoint(): product_id = request.args.get("product_id", PRODUCT_ID_DEFAULT) @@ -2394,7 +2393,74 @@ def determine_subscription_tiers(subscription): return tiers +@app.route('/api/subscriptions//change', methods=['PUT']) +def change_subscription(subscription_id): + try: + + data = request.json + new_plan_id = data.get('new_plan_id') + if not new_plan_id: + return jsonify({'error': 'new_plan_id is required'}), 400 + + # Retrieve subscription from Stripe + stripe_subscription = stripe.Subscription.retrieve(subscription_id) + if not stripe_subscription or stripe_subscription['status'] == 'canceled': + return jsonify({'error': 'Subscription not found or is already canceled'}), 404 + # Update the plan, which is reflected and charged when changing it + updated_subscription = stripe.Subscription.modify( + subscription_id, + items=[{ + 'id': stripe_subscription['items']['data'][0]['id'], + 'price': new_plan_id, + }], + proration_behavior='none', # No proration + billing_cycle_anchor='now', # Change the billing cycle so that it is charged at that moment + cancel_at_period_end=False # Do not cancel the subscription + ) + + result = { + 'message': 'Subscription change successfully', + 'subscription': updated_subscription + } + + return jsonify(result), 200 + + except stripe.error.InvalidRequestError as e: + return jsonify({'error': f'Invalid request: {str(e)}'}), 400 + except stripe.error.AuthenticationError: + return jsonify({'error': 'Authentication with Stripe API failed'}), 403 + except stripe.error.PermissionError: + return jsonify({'error': 'Permission error when accessing the Stripe API'}), 403 + except stripe.error.RateLimitError: + return jsonify({'error': 'Too many requests to Stripe API, please try again later'}), 429 + except stripe.error.StripeError as e: + return jsonify({'error': f'Stripe API error: {str(e)}'}), 500 + + except Exception as e: + return jsonify({'error': 'Internal server error', 'details': str(e)}), 500 + + +@app.route('/api/subscriptions//cancel', methods=['DELETE']) +def cancel_subscription(subscription_id): + try: + + subscription = stripe.Subscription.retrieve(subscription_id) + + if not subscription: + return jsonify({'message': 'Subscription not found'}), 404 + + canceled_subscription = stripe.Subscription.delete(subscription_id) + + return jsonify({'message': 'Subscription canceled successfully'}), 200 + + except stripe.error.InvalidRequestError as e: + return jsonify({'message': 'Invalid subscription ID'}), 404 + except stripe.error.AuthenticationError as e: + return jsonify({'message': 'Unauthorized access'}), 403 + except Exception as e: + return jsonify({'error': 'Internal server error', 'details': str(e)}), 500 + ################################################ # Financial Doc Ingestion ################################################ diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 0c464e1b..64861371 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -736,4 +736,61 @@ export async function updateUser({ userId, updatedData }: { userId: string; upda if (response.status > 299 || !response.ok) { throw Error(`Error updating user with ID ${userId}`); } -} \ No newline at end of file +} + +export async function changeSubscription({ subscriptionId, newPlanId}: {subscriptionId: string;newPlanId: string;}): Promise { + + try { + const response = await fetch(`/api/subscriptions/${subscriptionId}/change`, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + new_plan_id: newPlanId, + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Subscription change failed: ${response.status} ${response.statusText} - ${errorText}`); + } + + const result: { message: string; subscription: any; } = await response.json(); + + console.log("Subscription changed successfully:", result.message); + return result.subscription; + } catch (error) { + console.error( + "Error changing subscription:", + error instanceof Error ? error.message : error + ); + throw error; + } +} + +export async function cancelSubscription({ subscriptionId }: {subscriptionId: string;}): Promise { + + try { + const response = await fetch(`/api/subscriptions/${subscriptionId}/cancel`, { + method: "DELETE", + headers: { + "Content-Type": "application/json" + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Subscription cancellation failed: ${response.status} ${response.statusText} - ${errorText}`); + } + + console.log("Subscription canceled successfully"); + } catch (error) { + console.error( + "Error canceling subscription:", + error instanceof Error ? error.message : error + ); + throw error; + } +} + From 940418cab5c3d399751f76c71245caf27b0f132c Mon Sep 17 00:00:00 2001 From: Victor Maldonado <67714035+vmlcode@users.noreply.github.com> Date: Tue, 14 Jan 2025 08:21:19 -0400 Subject: [PATCH 297/820] Hotfix fa 311 implement UI for managing templates summarization reports (#262) * FIX: addSummarizationReport to update required fields and improve error handling * REMOVE: remove console log from getSummarizationTemplates call * FIX: update allowed filing types in addSummarizationReport for validation --- backend/app.py | 31 +++++++------------ .../src/pages/reports/ReportTemplates.tsx | 1 - 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/backend/app.py b/backend/app.py index e7c6faad..21bc8c09 100644 --- a/backend/app.py +++ b/backend/app.py @@ -955,28 +955,21 @@ def addSummarizationReport(*, context): JSON response with the created report template if successful. JSON error response with appropriate HTTP status code if an error occurs. """ - try: + try: data = request.get_json() if not data: - raise MissingJSONPayloadError("Missing JSON payload") - if not "name" in data: - raise MissingRequiredFieldError("name") + raise MissingJSONPayloadError('Missing JSON payload') + if not "templateType" in data: + raise MissingRequiredFieldError('templateType') if not "description" in data: - raise MissingRequiredFieldError("description") - if not "companyTickers" in data: - raise MissingRequiredFieldError("companyTickers") - valid_names = ["10-K", "10-Q", "8-K", "DEF 14A"] - if not data["name"] in valid_names: - raise InvalidParameterError( - "name", f"Must be one of: {', '.join(valid_names)}" - ) - new_template = { - "name": data["name"], - "companyTickers": data["companyTickers"], - "description": data["description"], - "status": "active", - "type": "summarization", - } + raise MissingRequiredFieldError('description') + if not "companyTicker" in data: + raise MissingRequiredFieldError('companyTicker') + if not "companyName" in data: + raise MissingRequiredFieldError('companyName') + if not data["templateType"] in ALLOWED_FILING_TYPES: + raise InvalidParameterError('templateType', f"Must be one of: {', '.join(ALLOWED_FILING_TYPES)}") + new_template = {'templateType': data['templateType'], 'description': data['description'], 'companyTicker': data['companyTicker'], 'companyName': data['companyName'], 'status': 'active', 'type': 'summarization'} # add to cosmosDB container result = create_template(new_template) return create_success_response(result) diff --git a/frontend/src/pages/reports/ReportTemplates.tsx b/frontend/src/pages/reports/ReportTemplates.tsx index 406109ea..5248d702 100644 --- a/frontend/src/pages/reports/ReportTemplates.tsx +++ b/frontend/src/pages/reports/ReportTemplates.tsx @@ -27,7 +27,6 @@ export const TemplateReports: React.FC = () => { setLoading(true); try { let templateList = await getSummarizationTemplates(); - console.log(templateList); if (!Array.isArray(templateList)) { templateList = []; } From 209ed53f5f3a5f5a4ae90ca8074fe9ed01a33d2b Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:26:47 -0500 Subject: [PATCH 298/820] Update rp2email function (#264) * modified rp2email.py to use web app url and orchestrator uri * remove recursive endpoint calling, and sending email --------- Co-authored-by: namtran6701 --- backend/rp2email.py | 169 ++++++++++++++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 54 deletions(-) diff --git a/backend/rp2email.py b/backend/rp2email.py index 461e323e..e81b0cd0 100644 --- a/backend/rp2email.py +++ b/backend/rp2email.py @@ -3,12 +3,17 @@ import markdown from pathlib import Path from typing import Literal, List, Dict, Optional, Any - +from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient from pydantic import BaseModel, Field, EmailStr - from report_email_templates.email_templates import EmailTemplateManager from llm_config import LLMManager from financial_doc_processor import BlobStorageManager +from utils import EmailService +from dotenv import load_dotenv +import requests + +load_dotenv() logging.basicConfig( level=logging.INFO, @@ -17,9 +22,23 @@ logger = logging.getLogger(__name__) TEMP_DIR = "blob_downloads" -EMAIL_ENDPOINT = '/api/reports/email' PDF_OUTPUT_NAME = "report.pdf" -AZURE_FUNCTION_URL = f"{os.getenv('AZURE_FUNCTION_URL')}{os.getenv('AZURE_FUNCTION_CODE')}" +HTML_TO_PDF_ENDPOINT = os.getenv('ORCHESTRATOR_URI') + "/api/html_to_pdf_converter" + +def get_secret(secretName): + keyVaultName = os.getenv("AZURE_KEY_VAULT_NAME") + KVUri = f"https://{keyVaultName}.vault.azure.net" + credential = DefaultAzureCredential() + client = SecretClient(vault_url=KVUri, credential=credential) + logging.info(f"[webbackend] retrieving {secretName} secret from {keyVaultName}.") + retrieved_secret = client.get_secret(secretName) + return retrieved_secret.value + + +# get function code from key vault for html 2 pdf +html2pdf_function_code = get_secret("orchestrator-host--html2pdf") + + #################################### # Pydantic Models @@ -102,7 +121,18 @@ def __init__(self, blob_link: str): Args: blob_link (str): Link to the report blob + + Raises: + ValueError: If blob_link is None or empty """ + if not blob_link: + raise ValueError("Blob link cannot be None or empty") + + # Validate URL format + from urllib.parse import urlparse + parsed_url = urlparse(blob_link) + if not all([parsed_url.scheme, parsed_url.netloc]): + raise ValueError(f"Invalid blob link format: {blob_link}. URL must include scheme (e.g., https://) and hostname") self.blob_link = blob_link self.blob_manager = BlobStorageManager() @@ -290,36 +320,51 @@ def _parse_report_to_email_schema(self, summary: str) -> EmailSchema: raise def html_to_pdf(self, html_content: str, output_path: str) -> Path: - """Convert the html content to a pdf file using the azure function""" - import requests + """Convert the HTML content to a PDF file using the Azure function.""" + import requests + import json + from pathlib import Path - # Format the payload exactly as shown in the working test - payload = { - "html": html_content + payload = json.dumps({"html": html_content}) + + key_secret_name = "orchestrator-host--html2pdf" + + try: + function_key = get_secret(key_secret_name) + except Exception as e: + logger.exception(f"Error getting the function key: {str(e)}") + raise RuntimeError(f"Failed to retrieve function key: {key_secret_name}") + + headers = { + "Content-Type": "application/json", + "x-functions-key": function_key } - - try: - # Make the request with timeout - logger.info(f"Calling the pdf converter") + + try: + logger.info("Attempting to connect to HTML to PDF converter") response = requests.post( - AZURE_FUNCTION_URL, - json=payload, # Use json parameter to match the raw JSON format - timeout=30 + HTML_TO_PDF_ENDPOINT, + headers=headers, + data=payload, + timeout=30 # Add timeout ) response.raise_for_status() - # Create directory if it doesn't exist output_dir = Path(output_path).parent output_dir.mkdir(parents=True, exist_ok=True) - # Save the PDF content to a file - logger.info(f"Saving the PDF to {output_path}") with open(output_path, 'wb') as f: f.write(response.content) - logger.info(f"PDF saved successfully") + logger.info(f"PDF saved successfully at {output_path}") return Path(output_path) + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error to HTML to PDF converter: {str(e)}") + raise RuntimeError( + f"Failed to connect to HTML to PDF converter at {HTML_TO_PDF_ENDPOINT}. " + "Please ensure the Azure Function is running and accessible." + ) except requests.exceptions.HTTPError as e: logger.error(f"HTTP error occurred: {e.response.text if e.response else str(e)}") raise @@ -360,7 +405,7 @@ def send_email( email_subject: Optional[str] = None, save_email: Optional[str] = "yes" ) -> bool: - """Send an email to the recipients. + """Send an email to the recipients Args: email_data: Dictionary containing email content @@ -390,37 +435,50 @@ def send_email( "save_email": save_email } - # overwrite the attachment path if provided if attachment_path: payload["attachment_path"] = attachment_path # if attachment path is 'no', set it to None if attachment_path.lower() == 'no': payload["attachment_path"] = None - + else: + payload["attachment_path"] = str(attachment_path) + if email_subject: payload["subject"] = email_subject - - # send email using the endpoint - with current_app.test_client() as client: - response = client.post(EMAIL_ENDPOINT, json=payload) - - if response.status_code == 200: - logger.info(f"Email sent successfully at {datetime.now(timezone.utc)}") - # log recipients - logger.info(f"Recipients: {recipients}") - return True - - error_data = response.get_json() - error_message = error_data.get('message', 'Unknown error') - logger.error(f"Failed to send email. Status code: {response.status_code}. Error: {error_message}") - raise EmailSendingError(error_message) # Raise the custom exception - - except EmailSendingError: - raise # Re-raise email-specific errors + + logger.info(f"Payload: {payload}") + + email_config = { + "smtp_server": os.getenv("EMAIL_HOST"), + "smtp_port": os.getenv("EMAIL_PORT"), + "username": os.getenv("EMAIL_USER"), + "password": os.getenv("EMAIL_PASS"), + } + + email_service = EmailService(**email_config) + + email_params = { + "subject": payload["subject"], + "html_content": payload["html_content"], + "recipients": payload["recipients"], + "attachment_path": payload.get("attachment_path"), + } + + # send the email + email_service.send_email(**email_params) + + logger.info(f"Email sent successfully at {datetime.now(timezone.utc)}") + logger.info(f"Recipients: {recipients}") + return True + except requests.exceptions.RequestException as e: + error_msg = f"Network error while sending email: {str(e)}" + logger.error(error_msg) + raise EmailSendingError(error_msg) except Exception as e: - logger.exception(f"Error sending email: {str(e)}") - raise EmailSendingError(f"Unexpected error while sending email: {str(e)}") + error_msg = f"Unexpected error while sending email: {str(e)}" + logger.error(error_msg) + raise EmailSendingError(error_msg) def process_and_send_email(blob_link: str, recipients: List[str], @@ -438,18 +496,21 @@ def process_and_send_email(blob_link: str, Returns: bool: True if the email is sent successfully, False otherwise. """ - processor = None - success = False - processor = ReportProcessor(blob_link) - - with processor._resource_cleanup(): - try: - # initialize and process report + try: + if not blob_link: + raise ValueError("Blob link cannot be None or empty") + + processor = ReportProcessor(blob_link) + with processor._resource_cleanup(): email_data = processor.process() - # send email success = send_email(email_data, recipients, attachment_path, email_subject, save_email) return success - except Exception as e: - logger.exception(f"Error processing and sending email: {str(e)}") - return False + except ValueError as e: + logger.error(f"Invalid input: {str(e)}") + return False + except Exception as e: + logger.exception(f"Error processing and sending email: {str(e)}") + return False + + From 86bd61a1fee9dc76bdc3247d74586250226b4818 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:55:52 -0500 Subject: [PATCH 299/820] added documentation for Nam's endpoints (#256) * added documentation for Nam's endpoints * Quick Fix: Update endpoint documentation for Curation Report --------- Co-authored-by: Victor Maldonado <67714035+vmlcode@users.noreply.github.com> --- docs/Nam Endpoint Documentation.md | 607 +++++++++++++++++++++++++++++ 1 file changed, 607 insertions(+) create mode 100644 docs/Nam Endpoint Documentation.md diff --git a/docs/Nam Endpoint Documentation.md b/docs/Nam Endpoint Documentation.md new file mode 100644 index 00000000..d78d64af --- /dev/null +++ b/docs/Nam Endpoint Documentation.md @@ -0,0 +1,607 @@ +# Nam Endpoint Documentation + + +# 1. Get financial data + +An API endpoint that processes and uploads financial documents from SEC EDGAR. This function handles the downloading, processing, and uploading of SEC filings for specified companies. + + +GET /api/SECEdgar/financialdocuments + + +Request Body JSON payload with the following parameters: +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| equity_id | string | Yes | Stock symbol/ticker (e.g., 'AAPL', 'MSFT') | +| filing_type | string | Yes | Type of SEC filing (Currently supports 10-K, 10-Q, 8-K, DEF 14A) | +| after_date | string | No | Optional date filter in YYYY-MM-DD format | + + +## Example Request + +```bash +{ + "equity_id": "AAPL", + "filing_type": "10-K", + "after_date": "2023-01-01" +} +``` + +## Response + +### Success Response +Returns a JSON object with the processing results: + +```bash +{ + "status": "success", + "code": 200, + "message": "Document processed successfully", + "results": { + "": { + "": { + "blob_path": "financial//.pdf", + "blob_url": "https:///documents/financial//.pdf", + "metadata": { + "equity_id": "", + "filing_type": "", + "source": "SEC EDGAR", + "uploaded_date": "YYYY-MM-DD" + }, + "status": "success" + } + } + } +} +``` + +### Error Response + + +**404 Not Found** + +No document found for the given equity_id and filing_type **after the given date** + +```bash +{ + "code": 404, + "message": "No 10-Q found after for ", + "status": "not_found" +} +``` + +**500 Internal Server Error** + +Returned when server-side processing fails + +```bash +{ + "status": "error", + "message": "", + "code": 500 +} +``` + +Dependencies +- Requires **wkhtmltopdf** to be installed on the system + +**400 Bad Request** +Returned when the request is invalid or missing required parameters + +```bash +{ + "code": 400, + "message": "", + "status": "bad_request" +} +``` + +# 2. Summarization + +Endpoint to generate a summary of financial documents + +POST /api/SECEdgar/financialdocuments/summary + +Request Body JSON payload with the following parameters: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| equity_name | string | Yes | Stock symbol/ticker (e.g., 'AAPL', 'MSFT') | +| financial_type | string | Yes | Type of SEC filing (Currently supports 10-K, 10-Q, 8-K, DEF 14A) | + + +### Example Request + +```bash +{ + "equity_name": "", + "financial_type": "" # 10-K, 10-Q, 8-K, DEF 14A +} +``` + +## Response + +### Success Response + + +```bash +{ + "status": "success", + "equity_name": "", + "financial_type": "", + "blob_path": "financial//_summary.pdf", + "remote_blob_url": "https:///documents/financial//_summary.pdf", + "summary": "" +} +``` + +### Error Response + +```bash +# 1. Request Validation Errors (400) +# Invalid JSON +return jsonify({ + 'error': 'Invalid request', + 'details': 'Request body is requred and must be a valid JSON object' +}), 400 + +# Missing fields +return jsonify({ + 'error': 'Missing required fields', + 'details': 'equity_name and financial_type are required' +}), 400 + +# Invalid types +return jsonify({ + 'error': 'Invalid input type', + 'details': 'equity_name and financial_type must be strings' +}), 400 + +# 2. Service Errors (503) +# Connection issues +return jsonify({ + 'error': 'Connection error', + 'details': 'Failed to connect to storage service' +}), 503 + +# 3. Internal Server Errors (500) +# Service initialization +return jsonify({ + 'error': 'Service initialization failed', + 'details': str(e) +}), 500 + +# Directory management +return jsonify({ + 'error': 'Cleanup failed', + 'details': 'Failed to clean up directories to prepare for processing' +}), 500 + +# Generic unexpected errors +return jsonify({ + 'error': 'Internal server error', + 'details': str(e) +}), 500 + +``` + +# 3. Process and Summarize Edgar Financial Documents +Endpoint to process and generate summaries for SEC Edgar financial documents in a single request. + +POST /api/SECEdgar/financialdocuments/process-and-summarize + +### Example Request + +```json +{ +"equity_id": "AAPL", // Stock symbol/ticker +"filing_type": "10-K", // SEC filing type +"after_date": "2023-01-01" // Optional, format: YYYY-MM-DD +} +``` + +### Required Fields +| Field | Type | Description | +|-------|------|-------------| +| equity_id | string | Stock symbol/ticker (e.g., 'AAPL') | +| filing_type | string | Type of SEC filing (must be one of FILING_TYPES) | + +### Optional Fields +| Field | Type | Description | +|-------|------|-------------| +| after_date | string | Filter for filings after this date (YYYY-MM-DD format) | + +## Processing Steps +1. Document Processing: Downloads and processes SEC Edgar document (it uses the financialdocuments endpoint) +2. Summary Generation: Creates a summary of the processed document (it uses the summary endpoint) + +## Responses + +### Success Response + +Returns a JSON object containing both the document processing and summary results: + +```json +{ + "status": "success", + "edgar_data_process": { + "code": 200, + "message": "Document processed successfully", + "status": "success", + "results": { + "": { + "": { + "blob_path": "financial//.pdf", + "blob_url": "https:///documents/financial//.pdf", + "metadata": { + "equity_id": "", + "filing_type": "", + "source": "SEC EDGAR", + "uploaded_date": "YYYY-MM-DD" + }, + "status": "success" + } + } + } + }, + "summary_process": { + "status": "success", + "equity_name": "", + "financial_type": "", + "blob_path": "financial//_summary.pdf", + "remote_blob_url": "https:///documents/financial//_summary.pdf", + "summary": "" + } +} +``` + +#### Response Fields + +##### Top Level +| Field | Type | Description | +|-------|------|-------------| +| status | string | Overall processing status | +| edgar_data_process | object | Results from document processing | +| summary_process | object | Results from summary generation | + +##### Edgar Data Process +| Field | Type | Description | +|-------|------|-------------| +| code | number | HTTP status code (200 for success) | +| message | string | Processing status message | +| status | string | Processing status | +| results | object | Document processing results by equity | + +##### Summary Process +| Field | Type | Description | +|-------|------|-------------| +| status | string | Summary generation status | +| equity_name | string | Stock symbol/ticker | +| financial_type | string | Type of SEC filing | +| blob_path | string | Path to summary file in storage | +| remote_blob_url | string | Full URL to access summary | +| summary | string | Generated text summary | + + + +### Error Responses + +#### 1. Bad Request (400) +Returned for request validation errors: + +```json +{ + "status": "error", + "error": "", + "details": "", + "timestamp": "" +} +``` + +Common 400 error scenarios: +```json +// Missing JSON body +{ + "status": "error", + "error": "Invalid request", + "details": "Request body is required and must be a valid JSON object", + "timestamp": "2024-01-10T12:00:00Z" +} + +// Missing required fields +{ + "status": "error", + "error": "Missing required fields", + "details": "Missing required fields: equity_id, filing_type", + "timestamp": "2024-01-10T12:00:00Z" +} + +// Invalid filing type +{ + "status": "error", + "error": "Invalid filing type", + "details": "Invalid filing type. Must be one of: 10-K, 10-Q, 8-K, DEF 14A", + "timestamp": "2024-01-10T12:00:00Z" +} + +// Invalid date format +{ + "status": "error", + "error": "Invalid date format", + "details": "Use YYYY-MM-DD", + "timestamp": "2024-01-10T12:00:00Z" +} +``` + +#### 2. Not Found (404) +Returned when the requested document cannot be found: + +```json +{ + "status": "not_found", + "error": "No document found for the specified criteria", + "code": 404, + "timestamp": "" +} +``` + +#### 3. Internal Server Error (500) +Returned for processing failures: + +```json +{ + "status": "error", + "error": "", + "details": "", + "timestamp": "" +} +``` + +Common 500 error scenarios: +```json +// Document processing failure +{ + "status": "error", + "error": "Document processing failed", + "details": "Failed to process SEC Edgar document", + "timestamp": "2024-01-10T12:00:00Z" +} + +// Summary generation failure +{ + "status": "error", + "error": "Summary generation failed", + "details": "Failed to generate document summary", + "timestamp": "2024-01-10T12:00:00Z" +} + +// Unexpected error +{ + "status": "error", + "error": "An unexpected error occurred while processing the document", + "details": "", + "timestamp": "2024-01-10T12:00:00Z" +} +``` + +# 4. Curation Report + +Endpoint to generate curated reports based on a specific topic + +POST /api/reports/generate/curation + +### Example Request + +```bash +{ + "report_topic": "" # Monthly_Economics, Weekly_Economics, Ecommerce +} +``` + +## Response + +### Success Response + +```json +{ + "status": "success", + "message": "Report generated for ", + "report_url": "https:///documents/Reports/Curation_Reports///Week_.html" +} +``` +#### Response Fields +| Field | Type | Description | +|-------|------|-------------| +| status | string | Processing status ("success") | +| message | string | Confirmation message including the report type | +| report_url | string | Full URL to access the generated report | + +#### URL Structure +The `report_url` follows this pattern: +- Weekly reports: `.../Reports/Curation_Reports///Week_.html` +- Monthly reports: `.../Reports/Curation_Reports//.html` + +### Error Response + +**Missing required fields** + +```json +{ + "error": "report_topic is required" +} +``` + +**Invalid report type** + +```json +{ +"error": "Invalid report type. Please choose from: []" +} +``` + +**Internal Server Error** + +```json +{ +"error": "An unexpected error occurred while generating the report" +} +``` + +# 5. Send Email Endpoint + +Endpoint to send HTML-formatted emails with optional attachments and storage capabilities. + +POST /api/reports/email + +### Example Request + +```json +{ + "subject": "Email subject", + "html_content": "HTML formatted content", + "recipients": ["email1@domain.com", "email2@domain.com"], + "attachment_path": "path/to/attachment.pdf", // Optional + "save_email": "yes" // Optional, default: "no" +} + +``` +### Required Fields +| Field | Type | Description | +|-------|------|-------------| +| subject | string | Email subject line | +| html_content | string | HTML-formatted email body | +| recipients | array | List of recipient email addresses | + +### Optional Fields +| Field | Type | Description | +|-------|------|-------------| +| attachment_path | string | Path to attachment file (use forward slashes) | +| save_email | string | Whether to save email to blob storage ("yes"/"no") | + +## Responses + +### Success Response + +```json + { + "status": "success", + "message": "Email sent successfully", + "blob_name": "" // Only if save_email="yes", if not then return null + } +``` + +### Error Response + +Returned for validation errors: + +```json +{ +"status": "error", +"message": "" +} +``` + + +Common 400 error messages: +- "No JSON data provided" +- "Missing required fields: subject, html_content, recipients" +- "Recipients must be provided as a list" +- "At least one recipient is required" +- "Attachment file not found: " + +Internal Server Error + +```json +{ +"status": "error", +"message": "An unexpected error occurred while processing the request" +} +``` + + +Common 500 error messages: +- "Email service configuration error" +- "Failed to send email:
    " +- "Email has been sent, but failed to upload to blob storage:
    " +- "An unexpected error occurred:
    " + +## Notes +- Attachments must be accessible from the server's file system +- Windows-style paths are automatically converted to proper format +- Email configuration is managed through environment variables +- Emails can optionally be saved to blob storage +- All operations are logged for debugging purposes + + +# 6. Process and Email Report Digest + +Endpoint to process a report from a blob storage and send it via email + +POST /api/reports/digest + +### Example Request +```json +{ +"blob_link": "https://storage.com/path/to/report", // Required: URL to the report +"recipients": ["email1@domain.com"], // Required: Array of email addresses +"attachment_path": "path/to/attachment.pdf", // Optional: Custom attachment path +"email_subject": "Custom Report Subject", // Optional: Email subject line +"save_email": "yes" // Optional: Save email to storage (default: "yes") +} +``` + + +### Required Fields +| Field | Type | Description | +|-------|------|-------------| +| blob_link | string | Full URL to the report in blob storage | +| recipients | array | List of recipient email addresses | + +### Optional Fields +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| attachment_path | string | blob document | Path to custom attachment (use forward slashes) | +| email_subject | string | auto-generated | Custom email subject line | +| save_email | string | "yes" | Whether to save email to blob storage ("yes"/"no") | + +**IMPORTANT** + +By default, the email will include a PDF version of the report from the provided blob link as an attachment. + + +## Responses + +### Success Response + +```json +{ +"status": "success", +"message": "Report processed and email sent successfully" +} +``` + +### Error Response + +```json +{ +"status": "error", +"message": "" +} +``` + + +Common 400 error messages: +- "No JSON data provided" +- "Missing required fields: blob_link and recipients" + +#### Internal Server Error (500) + +Common 500 error messages: +- "Failed to process report and send email" +- Specific error messages from processing/sending attempts + +## Notes +- By default, the report from the blob_link is attached to the email +- Use attachment_path="no" to disable automatic attachment +- Custom attachments must be accessible from the server +- Emails can be automatically saved to blob storage From 0443cfe42b257b33fb3c38787cbb833a2f8c0fdf Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Fri, 17 Jan 2025 19:15:25 -0400 Subject: [PATCH 300/820] Hotfix-FA-280 Minor fixes in Distribution Lists for the PlatformAdmin role and fixes in the CurationCreation page (#265) --- .../src/pages/reports/DistributionLists.module.css | 12 ++++++++++++ frontend/src/pages/reports/DistributionLists.tsx | 7 ++++++- .../ReportCreation/CurationCreation.module.css | 8 ++++++-- .../reports/ReportCreation/CurationCreation.tsx | 12 +++++++----- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/reports/DistributionLists.module.css b/frontend/src/pages/reports/DistributionLists.module.css index 950e61aa..9833f41d 100644 --- a/frontend/src/pages/reports/DistributionLists.module.css +++ b/frontend/src/pages/reports/DistributionLists.module.css @@ -108,6 +108,15 @@ border-radius: 15px; } +.rolePlatformAdmin{ + width: 100px; + font-size: 0.8em; + background-color: #d9ff98; + padding: 5px; + color: #1b4332; + border-radius: 15px; +} + @media (max-width: 900px){ .roleAdmin{ width: 50px; @@ -115,6 +124,9 @@ .roleUser{ width: 50px; } + .rolePlatformAdmin{ + width: 50px; + } } .checkbox{ diff --git a/frontend/src/pages/reports/DistributionLists.tsx b/frontend/src/pages/reports/DistributionLists.tsx index c29a0daa..a178d2da 100644 --- a/frontend/src/pages/reports/DistributionLists.tsx +++ b/frontend/src/pages/reports/DistributionLists.tsx @@ -19,6 +19,11 @@ const DistributionLists: React.FC = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [dataLoad, setDataLoad] = useState(false) + const roleStyles: { [key in 'admin' | 'user' | 'platformAdmin']: string } = { + admin: styles.roleAdmin, + user: styles.roleUser, + platformAdmin: styles.rolePlatformAdmin, + } useEffect(() => { const getUserList = async () => { @@ -111,7 +116,7 @@ const DistributionLists: React.FC = () => {
    -
    +
    {user.data.role}
    diff --git a/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css b/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css index 50e0527c..a5e28547 100644 --- a/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css +++ b/frontend/src/pages/reports/ReportCreation/CurationCreation.module.css @@ -154,9 +154,9 @@ .modalPopup{ position: fixed; - top: 10%; + top: 50%; left: 50%; - transform: translateX(-50%); + transform: translate(-50%, -50%); width: auto; height: auto; display: flex; @@ -213,4 +213,8 @@ .closeButtonContainer:active{ background-color: rgba(214, 214, 214, 0.315); border-radius: 5px; +} + +.fieldDisclaimer{ + color: red; } \ No newline at end of file diff --git a/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx b/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx index 31070516..1dfe643a 100644 --- a/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx +++ b/frontend/src/pages/reports/ReportCreation/CurationCreation.tsx @@ -60,11 +60,12 @@ const CurationCreation: React.FC = () => { setIsPopupActive(true) timer = setTimeout(() => { setIsPopupActive(false); + navigate('/curation-reports') }, 3000); - + } catch (error){ console.error("Error trying to create the report: ", error); - } + } } return ( @@ -81,10 +82,11 @@ const CurationCreation: React.FC = () => {
    - + - + + All fields are required (*)
    @@ -102,7 +104,7 @@ const CurationCreation: React.FC = () => { {isConfirm && (
    - +
    {loading ? ( - - ) : ( - - - - - - - - - - - - - + + +
    Subscription NameExpiration DateActions
    - {subscriptionName} - - {expirationDate} - + + ) : ( + + + + + + + + + + + + + - - -
    Subscription NameExpiration DateActions
    {subscriptionName}{expirationDate}
    - -
    -
    - )} + + +
    + )}
    - + {FinancialAssistantText}
    @@ -192,16 +330,87 @@ const SubscriptionManagement: React.FC = () => { Financial Assistant
    + {isRecentChangesModal && ( +
    +
    +

    Recent Changes

    + +
    +
    + + +
    + + + + + + + + + + + {dummyData.map((data, index) => ( + + {data.action === "Subscription Tier Change" && ( + <> + + + + + + )} + {data.action === "Financial Assistant Change" && ( + <> + + + + + + )} + + ))} + +
    DateActionModified byDetails
    + {new Date(data.changeTime) + .toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false + }) + .replaceAll(",", "")} + Subscription Tier change{data.modified_by_name} + {data.previous_plan} → {data.current_plan} + + {new Date(data.changeTime) + .toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: false + }) + .replaceAll(",", "")} + FA Add-On Toggled{data.modified_by_name}Status: {data.status_financial_assistant}
    +
    + )} {isViewModal && (
    - + {prices.map((price, index) => (

    {price.nickname}

    -

    {price.description} -

    - +

    {price.description}

    +

    ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval}

    @@ -218,17 +427,20 @@ const SubscriptionManagement: React.FC = () => { : "Subscribe"}
    - ))} + ))}
    )} {isConfirmationModal && ( -
    - +
    + {selectedSubscriptionName === subscriptionName ? (
    -
    )} -
    )} {isSubscriptionModal && (
    - +
    @@ -261,7 +474,9 @@ const SubscriptionManagement: React.FC = () => { )} {isUnsubscriptionModal && (
    - +
    @@ -272,15 +487,15 @@ const SubscriptionManagement: React.FC = () => { )} {isErrorModal && (
    - - - -
    + + + +
    )}
    -
    - ); }; From 643c25c08e2aebf7ccf277311ec5f6bfd7000d6b Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 20 Jan 2025 08:43:56 -0400 Subject: [PATCH 302/820] FA-345 Integrate subscription management page with backend API (#266) * FA-345 Integrate the change subscription plan and an emergent modal to give feedback to the user * FA-345 Minor changes * FA-345 Adding the subscription fee disclaimer --- frontend/src/App.tsx | 1 + .../SubscriptionManagement.tsx | 60 +++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b1d85ee9..2465e934 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,6 +33,7 @@ export default function App() { {/* Public Routes */} } /> } /> + } /> {/* Protected Routes for Authenticated Users (Regular and Admin) */} { - const { user, organization, subscriptionTiers } = useAppContext(); + const { user, organization, subscriptionTiers, setIsFinancialAssistantActive} = useAppContext(); const [subscriptionStatus, setSubscriptionStatus] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -15,11 +15,13 @@ const SubscriptionManagement: React.FC = () => { const [isSubscriptionModal, setIsSubscriptionModal] = useState(false) const [isUnsubscriptionModal, setIsUnsubscriptionModal] = useState(false) const [isViewModal, setIsViewModal] = useState(false) - const subscriptionName = subscriptionTiers[0] || ''; + const [subscriptionName, setSusbscriptionName] = useState('') const [prices, setPrices] = useState([]); const [isConfirmationModal, setIsConfirmationModal] = useState(false) const [selectedSubscriptionName, setSelectedSubscriptionName] = useState('') const [selectedSubscriptionID, setSelectedSubscriptionID] = useState('') + const [dataLoad, setDataLoad] = useState(false) + const [isSubscriptionChangeModal, setIsSubscriptionChangeModal] = useState(false) const expirationDate = new Date((organization?.subscriptionExpirationDate || 0) * 1000).toLocaleDateString(); @@ -29,6 +31,7 @@ const SubscriptionManagement: React.FC = () => { const fetchStatus = async () => { setLoading(true); try { + setSusbscriptionName(subscriptionTiers[0] || '') if (!user?.organizationId) { throw new Error("Organization ID is required"); } @@ -59,7 +62,7 @@ const SubscriptionManagement: React.FC = () => { }; fetchStatus(); - }, [user, organization]); + }, [dataLoad]); useEffect(() => { @@ -75,7 +78,7 @@ const SubscriptionManagement: React.FC = () => { } fetchPrices(); - }, [user]); + }, [dataLoad]); const handleSubscribe = async () => { @@ -103,6 +106,7 @@ const SubscriptionManagement: React.FC = () => { await removeFinancialAssistant({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); setSubscriptionStatus(false); setIsUnsubscriptionModal(false); + setIsFinancialAssistantActive(false); //This reloads the page so the financial assistant toggle disappears after click window.location.reload(); } catch { @@ -133,7 +137,31 @@ const SubscriptionManagement: React.FC = () => { } const handleCheckout = async (priceId: string) => { + setIsConfirmationModal(false); + setIsViewModal(false) + setLoading(true) + let timer: NodeJS.Timeout; + try{ + await changeSubscription({ + subscriptionId: organization?.subscriptionId ?? "", + newPlanId: priceId + }); + } catch(error){ + console.error("Error trying to change the subscription: ", error); + setError("Error trying to change the subscription: ") + } finally{ + setLoading(false) + setIsSubscriptionChangeModal(true); + setDataLoad(true) + timer = setTimeout(() => { + setIsSubscriptionChangeModal(false); + }, 5000); + //If we don't reload the page, the subscription tiers won't update correctly in the platform + window.location.reload(); + } + + } const FinancialAssistantText = subscriptionStatus ? "You are subscribed to the Financial Assistant feature." : @@ -238,7 +266,9 @@ const SubscriptionManagement: React.FC = () => { ) : (
    - +
    setIsConfirmationModal(false)} text="Cancel" /> handleCheckout(selectedSubscriptionID)} text="Confirm Subscription" /> @@ -250,11 +280,11 @@ const SubscriptionManagement: React.FC = () => { )} {isSubscriptionModal && (
    - +
    - setSubscriptionStatus(false)} text="Cancel" /> + setIsSubscriptionModal(false)} text="Cancel" />
    @@ -272,10 +302,16 @@ const SubscriptionManagement: React.FC = () => { )} {isErrorModal && (
    - - - -
    + + + +
    + )} + {isSubscriptionChangeModal && ( +
    + + +
    )}
    From 3dd4c9dc7c38f6eac81cb7f96bd863859b9aab33 Mon Sep 17 00:00:00 2001 From: Nam Tran <102604449+namtran6701@users.noreply.github.com> Date: Mon, 20 Jan 2025 07:45:17 -0500 Subject: [PATCH 303/820] use default azure credential and organize sent emails by date (#267) --- backend/utils.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/utils.py b/backend/utils.py index 23964d24..f7993c68 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -341,9 +341,8 @@ def _extract_response_data(response): from typing import List from email.message import EmailMessage import smtplib -from dotenv import load_dotenv -load_dotenv() +EMAIL_CONTAINER_NAME = 'emails' class EmailServiceError(Exception): """Base exception for email service errors""" pass @@ -433,24 +432,29 @@ def _save_email_to_blob(self, """ Save the email content to a blob storage container """ - EMAIL_CONTAINER_NAME = 'emails' from azure.storage.blob import BlobServiceClient from datetime import datetime, timezone from azure.storage.blob import ContentSettings - - blob_service_client = BlobServiceClient.from_connection_string(os.getenv('BLOB_CONNECTION_STRING')) - blob_container_client = blob_service_client.get_container_client(EMAIL_CONTAINER_NAME) - - # get the blob storage container + from azure.identity import DefaultAzureCredential from financial_doc_processor import BlobUploadError import uuid + credential = DefaultAzureCredential() + BLOB_STORAGE_URL = f"https://{os.getenv('STORAGE_ACCOUNT')}.blob.core.windows.net" + blob_service_client = BlobServiceClient( + account_url=BLOB_STORAGE_URL, + credential=credential + ) + blob_container_client = blob_service_client.get_container_client(EMAIL_CONTAINER_NAME) # create an id for the email email_id = str(uuid.uuid4()) - + timestamp = datetime.now(timezone.utc).isoformat() + # get date only from timestamp + date_only = timestamp.split('T')[0] + # create a blob name for the email - blob_name = f"{email_id}/content.html" + blob_name = f"{date_only}/{email_id}/content.html" # add metadata to the blob metadata = { From c7da128abd58d8c21b0db0175d36ee1cc2565c39 Mon Sep 17 00:00:00 2001 From: Victor Maldonado <67714035+vmlcode@users.noreply.github.com> Date: Mon, 20 Jan 2025 10:57:37 -0400 Subject: [PATCH 304/820] Add logs data state and filter functionality to subscription management --- .../SubscriptionManagement.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx index 6f17679a..62570fba 100644 --- a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx @@ -21,6 +21,8 @@ const SubscriptionManagement: React.FC = () => { const [selectedSubscriptionName, setSelectedSubscriptionName] = useState(""); const [selectedSubscriptionID, setSelectedSubscriptionID] = useState(""); const [isRecentChangesModal, setIsRecentChangesModal] = useState(false); + const [logsData, setlogsData] = useState([]) + const [filteredLogsData, setFilteredLogsData] = useState() const expirationDate = new Date((organization?.subscriptionExpirationDate || 0) * 1000).toLocaleDateString(); @@ -158,9 +160,9 @@ const SubscriptionManagement: React.FC = () => { ]; const FilterOptions = [ - { key: "1", text: "All Actions" }, - { key: "2", text: "Financial Assistant Change" }, - { key: "3", text: "Subscription Tier Change" } + { key: "1", text: "All Actions"}, + { key: "2", text: "Financial Assistant" }, + { key: "3", text: "Subscription Tier" } ]; useEffect(() => { @@ -274,6 +276,8 @@ const SubscriptionManagement: React.FC = () => { setIsRecentChangesModal(true); }; + const handleFilterChange = (event: any, selectedOption: any) => {} + const FinancialAssistantText = subscriptionStatus ? "You are subscribed to the Financial Assistant feature." : "Subscribe to Financial Assistant"; return ( @@ -340,7 +344,7 @@ const SubscriptionManagement: React.FC = () => {
    - +
    From 94ac846e39fe469bc0261712d6ca8f923a033172 Mon Sep 17 00:00:00 2001 From: egdagger <78937829+egdagger@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:29:18 -0400 Subject: [PATCH 305/820] FA-373 Implement the stripe customer portal to cancel and change payment information of the actual subscription (#269) * FA-373 Making the endpoints for the Stripe Customer Portal and aplying to the button functionality * FA-373-Addressing comments and resolving conflicts --- backend/app.py | 47 ++++ frontend/src/api/api.ts | 52 ++++ .../SubscriptionManagement.tsx | 236 ++++++++++-------- 3 files changed, 226 insertions(+), 109 deletions(-) diff --git a/backend/app.py b/backend/app.py index 21bc8c09..b9440ed6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1133,6 +1133,53 @@ def create_checkout_session(): return jsonify({"url": checkout_session.url}) +@app.route("/get-customer", methods=['POST']) +def get_customer(): + + subscription_id = request.json["subscription_id"] + + if not subscription_id: + logging.warning({"Error": "No subscription_id was provided for this request."}) + return jsonify({"error": "No subscription_id was provided for this request."}), 404 + + try: + subscription = stripe.Subscription.retrieve(subscription_id) + customer_id = subscription.get("customer") + + if not customer_id: + logging.warning({"error": "No customer_id found for the provided subscription."}) + return jsonify({"error": "No customer_id found for the provided subscription."}), 404 + + return jsonify({"customer_id": customer_id}), 200 + + except stripe.error.StripeError as e: + logging.warning({"error": {str(e)}}) + return jsonify({"error": str(e)}), 500 + except Exception as e: + logging.warning({"error": "Unexpected error: " + {str(e)}}) + return jsonify({"error": "Unexpected error: " + str(e)}), 500 + +@app.route("/create-customer-portal-session", methods=["POST"]) +def create_customer_portal_session(): + customer = request.json["customer"] + return_url = request.json["return_url"] + + if not customer or not return_url: + logging.warning({"error": "Missing 'customer' or 'return_url'"}) + return jsonify({"error": "Missing 'customer' or 'return_url'"}), 404 + + try: + + portal_session = stripe.billing_portal.Session.create( + customer = customer, + return_url = return_url + ) + + except Exception as e: + logging.warning({"error": "Unexpected error: " + {str(e)}}) + return jsonify({"error": "Unexpected error: " + str(e)}), 500 + + return jsonify({"url": portal_session.url}) @app.route("/api/stripe", methods=["GET"]) def getStripe(): diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 64861371..a16007f1 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -499,6 +499,58 @@ export async function createCheckoutSession({ userId, priceId, successUrl, cance return session; } +export async function getCustomerId({ subscriptionId }: { subscriptionId: string }): Promise { + const response = await fetch("/get-customer", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + subscription_id: subscriptionId + }) + }); + if (response.status > 299 || !response.ok) { + throw Error("Error creating checkout session") + } + + const data = await response.json(); + return data.customer_id; +} + +interface CustomerPortalSession { + url: string; +} + +export async function createCustomerPortalSession({ + customerId, + return_url +}: { + customerId: string; + return_url: string; +}): Promise{ + const response = await fetch("/create-customer-portal-session", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + customer: customerId, + return_url + }) + }); + if (response.status > 299 || !response.ok) { + throw Error("Error creating checkout session"); + } + + if (!response.ok) { + throw new Error("Error creating customer portal session"); + } + + const session = await response.json(); + return session; + +} + export async function getProductPrices({ user }: { user: any }): Promise { const user_id = user ? user.id : "00000000-0000-0000-0000-000000000000"; const user_name = user ? user.name : "anonymous"; diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx index 4d38f3ff..72ccabd5 100644 --- a/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx @@ -1,37 +1,43 @@ import React, { useEffect, useState } from "react"; -import styles from "./SubscriptionManagement.module.css" +import styles from "./SubscriptionManagement.module.css"; import { DefaultButton, Label, MessageBar, MessageBarType, PrimaryButton, Spinner } from "@fluentui/react"; import { useAppContext } from "../../providers/AppProviders"; -import { changeSubscription, getFinancialAssistant, getProductPrices, removeFinancialAssistant, upgradeSubscription } from "../../api"; +import { + createCustomerPortalSession, + getCustomerId, + changeSubscription, + getFinancialAssistant, + getProductPrices, + removeFinancialAssistant, + upgradeSubscription +} from "../../api"; import { IconX } from "@tabler/icons-react"; import { ChartPerson48Regular } from "@fluentui/react-icons"; const SubscriptionManagement: React.FC = () => { - const { user, organization, subscriptionTiers, setIsFinancialAssistantActive} = useAppContext(); + const { user, organization, subscriptionTiers, setIsFinancialAssistantActive } = useAppContext(); const [subscriptionStatus, setSubscriptionStatus] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [isErrorModal, setIsErrorModal] = useState(false) - const [isSubscriptionModal, setIsSubscriptionModal] = useState(false) - const [isUnsubscriptionModal, setIsUnsubscriptionModal] = useState(false) - const [isViewModal, setIsViewModal] = useState(false) - const [subscriptionName, setSusbscriptionName] = useState('') + const [isErrorModal, setIsErrorModal] = useState(false); + const [isSubscriptionModal, setIsSubscriptionModal] = useState(false); + const [isUnsubscriptionModal, setIsUnsubscriptionModal] = useState(false); + const [isViewModal, setIsViewModal] = useState(false); + const [subscriptionName, setSusbscriptionName] = useState(""); const [prices, setPrices] = useState([]); - const [isConfirmationModal, setIsConfirmationModal] = useState(false) - const [selectedSubscriptionName, setSelectedSubscriptionName] = useState('') - const [selectedSubscriptionID, setSelectedSubscriptionID] = useState('') - const [dataLoad, setDataLoad] = useState(false) - const [isSubscriptionChangeModal, setIsSubscriptionChangeModal] = useState(false) + const [isConfirmationModal, setIsConfirmationModal] = useState(false); + const [selectedSubscriptionName, setSelectedSubscriptionName] = useState(""); + const [selectedSubscriptionID, setSelectedSubscriptionID] = useState(""); + const [dataLoad, setDataLoad] = useState(false); + const [isSubscriptionChangeModal, setIsSubscriptionChangeModal] = useState(false); const expirationDate = new Date((organization?.subscriptionExpirationDate || 0) * 1000).toLocaleDateString(); - - useEffect(() => { const fetchStatus = async () => { setLoading(true); try { - setSusbscriptionName(subscriptionTiers[0] || '') + setSusbscriptionName(subscriptionTiers[0] || ""); if (!user?.organizationId) { throw new Error("Organization ID is required"); } @@ -48,13 +54,13 @@ const SubscriptionManagement: React.FC = () => { if (error.status === false) { setSubscriptionStatus(false); setError("Financial Assistant feature is not present in this subscription."); - setIsErrorModal(true) + setIsErrorModal(true); } else if (error.status === null) { setError("Bad request: unable to retrieve subscription status."); - setIsErrorModal(true) + setIsErrorModal(true); } else { setError("An error occurred while fetching subscription status."); - setIsErrorModal(true) + setIsErrorModal(true); } } finally { setLoading(false); @@ -65,21 +71,19 @@ const SubscriptionManagement: React.FC = () => { }, [dataLoad]); useEffect(() => { - - async function fetchPrices() { - try { - const data = await getProductPrices({ user }); - setPrices(data.prices); - } catch (err) { - console.error("Failed to fetch product prices:", err); - setError("Unable to fetch product prices. Please try again later."); - setIsErrorModal(true) - } + async function fetchPrices() { + try { + const data = await getProductPrices({ user }); + setPrices(data.prices); + } catch (err) { + console.error("Failed to fetch product prices:", err); + setError("Unable to fetch product prices. Please try again later."); + setIsErrorModal(true); } - - fetchPrices(); - }, [dataLoad]); - + } + + fetchPrices(); + }, [dataLoad]); const handleSubscribe = async () => { try { @@ -93,7 +97,7 @@ const SubscriptionManagement: React.FC = () => { } catch { setError("An error occurred while subscribing to the Financial Assistant feature."); setIsSubscriptionModal(false); - setIsErrorModal(true) + setIsErrorModal(true); } finally { setLoading(false); } @@ -112,61 +116,68 @@ const SubscriptionManagement: React.FC = () => { } catch { setError("An error occurred while unsubscribing from the Financial Assistant feature."); setIsUnsubscriptionModal(false); - setIsErrorModal(true) + setIsErrorModal(true); } finally { setLoading(false); } }; const handleFinancialAssistantToggle = async () => { - if(subscriptionStatus == true){ - setIsUnsubscriptionModal(true) - }else{ - setIsSubscriptionModal(true) + if (subscriptionStatus == true) { + setIsUnsubscriptionModal(true); + } else { + setIsSubscriptionModal(true); } - } + }; - const handleViewSubscription = () =>{ - setIsViewModal(true) - } + const handleViewSubscription = () => { + setIsViewModal(true); + }; - const handleSelectedSubscription = (priceNickname: string, priceID: string) =>{ - setSelectedSubscriptionName(priceNickname) - setSelectedSubscriptionID(priceID) - setIsConfirmationModal(true) - } + const handleSelectedSubscription = (priceNickname: string, priceID: string) => { + setSelectedSubscriptionName(priceNickname); + setSelectedSubscriptionID(priceID); + setIsConfirmationModal(true); + }; const handleCheckout = async (priceId: string) => { + if (subscriptionName === selectedSubscriptionName) { + const customerId = await getCustomerId({ + subscriptionId: organization?.subscriptionId ?? "" + }); + + const { url } = await createCustomerPortalSession({ + customerId: customerId, + return_url: window.location.origin + "/#/subscription-management" + }); + window.location.href = url; + } setIsConfirmationModal(false); - setIsViewModal(false) - setLoading(true) + setIsViewModal(false); + setLoading(true); let timer: NodeJS.Timeout; - try{ + try { await changeSubscription({ subscriptionId: organization?.subscriptionId ?? "", newPlanId: priceId }); - } catch(error){ + } catch (error) { console.error("Error trying to change the subscription: ", error); - setError("Error trying to change the subscription: ") - } finally{ - setLoading(false) + setError("Error trying to change the subscription: "); + } finally { + setLoading(false); setIsSubscriptionChangeModal(true); - setDataLoad(true) + setDataLoad(true); timer = setTimeout(() => { setIsSubscriptionChangeModal(false); }, 5000); //If we don't reload the page, the subscription tiers won't update correctly in the platform window.location.reload(); } - - - } + }; - const FinancialAssistantText = subscriptionStatus ? "You are subscribed to the Financial Assistant feature." : - "Subscribe to Financial Assistant" - + const FinancialAssistantText = subscriptionStatus ? "You are subscribed to the Financial Assistant feature." : "Subscribe to Financial Assistant"; return (
    @@ -175,39 +186,38 @@ const SubscriptionManagement: React.FC = () => {
    {loading ? ( - - ) : ( -
    - - - - - - - - - - - - + + +
    Subscription NameExpiration DateActions
    - {subscriptionName} - - {expirationDate} - + + ) : ( + + + + + + + + + + + + + - - -
    Subscription NameExpiration DateActions
    {subscriptionName}{expirationDate}
    - -
    -
    - )} + + +
    + )}
    - + {FinancialAssistantText}
    @@ -222,14 +232,15 @@ const SubscriptionManagement: React.FC = () => {
    {isViewModal && (
    - + {prices.map((price, index) => (

    {price.nickname}

    -

    {price.description} -

    - +

    {price.description}

    +

    ${(price.unit_amount / 100).toFixed(2)} {price.currency.toUpperCase()} per {price.recurring?.interval}

    @@ -246,17 +257,20 @@ const SubscriptionManagement: React.FC = () => { : "Subscribe"}
    - ))} + ))}
    )} {isConfirmationModal && (
    - + {selectedSubscriptionName === subscriptionName ? (
    -
    )} - {isFinancialAssistantActive && (
    Sales Factory logo @@ -446,66 +526,75 @@ const Chat = () => {
    ); }) - : answers.map((answer, index) => ( -
    - -
    - onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} - showFollowupQuestions={false} - showSources={true} - /> + : answers.map((answer, index) => { + return ( +
    + +
    + onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} + showFollowupQuestions={false} + showSources={true} + /> +
    -
    - ))} - - {isLoading && ( - <> - -
    - -
    - - )} + ); + })} {error ? ( <>
    - makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) + onRetry={ + () => streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) + //makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) } />
    ) : null} + {lastQuestionRef.current !== "" && ( + <> + +
    + {}} + onThoughtProcessClicked={() => {}} + onSupportingContentClicked={() => {}} + onFollowupQuestionClicked={q => {}} + showFollowupQuestions={false} + showSources={true} + /> +
    + + )}
    )}
    - {/*
    - -
    */} { - makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + //makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); }} extraButtonNewChat={} /> From ee2de0d9c7e0a0b3dd1e897c1408fa4f03774354 Mon Sep 17 00:00:00 2001 From: Alexjes98 Date: Mon, 17 Feb 2025 17:35:34 -0400 Subject: [PATCH 376/820] Refactor chat request handling to include conversation history and additional parameters --- frontend/src/pages/chat/Chat.tsx | 33 +++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 3d1206fc..638d57fb 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -98,13 +98,44 @@ const Chat = () => { agent = isFinancialAssistantActive ? "financial" : "consumer"; + let history: ChatTurn[] = []; + if (dataConversation.length > 0) { + history.push(...dataConversation); + } else { + history.push(...answers.map(a => ({ user: a[0], bot: { message: a[1]?.answer, thoughts: a[1]?.thoughts || [] } }))); + } + history.push({ user: question, bot: undefined }); + const request: ChatRequestGpt = { + history: history, + approach: Approaches.ReadRetrieveRead, + conversation_id: chatId !== null ? chatId : userId, + query: question, + file_blob_url: fileBlobUrl || "", + documentName, + agent, + overrides: { + promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, + excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, + top: retrieveCount, + semanticRanker: useSemanticRanker, + semanticCaptions: useSemanticCaptions, + suggestFollowupQuestions: useSuggestFollowupQuestions + } + }; + try { const response = await fetch("/stream_chatgpt", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ question: question, conversation_id: chatId, file_blob_url: fileBlobUrl }) + body: JSON.stringify({ + question: request.query, + conversation_id: request.conversation_id, + url: request.file_blob_url, + agent: request.agent, + documentName: request.documentName + }) }); if (!response.body) { From 448780aaeedf2656bb4e12c51e1bb611e4af2140 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 18 Feb 2025 07:40:56 -0400 Subject: [PATCH 377/820] =?UTF-8?q?Add=20streaming=20support=20for=20ChatG?= =?UTF-8?q?PT=20responses=20and=20improve=20loading=20text=20=E2=80=A6=20(?= =?UTF-8?q?#309)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add streaming support for ChatGPT responses and improve loading text in Answer component * Refactor chat request handling to include conversation history and additional parameters --- backend/app.py | 61 +++++++ frontend/src/components/Answer/Answer.tsx | 31 ++++ frontend/src/pages/chat/Chat.tsx | 206 +++++++++++++++++----- 3 files changed, 255 insertions(+), 43 deletions(-) diff --git a/backend/app.py b/backend/app.py index 18a3f02e..9e2352a0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -26,6 +26,9 @@ from identity.flask import Auth from datetime import timedelta, datetime +from flask import Flask, Response, stream_with_context +import requests + import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart @@ -485,6 +488,64 @@ def get_user(*, context: Dict[str, Any]) -> Tuple[Dict[str, Any], int]: ) +@app.route("/stream_chatgpt", methods=["POST"]) +def proxy_orc(): + conversation_id = request.json["conversation_id"] + question = request.json["question"] + file_blob_url = request.json["url"] + agent = request.json["agent"] + documentName = request.json["documentName"] + + client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") + client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") + + logging.info("[webbackend] conversation_id: " + conversation_id) + logging.info("[webbackend] question: " + question) + logging.info(f"[webbackend] file_blob_url: {file_blob_url}") + logging.info(f"[webbackend] User principal: {client_principal_id}") + logging.info(f"[webbackend] User name: {client_principal_name}") + logging.info(f"[webappend] Agent: {agent}") + try: + # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function + # It is set during the infrastructure deployment. + keySecretName = "orchestrator-host--financial" if agent == "financial" else "orchestrator-host--functionKey" + functionKey = get_azure_key_vault_secret(keySecretName) + except Exception as e: + logging.exception( + "[webbackend] exception in /api/orchestrator-host--functionKey" + ) + return ( + jsonify( + { + "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" + } + ), + 500, + ) + orchestrator_url = FINANCIAL_ASSISTANT_ENDPOINT if agent == "financial" else ORCHESTRATOR_ENDPOINT + + payload = json.dumps( + { + "conversation_id": conversation_id, + "question": question, + "url": file_blob_url, + "client_principal_id": client_principal_id, + "client_principal_name": client_principal_name, + "documentName": documentName, + } + ) + + headers = {"Content-Type": "text/event-stream", "x-functions-key": functionKey} + + def generate(): + with requests.post(orchestrator_url, stream=True, headers=headers, + data=payload) as r: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + yield chunk.decode() + + return Response(stream_with_context(generate()), content_type="text/event-stream") + @app.route("/chatgpt", methods=["POST"]) def chatgpt(): conversation_id = request.json["conversation_id"] diff --git a/frontend/src/components/Answer/Answer.tsx b/frontend/src/components/Answer/Answer.tsx index 4664a1f8..c01e554f 100644 --- a/frontend/src/components/Answer/Answer.tsx +++ b/frontend/src/components/Answer/Answer.tsx @@ -11,6 +11,8 @@ import { AskResponse, getCitationFilePath, getFilePath } from "../../api"; import { parseAnswerToHtml } from "./AnswerParser"; import { AnswerIcon } from "./AnswerIcon"; +import { animated, useSpring } from "@react-spring/web"; + const userLanguage = navigator.language; let citation_label_text = ""; if (userLanguage.startsWith("pt")) { @@ -21,6 +23,14 @@ if (userLanguage.startsWith("pt")) { citation_label_text = "Sources"; } +let generating_answer_text = ''; +if (userLanguage.startsWith('pt')) { + generating_answer_text = 'Gerando resposta'; +} else if (userLanguage.startsWith('es')) { + generating_answer_text = 'Generando respuesta'; +} else { + generating_answer_text = 'Generating response'; +} interface Props { answer: AskResponse; isSelected?: boolean; @@ -51,10 +61,31 @@ export const Answer = ({ showFollowupQuestions, showSources }: Props) => { + const animatedStyles = useSpring({ + from: { opacity: 0 }, + to: { opacity: 1 } + }); + const parsedAnswer = useMemo(() => parseAnswerToHtml(answer.answer, !!showSources, onCitationClicked), [answer]); const sanitizedAnswerHtml = DOMPurify.sanitize(parsedAnswer.answerHtml); + if (answer.answer === "") { + return ( + + + + +

    + {generating_answer_text} + +

    +
    +
    +
    + ); + } + return ( diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 38cb87dd..638d57fb 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -83,6 +83,121 @@ const Chat = () => { const [userId, setUserId] = useState(""); // this is more like a conversation id instead of a user id const triggered = useRef(false); + const [lastAnswer, setLastAnswer] = useState(""); + + const streamResponse = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { + let agent; + lastQuestionRef.current = question; + lastFileBlobUrl.current = fileBlobUrl; + + error && setError(undefined); + setIsLoading(true); + setActiveCitation(undefined); + setActiveAnalysisPanelTab(undefined); + setLastAnswer(""); + + agent = isFinancialAssistantActive ? "financial" : "consumer"; + + let history: ChatTurn[] = []; + if (dataConversation.length > 0) { + history.push(...dataConversation); + } else { + history.push(...answers.map(a => ({ user: a[0], bot: { message: a[1]?.answer, thoughts: a[1]?.thoughts || [] } }))); + } + history.push({ user: question, bot: undefined }); + const request: ChatRequestGpt = { + history: history, + approach: Approaches.ReadRetrieveRead, + conversation_id: chatId !== null ? chatId : userId, + query: question, + file_blob_url: fileBlobUrl || "", + documentName, + agent, + overrides: { + promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, + excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, + top: retrieveCount, + semanticRanker: useSemanticRanker, + semanticCaptions: useSemanticCaptions, + suggestFollowupQuestions: useSuggestFollowupQuestions + } + }; + + try { + const response = await fetch("/stream_chatgpt", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + question: request.query, + conversation_id: request.conversation_id, + url: request.file_blob_url, + agent: request.agent, + documentName: request.documentName + }) + }); + + if (!response.body) { + throw new Error("ReadableStream not supported in this browser."); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let result = ""; + let resultObj = { + conversation_id: "", + answer: "", + thoughts: "" + }; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value, { stream: true }); + if (chunk.startsWith("{")) { + resultObj = JSON.parse(chunk); + const conditionOne = answers.map(a => ({ user: a[0] })); + if (conditionOne.length <= 0) { + setRefreshFetchHistory(true); + setChatId(resultObj.conversation_id); + } else { + setRefreshFetchHistory(false); + } + setUserId(resultObj.conversation_id); + } else { + result += chunk; + setLastAnswer(result); + } + } + setAnswers([ + ...answers, + [ + question, + { + answer: result || "", + conversation_id: resultObj.conversation_id, + data_points: [""], + thoughts: resultObj.thoughts || [] + } as AskResponse + ] + ]); + const botResponse = { + answer: result || "", + conversation_id: resultObj.conversation_id, + data_points: [""], + thoughts: resultObj.thoughts || [] + } as AskResponse; + setDataConversation([...dataConversation, { user: question, bot: { message: botResponse.answer, thoughts: botResponse.thoughts } }]); + lastQuestionRef.current = ""; + } catch (error) { + console.error("Error fetching streamed response:", error); + setError(error); + } finally { + setIsLoading(false); + } + }; + const makeApiRequestGpt = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { let agent = null; lastQuestionRef.current = question; @@ -170,9 +285,6 @@ const Chat = () => { } catch (e) { setError(e); console.log(e); - console.log(typeof e); - console.log(Object.keys(e as object)); - console.log((e as Error).toString()); } finally { setIsLoading(false); } @@ -399,7 +511,6 @@ const Chat = () => {

    )} - {isFinancialAssistantActive && (
    Sales Factory logo @@ -446,66 +557,75 @@ const Chat = () => {
    ); }) - : answers.map((answer, index) => ( -
    - -
    - onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} - showFollowupQuestions={false} - showSources={true} - /> + : answers.map((answer, index) => { + return ( +
    + +
    + onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} + showFollowupQuestions={false} + showSources={true} + /> +
    -
    - ))} - - {isLoading && ( - <> - -
    - -
    - - )} + ); + })} {error ? ( <>
    - makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) + onRetry={ + () => streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) + //makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) } />
    ) : null} + {lastQuestionRef.current !== "" && ( + <> + +
    + {}} + onThoughtProcessClicked={() => {}} + onSupportingContentClicked={() => {}} + onFollowupQuestionClicked={q => {}} + showFollowupQuestions={false} + showSources={true} + /> +
    + + )}
    )}
    - {/*
    - -
    */} { - makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + //makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); }} extraButtonNewChat={} /> From ddd92a96bf86650c410a924ec544320fcfcfa092 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Tue, 18 Feb 2025 13:54:55 -0400 Subject: [PATCH 378/820] =?UTF-8?q?Revert=20"Add=20streaming=20support=20f?= =?UTF-8?q?or=20ChatGPT=20responses=20and=20improve=20loading=20text=20?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 448780aaeedf2656bb4e12c51e1bb611e4af2140. --- backend/app.py | 61 ------- frontend/src/components/Answer/Answer.tsx | 31 ---- frontend/src/pages/chat/Chat.tsx | 206 +++++----------------- 3 files changed, 43 insertions(+), 255 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9e2352a0..18a3f02e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -26,9 +26,6 @@ from identity.flask import Auth from datetime import timedelta, datetime -from flask import Flask, Response, stream_with_context -import requests - import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart @@ -488,64 +485,6 @@ def get_user(*, context: Dict[str, Any]) -> Tuple[Dict[str, Any], int]: ) -@app.route("/stream_chatgpt", methods=["POST"]) -def proxy_orc(): - conversation_id = request.json["conversation_id"] - question = request.json["question"] - file_blob_url = request.json["url"] - agent = request.json["agent"] - documentName = request.json["documentName"] - - client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") - client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") - - logging.info("[webbackend] conversation_id: " + conversation_id) - logging.info("[webbackend] question: " + question) - logging.info(f"[webbackend] file_blob_url: {file_blob_url}") - logging.info(f"[webbackend] User principal: {client_principal_id}") - logging.info(f"[webbackend] User name: {client_principal_name}") - logging.info(f"[webappend] Agent: {agent}") - try: - # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function - # It is set during the infrastructure deployment. - keySecretName = "orchestrator-host--financial" if agent == "financial" else "orchestrator-host--functionKey" - functionKey = get_azure_key_vault_secret(keySecretName) - except Exception as e: - logging.exception( - "[webbackend] exception in /api/orchestrator-host--functionKey" - ) - return ( - jsonify( - { - "error": f"Check orchestrator's function key was generated in Azure Portal and try again. ({keySecretName} not found in key vault)" - } - ), - 500, - ) - orchestrator_url = FINANCIAL_ASSISTANT_ENDPOINT if agent == "financial" else ORCHESTRATOR_ENDPOINT - - payload = json.dumps( - { - "conversation_id": conversation_id, - "question": question, - "url": file_blob_url, - "client_principal_id": client_principal_id, - "client_principal_name": client_principal_name, - "documentName": documentName, - } - ) - - headers = {"Content-Type": "text/event-stream", "x-functions-key": functionKey} - - def generate(): - with requests.post(orchestrator_url, stream=True, headers=headers, - data=payload) as r: - for chunk in r.iter_content(chunk_size=8192): - if chunk: - yield chunk.decode() - - return Response(stream_with_context(generate()), content_type="text/event-stream") - @app.route("/chatgpt", methods=["POST"]) def chatgpt(): conversation_id = request.json["conversation_id"] diff --git a/frontend/src/components/Answer/Answer.tsx b/frontend/src/components/Answer/Answer.tsx index c01e554f..4664a1f8 100644 --- a/frontend/src/components/Answer/Answer.tsx +++ b/frontend/src/components/Answer/Answer.tsx @@ -11,8 +11,6 @@ import { AskResponse, getCitationFilePath, getFilePath } from "../../api"; import { parseAnswerToHtml } from "./AnswerParser"; import { AnswerIcon } from "./AnswerIcon"; -import { animated, useSpring } from "@react-spring/web"; - const userLanguage = navigator.language; let citation_label_text = ""; if (userLanguage.startsWith("pt")) { @@ -23,14 +21,6 @@ if (userLanguage.startsWith("pt")) { citation_label_text = "Sources"; } -let generating_answer_text = ''; -if (userLanguage.startsWith('pt')) { - generating_answer_text = 'Gerando resposta'; -} else if (userLanguage.startsWith('es')) { - generating_answer_text = 'Generando respuesta'; -} else { - generating_answer_text = 'Generating response'; -} interface Props { answer: AskResponse; isSelected?: boolean; @@ -61,31 +51,10 @@ export const Answer = ({ showFollowupQuestions, showSources }: Props) => { - const animatedStyles = useSpring({ - from: { opacity: 0 }, - to: { opacity: 1 } - }); - const parsedAnswer = useMemo(() => parseAnswerToHtml(answer.answer, !!showSources, onCitationClicked), [answer]); const sanitizedAnswerHtml = DOMPurify.sanitize(parsedAnswer.answerHtml); - if (answer.answer === "") { - return ( - - - - -

    - {generating_answer_text} - -

    -
    -
    -
    - ); - } - return ( diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 638d57fb..38cb87dd 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -83,121 +83,6 @@ const Chat = () => { const [userId, setUserId] = useState(""); // this is more like a conversation id instead of a user id const triggered = useRef(false); - const [lastAnswer, setLastAnswer] = useState(""); - - const streamResponse = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { - let agent; - lastQuestionRef.current = question; - lastFileBlobUrl.current = fileBlobUrl; - - error && setError(undefined); - setIsLoading(true); - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - setLastAnswer(""); - - agent = isFinancialAssistantActive ? "financial" : "consumer"; - - let history: ChatTurn[] = []; - if (dataConversation.length > 0) { - history.push(...dataConversation); - } else { - history.push(...answers.map(a => ({ user: a[0], bot: { message: a[1]?.answer, thoughts: a[1]?.thoughts || [] } }))); - } - history.push({ user: question, bot: undefined }); - const request: ChatRequestGpt = { - history: history, - approach: Approaches.ReadRetrieveRead, - conversation_id: chatId !== null ? chatId : userId, - query: question, - file_blob_url: fileBlobUrl || "", - documentName, - agent, - overrides: { - promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, - excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, - top: retrieveCount, - semanticRanker: useSemanticRanker, - semanticCaptions: useSemanticCaptions, - suggestFollowupQuestions: useSuggestFollowupQuestions - } - }; - - try { - const response = await fetch("/stream_chatgpt", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - question: request.query, - conversation_id: request.conversation_id, - url: request.file_blob_url, - agent: request.agent, - documentName: request.documentName - }) - }); - - if (!response.body) { - throw new Error("ReadableStream not supported in this browser."); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let result = ""; - let resultObj = { - conversation_id: "", - answer: "", - thoughts: "" - }; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value, { stream: true }); - if (chunk.startsWith("{")) { - resultObj = JSON.parse(chunk); - const conditionOne = answers.map(a => ({ user: a[0] })); - if (conditionOne.length <= 0) { - setRefreshFetchHistory(true); - setChatId(resultObj.conversation_id); - } else { - setRefreshFetchHistory(false); - } - setUserId(resultObj.conversation_id); - } else { - result += chunk; - setLastAnswer(result); - } - } - setAnswers([ - ...answers, - [ - question, - { - answer: result || "", - conversation_id: resultObj.conversation_id, - data_points: [""], - thoughts: resultObj.thoughts || [] - } as AskResponse - ] - ]); - const botResponse = { - answer: result || "", - conversation_id: resultObj.conversation_id, - data_points: [""], - thoughts: resultObj.thoughts || [] - } as AskResponse; - setDataConversation([...dataConversation, { user: question, bot: { message: botResponse.answer, thoughts: botResponse.thoughts } }]); - lastQuestionRef.current = ""; - } catch (error) { - console.error("Error fetching streamed response:", error); - setError(error); - } finally { - setIsLoading(false); - } - }; - const makeApiRequestGpt = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { let agent = null; lastQuestionRef.current = question; @@ -285,6 +170,9 @@ const Chat = () => { } catch (e) { setError(e); console.log(e); + console.log(typeof e); + console.log(Object.keys(e as object)); + console.log((e as Error).toString()); } finally { setIsLoading(false); } @@ -511,6 +399,7 @@ const Chat = () => {

    )} + {isFinancialAssistantActive && (
    Sales Factory logo @@ -557,75 +446,66 @@ const Chat = () => {
    ); }) - : answers.map((answer, index) => { - return ( -
    - -
    - onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} - showFollowupQuestions={false} - showSources={true} - /> -
    + : answers.map((answer, index) => ( +
    + +
    + onShowCitation(c, n, index)} + onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} + onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} + onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} + showFollowupQuestions={false} + showSources={true} + />
    - ); - })} +
    + ))} + + {isLoading && ( + <> + +
    + +
    + + )} {error ? ( <>
    streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) - //makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) + onRetry={() => + makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) } />
    ) : null} - {lastQuestionRef.current !== "" && ( - <> - -
    - {}} - onThoughtProcessClicked={() => {}} - onSupportingContentClicked={() => {}} - onFollowupQuestionClicked={q => {}} - showFollowupQuestions={false} - showSources={true} - /> -
    - - )}
    )}
    + {/*
    + +
    */} { - streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); - //makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); }} extraButtonNewChat={} /> From b0cb077530975802a9f8cac893b249677b27a5d3 Mon Sep 17 00:00:00 2001 From: Alexjes98 Date: Wed, 19 Feb 2025 10:49:38 -0400 Subject: [PATCH 379/820] Improve error handling and request processing in ChatGPT streaming endpoint --- backend/app.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9e2352a0..7b91baa0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -490,26 +490,26 @@ def get_user(*, context: Dict[str, Any]) -> Tuple[Dict[str, Any], int]: @app.route("/stream_chatgpt", methods=["POST"]) def proxy_orc(): - conversation_id = request.json["conversation_id"] - question = request.json["question"] - file_blob_url = request.json["url"] - agent = request.json["agent"] - documentName = request.json["documentName"] + data = request.get_json() + conversation_id = data.get("conversation_id") + question = data.get("question") + file_blob_url = data.get("url") + agent = data.get("agent") + documentName = data.get("documentName") + + if not question: + return jsonify({"error": "Missing required parameters"}), 400 client_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID") client_principal_name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME") - logging.info("[webbackend] conversation_id: " + conversation_id) - logging.info("[webbackend] question: " + question) - logging.info(f"[webbackend] file_blob_url: {file_blob_url}") - logging.info(f"[webbackend] User principal: {client_principal_id}") - logging.info(f"[webbackend] User name: {client_principal_name}") - logging.info(f"[webappend] Agent: {agent}") try: # keySecretName is the name of the secret in Azure Key Vault which holds the key for the orchestrator function # It is set during the infrastructure deployment. keySecretName = "orchestrator-host--financial" if agent == "financial" else "orchestrator-host--functionKey" functionKey = get_azure_key_vault_secret(keySecretName) + if not functionKey: + raise ValueError(f"Function key {keySecretName} is empty") except Exception as e: logging.exception( "[webbackend] exception in /api/orchestrator-host--functionKey" @@ -538,11 +538,15 @@ def proxy_orc(): headers = {"Content-Type": "text/event-stream", "x-functions-key": functionKey} def generate(): - with requests.post(orchestrator_url, stream=True, headers=headers, - data=payload) as r: - for chunk in r.iter_content(chunk_size=8192): - if chunk: - yield chunk.decode() + try: + with requests.post(orchestrator_url, stream=True, headers=headers, + data=payload) as r: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + yield chunk.decode() + except Exception as e: + logging.exception(f"[webbackend] exception in /stream_chatgpt: {str(e)}") + yield jsonify({"error": str(e)}), 500 return Response(stream_with_context(generate()), content_type="text/event-stream") From acd138afbac619f95c9b63977513d7c7dbc9aa26 Mon Sep 17 00:00:00 2001 From: Alexjes98 Date: Thu, 20 Feb 2025 10:52:08 -0400 Subject: [PATCH 380/820] Add user authentication headers to ChatGPT streaming request --- frontend/src/pages/chat/Chat.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 638d57fb..a028d434 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -127,7 +127,9 @@ const Chat = () => { const response = await fetch("/stream_chatgpt", { method: "POST", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-MS-CLIENT-PRINCIPAL-ID": user?.id || "", + "X-MS-CLIENT-PRINCIPAL-NAME": user?.name || "" }, body: JSON.stringify({ question: request.query, From cdae30075ed13854bd74cde5a1d9f14dcefce410 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 20 Feb 2025 16:43:51 -0400 Subject: [PATCH 381/820] Remove source URL artifacts from chat response (#313) --- frontend/src/pages/chat/Chat.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index a028d434..1dcc5aaf 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -172,6 +172,8 @@ const Chat = () => { setLastAnswer(result); } } + const regex = /(Source:\s?\/?)?(source:)?(https:\/\/)?([^/]+\.blob\.core\.windows\.net)?(\/?documents\/?)?/g; + result = result.replace(regex, ""); setAnswers([ ...answers, [ From 5db8c6dc3ad17f74289352f2e13da281e766a6a9 Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 20 Feb 2025 16:44:50 -0400 Subject: [PATCH 382/820] Implement restart chat functionality with ref-based control (#314) --- frontend/src/pages/chat/Chat.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index 1dcc5aaf..c2f17c4c 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -70,26 +70,26 @@ const Chat = () => { const lastFileBlobUrl = useRef(""); const chatMessageStreamEnd = useRef(null); const [fileType, setFileType] = useState(""); - const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); - + const [activeCitation, setActiveCitation] = useState(); const [activeAnalysisPanelTab, setActiveAnalysisPanelTab] = useState(undefined); - + const [selectedAnswer, setSelectedAnswer] = useState(0); const [answers, setAnswers] = useState<[user: string, response: AskResponse][]>([]); - + const [userId, setUserId] = useState(""); // this is more like a conversation id instead of a user id const triggered = useRef(false); - + const [lastAnswer, setLastAnswer] = useState(""); - + const restartChat = useRef(false); + const streamResponse = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { let agent; lastQuestionRef.current = question; lastFileBlobUrl.current = fileBlobUrl; - + restartChat.current = false; error && setError(undefined); setIsLoading(true); setActiveCitation(undefined); @@ -154,6 +154,10 @@ const Chat = () => { }; while (true) { + if (restartChat.current) { + handleNewChat(); + return; + } const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); @@ -311,6 +315,7 @@ const Chat = () => { const handleNewChat = () => { if (lastQuestionRef.current || dataConversation.length > 0 || chatIsCleaned) { + restartChat.current = true; lastQuestionRef.current = ""; lastFileBlobUrl.current = ""; error && setError(undefined); From 25bff39d0beee95119cf9069c4f86b4981a16329 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:00:31 -0400 Subject: [PATCH 383/820] FA-239 fix doc preview (#315) --- frontend/src/pages/chat/Chat.tsx | 21 ++++++++++++++++++++- frontend/vite.config.ts | 11 +---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index c2f17c4c..a7393e15 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -414,11 +414,30 @@ const Chat = () => { setUseSuggestFollowupQuestions(!!checked); }; + const extractAfterDomain = (url: string) => { + const extensions = [".net", ".com"]; + + for (const ext of extensions) { + const index = url.lastIndexOf(ext); + if (index !== -1) { + let currentUrl = url.substring(index + ext.length); + if (currentUrl.startsWith("/")) { + currentUrl = currentUrl.substring(1); + } + return currentUrl; + } + } + return url; + } + const onShowCitation = async (citation: string, fileName: string, index: number) => { if (!citation.endsWith(".pdf") && !citation.endsWith(".doc") && !citation.endsWith(".docx")) { return window.open(citation, "_blank"); } - const response = await getPdf(fileName); + // Extract filepath if necessary + const modifiedFilename = extractAfterDomain(fileName) + + const response = await getPdf(modifiedFilename); if (activeCitation === citation && activeAnalysisPanelTab === AnalysisPanelTabs.CitationTab && selectedAnswer === index) { setActiveAnalysisPanelTab(undefined); } else { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4fa029a1..d190a786 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,19 +4,10 @@ import react from "@vitejs/plugin-react"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], - resolve: { - alias: { - 'path': 'path-browserify', - 'util': 'util', - }, - }, build: { outDir: "../backend/static", emptyOutDir: true, - sourcemap: true, - rollupOptions: { - external: ['path', 'util'], - }, + sourcemap: true }, server: { proxy: { From ab146f5e109b75a6c445770fec28456b65c797de Mon Sep 17 00:00:00 2001 From: Alexjes98 Date: Mon, 24 Feb 2025 15:48:11 -0400 Subject: [PATCH 384/820] Add conditional API request method based on financial assistant mode --- frontend/src/pages/chat/Chat.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index a028d434..987d6e00 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -258,6 +258,7 @@ const Chat = () => { thoughts: result.thoughts || [] } as AskResponse; setDataConversation([...dataConversation, { user: question, bot: { message: response.answer, thoughts: response.thoughts } }]); + lastQuestionRef.current = ""; // Voice Synthesis if (speechSynthesisEnabled) { @@ -585,10 +586,13 @@ const Chat = () => {
    streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) - //makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current) - } + onRetry={() => { + if (isFinancialAssistantActive) { + makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current); + } else { + streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current); + } + }} />
    @@ -626,8 +630,11 @@ const Chat = () => { placeholder={placeholderText} disabled={isLoading} onSend={(question, fileBlobUrl) => { - streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); - //makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + if (isFinancialAssistantActive) { + makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + } else { + streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); + } }} extraButtonNewChat={} /> From 574ee2aa49acfab0802dca5ceff625e0c300e6dd Mon Sep 17 00:00:00 2001 From: Alejandro Lopez Date: Thu, 27 Feb 2025 03:24:01 -0400 Subject: [PATCH 385/820] Remove financial assistant mode conditional logic in chat response handling (#319) --- frontend/src/pages/chat/Chat.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx index a0967c17..2f9aca00 100644 --- a/frontend/src/pages/chat/Chat.tsx +++ b/frontend/src/pages/chat/Chat.tsx @@ -613,11 +613,7 @@ const Chat = () => { { - if (isFinancialAssistantActive) { - makeApiRequestGpt(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current); - } else { - streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current); - } + streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current); }} />
    @@ -656,11 +652,7 @@ const Chat = () => { placeholder={placeholderText} disabled={isLoading} onSend={(question, fileBlobUrl) => { - if (isFinancialAssistantActive) { - makeApiRequestGpt(question, chatId !== "" ? chatId : null, fileBlobUrl || null); - } else { - streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); - } + streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); }} extraButtonNewChat={} /> From 437708ae0f85da1b3440ba9f18af454cc6aee3f3 Mon Sep 17 00:00:00 2001 From: Pedro Labrador <25873364+PedroLabrador@users.noreply.github.com> Date: Thu, 27 Feb 2025 03:28:19 -0400 Subject: [PATCH 386/820] FA-437 apply redirect for reports to the financial agent (#320) --- backend/app.py | 64 ++++++++++++++++++++++++++++++++++++++-- backend/requirements.txt | 3 +- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/backend/app.py b/backend/app.py index 7b91baa0..97f1deda 100644 --- a/backend/app.py +++ b/backend/app.py @@ -58,7 +58,8 @@ delete_conversation, ) import stripe.error - +from bs4 import BeautifulSoup +from urllib.parse import urlencode from shared.cosmo_db import ( create_report, get_report, @@ -278,15 +279,74 @@ def check_user_authorization( logger.error(f"[auth] Unexpected error validating user {client_principal_id}: {str(e)}") raise +def store_request_params_in_session(keys=None): + """ + Decorator to store specified request parameters into the Flask session. + + Args: + keys (list, optional): A list of parameter keys to store. + """ + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if keys is not None: + for key in keys: + if key in request.args: + session[key] = request.args[key] + elif key in request.form: + session[key] = request.form[key] + logger.info(f"Stored request parameters in session: {session}") + return f(*args, **kwargs) + return decorated_function + return decorator + +def append_script(file, query_params): + try: + with open(file, 'r') as f: + html_content = f.read() + + soup = BeautifulSoup(html_content, 'html.parser') + encoded_params = urlencode(query_params) + full_url = f"?{encoded_params}" + + script_tag = soup.new_tag('script', type='text/javascript') + script_content = f""" + console.log('Modifying location without reload: {full_url}'); + if (window.history && window.history.pushState) + window.history.pushState(null, '', '{full_url}'); + """ + script_tag.string = script_content + + soup.body.append(script_tag) + + modified_html = str(soup) + return Response(modified_html, mimetype='text/html') + + except FileNotFoundError: + return "HTML file not found", 404 + + @app.route("/") +@store_request_params_in_session(['agent','document']) @auth.login_required def index(*, context): """ Endpoint to get the current user's data from Microsoft Graph API """ logger.debug(f"User context: {context}") - return send_from_directory("static", "index.html") + # get session data if available + agent = session.get('agent') + document = session.get('document') + + session.pop('agent', None) + session.pop('document', None) + + if not agent or not document: + return send_from_directory("static", "index.html") + + query_params = {"agent": agent, "document": document} + return append_script('static/index.html', query_params) # route for other static files diff --git a/backend/requirements.txt b/backend/requirements.txt index c0a89e4e..770f153a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -33,4 +33,5 @@ langchain-openai==0.2.14 pydantic-settings==2.7.0 markdown pydantic[email] -weasyprint==63.1 \ No newline at end of file +weasyprint==63.1 +beautifulsoup4==4.13.3 \ No newline at end of file From f5a7475e716217115a28319c7a6cda1f0bff7140 Mon Sep 17 00:00:00 2001 From: Victor Maldonado <67714035+vmlcode@users.noreply.github.com> Date: Thu, 27 Feb 2025 03:30:44 -0400 Subject: [PATCH 387/820] 398 use the company data in the freedaid UI (#308) * Add company data API endpoint and integrate with frontend dropdown * Improve error logging for API request failure in getCompanyData function --- backend/app.py | 15 +++++- backend/shared/cosmo_db.py | 2 +- frontend/src/api/api.ts | 14 +++++ .../ReportCreation/ReportTemplateCreation.tsx | 54 +++++++++++++------ frontend/vite.config.ts | 3 +- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/backend/app.py b/backend/app.py index 97f1deda..aea3c385 100644 --- a/backend/app.py +++ b/backend/app.py @@ -77,7 +77,8 @@ get_organization_subscription, create_invitation, set_user, - create_organization + create_organization, + get_company_list ) load_dotenv(override=True) @@ -3500,7 +3501,17 @@ def get_logs(): logger.exception("Unexpected error in get_logs") return create_error_response("Internal Server Error", 500) - +@app.route("/api/companydata", methods=["GET"]) +def get_company_data(): + try: + data = get_company_list() + return create_success_response(data, 200) + except CosmosHttpResponseError as e: + logger.exception(f"Unexpected error in with cosmos db: {e}") + return create_error_response("Internal Server Error", 500) + except Exception as e: + logger.exception("Unexpected error in get_company_analysis") + return create_error_response("Internal Server Error", 500) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/backend/shared/cosmo_db.py b/backend/shared/cosmo_db.py index 6e34bcab..34d90ca2 100644 --- a/backend/shared/cosmo_db.py +++ b/backend/shared/cosmo_db.py @@ -669,7 +669,7 @@ def get_company_list(): try: items = list( container.query_items( - query="SELECT c.id, c.name, c.ticker, c.is_active, c.created_at, c.lastRun FROM c", + query="SELECT * FROM c", enable_cross_partition_query=True, ) ) diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 15e20007..2c8e2605 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -922,3 +922,17 @@ export async function getLogs(organizationId: string): Promise { } } +export async function getCompanyData() { + try { + const response = await fetch("/api/companydata", { + method: "GET", + headers: { + "Content-Type": "application/json", + } + }); + const companydata = await response.json() + return companydata.data + } catch { + console.error("API request failed") + } +} diff --git a/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx b/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx index cb99ae22..fe35d2fb 100644 --- a/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx +++ b/frontend/src/pages/reports/ReportCreation/ReportTemplateCreation.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styles from './ReportTemplateCreation.module.css'; import { IconArrowBack, IconX } from '@tabler/icons-react'; -import { Label, Dropdown, IDropdownOption } from '@fluentui/react'; +import { Label, Dropdown, IDropdownOption, Spinner } from '@fluentui/react'; import { useNavigate } from 'react-router-dom'; -import { createSummarizationReport } from '../../../api'; +import { createSummarizationReport, getCompanyData } from '../../../api'; export const TemplateCreation: React.FC = () => { @@ -13,7 +13,8 @@ export const TemplateCreation: React.FC = () => { const [description, setDescription] = useState('') const [errorMessage, setErrorMessage] = useState("") const [isConfirm, setIsConfirm] = useState(false) - + const [companyOptions, setCompanyOptions] = useState([]) + const [loading, setLoading] = useState(false) const templateTypeOptions = [ { key: '1', text: '10-K' }, @@ -21,15 +22,35 @@ export const TemplateCreation: React.FC = () => { { key: '3', text: '8-K' }, { key: '4', text: 'DEF 14A' }, ] - const companyOptions = [ - { key: '1', text: "The Home Depot", value: 'LOW' }, - { key: '2', text: "Lowe's Home Improvement", value: 'HD' }, - { key: '3', text: "Walmart", value: 'WMT' }, - { key: '4', text: "Ace Hardware", value: 'ACE' }, - { key: '5', text: "Amazon", value: 'AMZN' }, - { key: '7', text: "Costco", value: 'COST' }, - { key: '8', text: "Target", value: 'TGT' }, - ] + // const companyOptions = [ + // { key: '1', text: "The Home Depot", value: 'LOW' }, + // { key: '2', text: "Lowe's Home Improvement", value: 'HD' }, + // { key: '3', text: "Walmart", value: 'WMT' }, + // { key: '4', text: "Ace Hardware", value: 'ACE' }, + // { key: '5', text: "Amazon", value: 'AMZN' }, + // { key: '7', text: "Costco", value: 'COST' }, + // { key: '8', text: "Target", value: 'TGT' }, + // ] + + useEffect(() => { + const getDropdownCompanies = async () => { + setLoading(true) + try { + let data = await getCompanyData() + console.log(data) + setCompanyOptions(data) + console.log(companyOptions) + } + catch { + console.error("Error Fetching Company Data") + setCompanyOptions([]) + } + finally { + setLoading(false) + } + } + getDropdownCompanies() + }, []) const handleDropdownTemplate = (event: any, selectedOption: any) => { setTemplateType(selectedOption.text) @@ -99,14 +120,17 @@ export const TemplateCreation: React.FC = () => {

    Summarization Report Template Creation

    -
    + {loading ? ( + + ) : ( - + )} + {errorMessage !== null &&

    {errorMessage}

    }
    +
    + {/* Content */} +
    + {loading ? ( + { - onConfirm(); - }} - text="Send Invitation" /> -
    - - )} - {success && ( - -
    -

    Invitation Sent

    -

    - An invitation has been sent to {email}. They will receive an email with a link to create an account. -

    -
    -
    - -
    -
    - )} - + ) : !success ? ( +
    { + e.preventDefault(); + handleSubmit(); + }} + > +
    + + +
    +
    + + +
    +
    + + +
    + {errorMessage &&
    {errorMessage}
    } +
    + + +
    +
    + ) : ( +
    +

    Invitation Sent

    +

    + An invitation has been sent to {email}. They will receive an email with a link to create an account. +

    +
    + +
    +
    + )} +
    +
    +
    ); }; @@ -309,88 +218,240 @@ export const DeleteUserDialog = ({ isOpen, onDismiss, onConfirm, - isDeletingUser + isDeletingUser, + userName }: { isOpen: boolean; onDismiss: any; onConfirm: any; isDeletingUser: boolean; + userName?: string; +}) => { + if (!isOpen) return null; + + return ( +
    +
    e.stopPropagation()}> + {/* Header */} +
    +

    Delete User

    + +
    + + {/* Content */} +
    +

    + Are you sure you want to remove {userName} from the team? +

    + + {/* Buttons */} +
    + + +
    +
    +
    +
    + ); +}; +export const DeleteInvitationDialog = ({ + isOpen, + onDismiss, + onConfirm, + selectedInvitation, + isDeletingUser +}: { + isOpen: boolean; + onDismiss: () => void; + onConfirm: (invitationId: string) => void; + selectedInvitation?: { invitation_id: string; id: string }; + isDeletingUser: boolean; }) => { + if (!isOpen) return null; + + const handleConfirm = () => { + if (selectedInvitation) { + onConfirm(selectedInvitation.invitation_id); + } + }; + return ( - +
    +
    + ); +}; + +export const EditUserDialog = ({ + isOpen, + onDismiss, + onConfirm, + selectedUser, + inputUserName, + categorySelection, + roleOptions, + errorMessage, + handleInputName, + handleTypeDropdownChange, + isSavingUser +}: { + isOpen: boolean; + onDismiss: () => void; + onConfirm: (userId: string) => void; + selectedUser?: { id: string; data: { name: string; email: string } }; + inputUserName: string; + inputEmailName: string; + categorySelection: string; + roleOptions: Array<{ key: string; text: string }>; + errorMessage: string; + handleInputName: (e: React.ChangeEvent) => void; + handleTypeDropdownChange: (event: any, option: any) => void; + isSavingUser?: boolean; +}) => { + if (!isOpen || !selectedUser) return null; + + const handleCancel = () => { + onDismiss(); + }; + + const handleConfirm = () => { + onConfirm(selectedUser.id); + }; + + const [isResettingPassword, setIsResettingPassword] = useState(false); + + function generateRandomPassword(length = 12) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; + let password = ""; + for (let i = 0; i < length; i++) { + password += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return password; + } + + const handleResetPassword = async () => { + if (!selectedUser?.id) return; + setIsResettingPassword(true); + try { + const newPassword = generateRandomPassword(); + const res = await resetUserPassword({ userId: selectedUser.id, newPassword }); + if (res.error) { + toast("Error resetting password: " + res.error, { type: "error" }); + } else { + toast(`Password reset! The user will receive an email with their new password.`, { type: "success" }); + } + } catch (e) { + toast("Error resetting password", { type: "error" }); + } finally { + setIsResettingPassword(false); + } + }; + return ( +
    +
    e.stopPropagation()}> + {/* Header */} +
    +

    Edit User

    + +
    + + {/* Content */} +
    +
    + {/* Email Address */} +
    + + + Email address cannot be changed +
    + + {/* Username */} +
    + + + This name will be displayed to other users +
    + + {/* Role Dropdown */} +
    + + +
    + + {/* Change Password Button */} +
    + +
    + + {/* Buttons */} +
    + + +
    +
    +
    +
    +
    ); }; @@ -414,6 +475,12 @@ const Admin = () => { role: "" } }); + const [selectedInvitation, setSelectedInvitation] = useState({ + id: "", + nickname: "", + role: "", + invitation_id: "" + }); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); @@ -425,14 +492,15 @@ const Admin = () => { const [inputUserName, setInputUserName] = useState(""); const [inputEmailName, setInputEmailName] = useState(""); const roleOptions = [ - { key: "1", text: "user" }, - { key: "2", text: "admin" } + { key: "user", text: "user" }, + { key: "admin", text: "admin" } ]; const [categorySelection, setCategorySelection] = useState(""); const [errorMessage, setErrorMessage] = useState(""); - const [isError, setIsError] = useState(false); const [dataLoad, setDataLoad] = useState(false); + const [showDeleteInvitationModal, setShowDeleteInvitationModal] = useState(false); const [isEditSuccess, setIsEditSuccess] = useState(false); + const [isSavingUser, setIsSavingUser] = useState(false); const roleStyles: { [key in "admin" | "user" | "platformAdmin"]: string } = { admin: styles.roleAdmin, user: styles.roleUser, @@ -467,7 +535,6 @@ const Admin = () => { if (!Array.isArray(usersList)) { usersList = []; } - setUsers(usersList); setFilteredUsers(usersList); } catch (error) { @@ -485,9 +552,18 @@ const Admin = () => { useEffect(() => { let filtered = users; if (search) { - filtered = filtered.filter( - (user: any) => user.data.name.toLowerCase().includes(search.toLowerCase()) || user.data.email.toLowerCase().includes(search.toLowerCase()) - ); + filtered = filtered.filter((user: any) => { + if (user.user_new) { + return ( + (user.nickname && user.nickname.toLowerCase().includes(search.toLowerCase())) || + (user.data?.email && user.data.email.toLowerCase().includes(search.toLowerCase())) + ); + } + return ( + (user.data?.name && user.data.name.toLowerCase().includes(search.toLowerCase())) || + (user.data?.email && user.data.email.toLowerCase().includes(search.toLowerCase())) + ); + }); } if (roleFilter !== "all") { filtered = filtered.filter((user: any) => user.role === roleFilter); @@ -518,6 +594,25 @@ const Admin = () => { }); }; + const deleteInvitationFromOrganization = (id: string) => { + setIsDeletingUser(true); + deleteInvitation({ user, invitationId: id }).then(res => { + if (res.error) { + console.log("error", res.error); + toast("There was an error deleting the invitation", { type: "error" }); + setIsDeleting(false); + } else { + const updatedUsers = users.filter((user: any) => user.invitation_id !== id); + setUsers(updatedUsers); + setFilteredUsers(updatedUsers); + setIsDeleting(false); + setShowDeleteInvitationModal(false); + toast("Invitation deleted successfully", { type: "success" }); + } + setIsDeletingUser(false); + }); + }; + const handleEditClick = (user: any) => { setSelectedUser(user); setIsEditing(true); @@ -537,42 +632,32 @@ const Admin = () => { const editUser = async (userID: string) => { if (inputUserName == "") { - setErrorMessage("Please type the Username"); - setIsError(true); - return; - } - if (inputEmailName == "") { - setErrorMessage("Please type the Email"); - setIsError(true); - return; - } - if (categorySelection == "") { - setErrorMessage("Please select the Report Category"); - setIsError(true); + toast("Please type the Username", { type: "error" }); return; } let timer: NodeJS.Timeout; - + setIsSavingUser(true); try { await updateUserData({ userId: userID, patchData: { name: inputUserName, - email: inputEmailName, - role: categorySelection + role: categorySelection, + organizationId: user.organizationId } }); setIsEditing(false); setIsEditSuccess(true); timer = setTimeout(() => { setIsEditSuccess(false); - setInputEmailName(""); setInputUserName(""); }, 3000); } catch (error) { - console.error("Error trying to update the state: ", error); + toast("User updated failed", { type: "error" }); } finally { + toast("User updated successfully", { type: "success" }); + setIsSavingUser(false); setDataLoad(!dataLoad); } }; @@ -589,354 +674,349 @@ const Admin = () => { }, [isEditing]); return ( -
    + <> - <> -
    -
    -
    - - - - { - setSearch(newValue || ""); - }} - /> -
    - -
    - - {showRoleDropdown && ( -
    + <> +
    +
    +
    + - {roleFilterOptions.map(option => ( -
    { - setRoleFilter(option.value); - setShowRoleDropdown(false); - }} - style={{ - padding: "8px 16px", - cursor: "pointer", - fontWeight: roleFilter === option.value ? "bold" : "normal", - background: roleFilter === option.value ? "#f3f4f6" : "white" - }} - > - {option.label} -
    - ))} -
    - )} -
    -
    - { - setIsOpen(true); - }} - > - - Create User - -
    - - {loading ? null : } - { - setIsDeleting(false); - }} - onConfirm={() => { - deleteUserFromOrganization(selectedUser?.id); - }} - isDeletingUser={isDeletingUser} - /> - {showModal && ( - <> -
    setIsEditing(false)}>
    -
    - -
    - - - - - - - {isError && {errorMessage}} - - { - setInputEmailName(""); - setInputUserName(""); - setIsEditing(false); - }} - text="Cancel" - /> - { - editUser(selectedUser.id); + onChange={(_ev, newValue) => { + setSearch(newValue || ""); }} - text="Edit User" + onRenderSuffix={() => + search ? ( + + ) : null + } /> - -
    - - )} - {isEditSuccess && ( -
    - -
    - )} - {loading ? ( - - ) : ( -
    - - - - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - - - - - - - ); - })} - -
    + +
    + + {showRoleDropdown && ( +
    - Name -
    EmailRoleActions
    ( +
    { + setRoleFilter(option.value); + setShowRoleDropdown(false); }} - > - {user.data.name} -
    - {user.data.email} - -
    + ))} +
    + )} + + + { + setIsOpen(true); + }} + > + + Create User + + + + {loading ? null : } + { + setIsDeleting(false); + }} + onConfirm={() => { + deleteUserFromOrganization(selectedUser?.id); + }} + isDeletingUser={isDeletingUser} + userName={selectedUser?.data?.name} + /> + { + setInputEmailName(""); + setInputUserName(""); + setIsEditing(false); + }} + onConfirm={userId => editUser(userId)} + selectedUser={selectedUser} + inputUserName={inputUserName} + inputEmailName={inputEmailName} + categorySelection={categorySelection} + roleOptions={roleOptions} + errorMessage={errorMessage} + handleInputName={handleInputName} + handleTypeDropdownChange={handleTypeDropdownChange} + isSavingUser={isSavingUser} + /> + setShowDeleteInvitationModal(false)} + onConfirm={invitationId => deleteInvitationFromOrganization(invitationId)} + selectedInvitation={selectedInvitation} + isDeletingUser={isDeletingUser} + /> + {loading ? ( + + ) : ( +
      +
    • + Team Members +
    • + {filteredUsers.map((user: any, index) => { + const isNew = user.user_new; + const userName = isNew ? user.nickname : user.data.name; + const userEmail = isNew ? user.data.email : user.data.email; + const userRole = user.role; + + // Verifica si el usuario invitado tiene el token expirado + let userStatus = "Active"; + let statusColor = "#e5e7eb"; + let statusTextColor = "#1e2939"; + if (isNew) { + const now = Math.floor(Date.now() / 1000); + if (user.token_expiry && Number(user.token_expiry) < now) { + userStatus = "Expired"; + statusColor = "#fee2e2"; + statusTextColor = "#b91c1c"; + } else { + userStatus = "Invited"; + statusColor = "#fef9c2"; + statusTextColor = "#894b00"; + } + } + + return ( +
    • + {/* Info: name, email, role */} +
      +
      + {userName} + + {userRole === "platformAdmin" ? "Platform Admin" : userRole.charAt(0).toUpperCase() + userRole.slice(1)} + + -
      - {user.role === "platformAdmin" ? "Platform Admin" : user.role} -
      -
      -
    - { -
    - - -
    - } -
    -
    - )} - -
    + {userStatus} + +
    + {userEmail} +
    + {/* Actions */} +
    + {!isNew ? ( + <> + + + + ) : ( + <> + + + )} +
    + + ); + })} + + )} + +
    + ); }; From ecc61cf6f7073b74f0486153649153c9d2a1f0d3 Mon Sep 17 00:00:00 2001 From: Manuel Castro Date: Fri, 27 Jun 2025 15:30:13 -0400 Subject: [PATCH 571/820] release 1.2.1 --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 45573bca..32a9aec6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.0.3", + "version": "1.2.1", "type": "module", "scripts": { "dev": "vite", From 9df950e8736e2840c679ee08670c8b69858f8f34 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Mon, 30 Jun 2025 09:44:21 -0400 Subject: [PATCH 572/820] * Change the color for the platformAdmin role (#473) --- frontend/src/pages/admin/Admincopy.module.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/admin/Admincopy.module.css b/frontend/src/pages/admin/Admincopy.module.css index 329fa06a..ca91d45e 100644 --- a/frontend/src/pages/admin/Admincopy.module.css +++ b/frontend/src/pages/admin/Admincopy.module.css @@ -274,8 +274,8 @@ border-radius: 9999px; font-size: 0.75rem; font-weight: 600; - background-color: #d9ff98; - color: #1b4332; + background-color: #bfdbfe; + color: #1e40af; } From 8434dc76e31e7fbec8c998b351ca01d0457d6a43 Mon Sep 17 00:00:00 2001 From: Alexjes98 Date: Mon, 30 Jun 2025 14:06:52 -0400 Subject: [PATCH 573/820] Suppress Azure SDK logs in app.py and remove unused web indexing component from Organizationcopy.tsx --- backend/app.py | 6 ++ .../pages/organization/Organizationcopy.tsx | 57 ------------------- 2 files changed, 6 insertions(+), 57 deletions(-) diff --git a/backend/app.py b/backend/app.py index 8f779429..75a0b190 100644 --- a/backend/app.py +++ b/backend/app.py @@ -25,6 +25,12 @@ from urllib.parse import unquote import uuid +# Suppress Azure SDK logs (including Key Vault calls) +logging.getLogger('azure').setLevel(logging.WARNING) +logging.getLogger('azure.identity').setLevel(logging.WARNING) +logging.getLogger('azure.keyvault').setLevel(logging.WARNING) +logging.getLogger('azure.core').setLevel(logging.WARNING) + from identity.flask import Auth from datetime import timedelta, datetime diff --git a/frontend/src/pages/organization/Organizationcopy.tsx b/frontend/src/pages/organization/Organizationcopy.tsx index 77bd243f..a9665d3a 100644 --- a/frontend/src/pages/organization/Organizationcopy.tsx +++ b/frontend/src/pages/organization/Organizationcopy.tsx @@ -353,63 +353,6 @@ const Organization = () => {
    -
    -
    - Web Indexing -
    -
    -
    -
    - -
    -
    - setCurrentUrl(e.target.value)} - onKeyDown={handleKeyDown} - /> - -
    -
    - {urlsToScrape.length > 0 && ( -
    - {urlsToScrape.map((url, index) => ( -
    - {url} - -
    - ))} -
    - )} -
    - -
    -
    -
    From 71ef2d64702bac3ada0959edd4d86d6eff66650b Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Tue, 1 Jul 2025 10:13:04 -0400 Subject: [PATCH 574/820] FA-678 Inconsistent row count in recent changes table subscription management (#474) * * Remove desktop view paging and fix the endpoint so that it only brings the 10 most recent changes. * * Fix visual issues with the table --- .../SubscriptionManagementcopy.module.css | 24 +++++++++++++++---- .../SubscriptionManagementcopy.tsx | 14 +++++------ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.module.css b/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.module.css index 50e5761f..5f020a59 100644 --- a/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.module.css +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.module.css @@ -93,8 +93,9 @@ } .tableText, +.tableText2, .tableStatus { - text-align: justify; + text-align: left; font-size: 1rem; font-weight: 500; padding: 1rem 1.5rem; @@ -103,7 +104,8 @@ @media (max-width: 900px) { .tableText, - .tableStatus { + .tableStatus, + .tableText2 { font-size: 0.7rem; } } @@ -260,12 +262,25 @@ .auditBody {} .auditFilter { - margin-top: 20px; display: flex; gap: 10px; width: auto; } +@media (max-width: 1133px) { + + .thead, + .tableDate, + .tableText2, + .tableStatus { + font-size: 0.8rem; + } + + .titleRecent { + font-size: 1rem; + } +} + /* Modal Styles */ .modalAudit { position: absolute; @@ -290,11 +305,10 @@ @media (max-width: 700px) { .modalAudit { - width: 98vw; min-width: unset; left: 50%; top: 55%; - padding: 10px; + padding: 40px; border-radius: 8px; font-size: 0.95rem; box-sizing: border-box; diff --git a/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.tsx b/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.tsx index bfc4db79..7dbc3995 100644 --- a/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.tsx +++ b/frontend/src/pages/subscriptionmanagement/SubscriptionManagementcopy.tsx @@ -266,7 +266,7 @@ const SubscriptionManagement: React.FC = () => { useEffect(() => { const handleResize = () => { - setIsMobile(window.innerWidth <= 700); + setIsMobile(window.innerWidth <= 957); setMobilePage(1); }; window.addEventListener("resize", handleResize); @@ -416,9 +416,9 @@ const SubscriptionManagement: React.FC = () => { {data.action === "Subscription Tier Change" && ( <> {formatTimestamp(data._ts)} - Subscription Tier Change - {data.modified_by_name} - + Subscription Tier Change + {data.modified_by_name} + {data.previous_plan} → {data.current_plan} @@ -426,15 +426,15 @@ const SubscriptionManagement: React.FC = () => { {data.action === "Financial Assistant Change" && ( <> {formatTimestamp(data._ts)} - FA Add-On Toggled - {data.modified_by_name} + FA Add-On Toggled + {data.modified_by_name} Status: {data.status_financial_assistant} )} {data.action === "Subscription Created" && ( <> {formatTimestamp(data._ts)} - Subscription Created + Subscription Created {data.modified_by_name} Status: Active From 8c0691c697b51750bc6868ef6b1b96e753fd9e80 Mon Sep 17 00:00:00 2001 From: "Edgar Z. Dagger" <78937829+egdagger@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:19:49 -0400 Subject: [PATCH 575/820] FA-685 Improving the space between the controls in team management page ui (#476) --- frontend/src/pages/admin/Admincopy.module.css | 442 +++++++++--------- frontend/src/pages/admin/Admincopy.tsx | 11 +- 2 files changed, 224 insertions(+), 229 deletions(-) diff --git a/frontend/src/pages/admin/Admincopy.module.css b/frontend/src/pages/admin/Admincopy.module.css index ca91d45e..541ae56d 100644 --- a/frontend/src/pages/admin/Admincopy.module.css +++ b/frontend/src/pages/admin/Admincopy.module.css @@ -35,8 +35,6 @@ margin-left: 20px; } - - .closeButton { position: fixed; top: 0; @@ -78,10 +76,10 @@ width: 100%; } -.tableTitle{ +.tableTitle { padding: 10px; font-size: 1rem; - font-weight: 600 + font-weight: 600; } @media (max-width: 430px) { .tableContainer { @@ -96,11 +94,11 @@ gap: 8px; } - .tableContainer li>div { + .tableContainer li > div { width: 100% !important; } - .tableContainer li>div:first-child { + .tableContainer li > div:first-child { margin-bottom: 8px; } @@ -121,7 +119,8 @@ .rolePlatformAdmin { font-size: 12px !important; padding: 2px 6px !important; - } } + } +} .tableContainer { border-radius: 10px; overflow: hidden; @@ -130,7 +129,6 @@ border: 1px solid #d9d9d9; } - .button { background: none; justify-content: center; @@ -148,8 +146,9 @@ } .dialogFont { - font-family: 'Segoe UI', Arial, sans-serif; - font-size: 16px; } + font-family: "Segoe UI", Arial, sans-serif; + font-size: 16px; +} .addIcon2 { font-size: 15px; @@ -160,7 +159,6 @@ margin-right: 5px; } - @media (max-width: 768px) { .button { width: 100%; @@ -170,7 +168,7 @@ } .option { - font-size: 12px; + font-size: 10px; } .title { @@ -210,7 +208,6 @@ .modalTitle { margin-bottom: 40px; text-align: center; - } @media (max-width: 900px) { @@ -220,7 +217,6 @@ } .modalButtonContainer { - gap: 50px; } @@ -278,7 +274,6 @@ color: #1e40af; } - .dropdown { display: inline-flex; justify-content: center; @@ -331,21 +326,18 @@ } .responsiveSearch { - max-width: 300px; width: 100%; } - @media (max-width: 768px) { .responsiveSearch { - max-width: 260px; + width: 100%; } } - @media (max-width: 495px) { .responsiveSearch { - max-width: 140px; + width: 100%; } } @@ -361,9 +353,9 @@ border-radius: 0.5rem; transition: background-color 0.2s; width: 145px; + margin-left: "12px"; } - @media (max-width: 768px) { .responsiveButton { font-size: 14px; @@ -405,10 +397,11 @@ display: inline; } -@media (max-width: 405px) { +@media (max-width: 500px) { .hideOnMobile { display: none !important; - } } + } +} .bothIcons { display: flex; @@ -419,16 +412,15 @@ height: 18px; } -.bothIcons:hover{ +.bothIcons:hover { color: #364153; } -.buttonText{ +.buttonText { font-size: 1rem; font-weight: 600; } - .filterButton { display: flex; align-items: center; @@ -440,6 +432,7 @@ cursor: pointer; gap: 6px; transition: background 0.2s; + margin-left: 10px; } .filterButton:hover { @@ -468,7 +461,7 @@ background: #f3f4f6; } -.overlayD{ +.overlayD { position: fixed; top: 0; left: 0; @@ -492,7 +485,7 @@ position: relative; } -.spinnerOverlayD{ +.spinnerOverlayD { position: absolute; top: 50%; left: 50%; @@ -500,7 +493,7 @@ z-index: 9999; } -.spinnerD{ +.spinnerD { width: 50px; height: 50px; border: 4px solid #f3f4f6; @@ -519,21 +512,21 @@ } } -.headerD{ +.headerD { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem; } -.titleD{ +.titleD { font-size: 1rem; font-weight: 600; color: #111827; margin: 0; } -.closeButtonD{ +.closeButtonD { color: #9ca3af; background: none; border: none; @@ -546,25 +539,25 @@ color: #4b5563; } -.contentD{ +.contentD { padding: 1.5rem; } -.messageD{ +.messageD { color: #6b7280; margin: 0 0 1.5rem 0; line-height: 1.5; font-size: 1rem; } -.buttonContainerD{ +.buttonContainerD { display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 20px; } -.cancelButtonD{ +.cancelButtonD { padding: 0.5rem 1.5rem; border: 1px solid #d1d5db; border-radius: 6px; @@ -601,191 +594,192 @@ .deleteButtonD:disabled { opacity: 0.5; - cursor: not-allowed; } - - - .overlayE{ - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - z-index: 50; - } - - .dialogE{ - background-color: white; - border-radius: 8px; - box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); - max-width: 500px; - width: 100%; - margin: 0 1rem; - position: relative; - } - - .headerE{ - background-color: #16a34a; - color: white; - display: flex; - align-items: center; - justify-content: space-between; - padding: 1.5rem; - border-top-left-radius: 8px; - border-top-right-radius: 8px; - } - - .titleE{ - font-size: 1.2rem; - font-weight: 600; - margin: 0; - } - - .closeButtonE{ - color: white; - background: none; - border: none; - cursor: pointer; - padding: 0; - transition: opacity 0.2s ease; - } - - .closeButtonE:hover { - opacity: 0.8; - } - - .contentE{ - padding: 2rem; - } - - .fieldGroupE{ - margin-bottom: 1.5rem; - } - - .labelE{ - display: block; - font-weight: 600; - color: #374151; - margin-bottom: 0.5rem; - font-size: 1rem; - } - - .inputE{ - width: 100%; - padding: 0.75rem; - border: 1px solid #d1d5db; - border-radius: 6px; - font-size: 0.875rem; - background-color: white; - transition: border-color 0.2s ease; - box-sizing: border-box; - } - - .inputE:focus { - outline: none; - border-color: #16a34a; - box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1); - } - - .inputE:disabled { - background-color: #f9fafb; - color: #6b7280; - cursor: not-allowed; - } - - .dropdownE{ - width: 100%; - padding: 0.75rem; - border: 1px solid #d1d5db; - border-radius: 6px; - font-size: 0.875rem; - background-color: white; - cursor: pointer; - transition: border-color 0.2s ease; - box-sizing: border-box; - } - - .dropdownE:focus { - outline: none; - border-color: #16a34a; - box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1); - } - - .helpTextE{ - display: block; - font-size: 0.75rem; - color: #6b7280; - margin-top: 0.25rem; - } - - .passwordSectionE{ - margin-bottom: 2rem; - text-align: center; - } - - .changePasswordButtonE{ - background-color: #16a34a; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - font-weight: 500; - font-size: 0.875rem; - cursor: pointer; - transition: background-color 0.2s ease; - } - - .changePasswordButtonE:hover { - background-color: #15803d; - } - - .errorMessageE{ - color: #dc2626; - font-size: 0.875rem; - margin-bottom: 1rem; - padding: 0.5rem; - background-color: #fef2f2; - border: 1px solid #fecaca; - border-radius: 4px; - } - - .buttonContainerE{ - display: flex; - gap: 0.75rem; - justify-content: flex-end; - } - - .cancelButtonE{ - padding: 0.5rem 1rem; - border: 1px solid #d1d5db; - border-radius: 6px; - color: #374151; - background-color: white; - font-weight: 500; - font-size: 0.875rem; - cursor: pointer; - transition: background-color 0.2s ease; - } - - .cancelButtonE:hover { - background-color: #f9fafb; - } - - .saveButtonE{ - padding: 0.5rem 1rem; - background-color: #16a34a; - color: white; - border: none; - border-radius: 6px; - font-weight: 500; - font-size: 0.875rem; - cursor: pointer; - transition: background-color 0.2s ease; - } - - .saveButtonE:hover { - background-color: #15803d; } \ No newline at end of file + cursor: not-allowed; +} + +.overlayE { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + z-index: 50; +} + +.dialogE { + background-color: white; + border-radius: 8px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + max-width: 500px; + width: 100%; + margin: 0 1rem; + position: relative; +} + +.headerE { + background-color: #16a34a; + color: white; + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.titleE { + font-size: 1.2rem; + font-weight: 600; + margin: 0; +} + +.closeButtonE { + color: white; + background: none; + border: none; + cursor: pointer; + padding: 0; + transition: opacity 0.2s ease; +} + +.closeButtonE:hover { + opacity: 0.8; +} + +.contentE { + padding: 2rem; +} + +.fieldGroupE { + margin-bottom: 1.5rem; +} + +.labelE { + display: block; + font-weight: 600; + color: #374151; + margin-bottom: 0.5rem; + font-size: 1rem; +} + +.inputE { + width: 100%; + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.875rem; + background-color: white; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.inputE:focus { + outline: none; + border-color: #16a34a; + box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1); +} + +.inputE:disabled { + background-color: #f9fafb; + color: #6b7280; + cursor: not-allowed; +} + +.dropdownE { + width: 100%; + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 0.875rem; + background-color: white; + cursor: pointer; + transition: border-color 0.2s ease; + box-sizing: border-box; +} + +.dropdownE:focus { + outline: none; + border-color: #16a34a; + box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1); +} + +.helpTextE { + display: block; + font-size: 0.75rem; + color: #6b7280; + margin-top: 0.25rem; +} + +.passwordSectionE { + margin-bottom: 2rem; + text-align: center; +} + +.changePasswordButtonE { + background-color: #16a34a; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 6px; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.changePasswordButtonE:hover { + background-color: #15803d; +} + +.errorMessageE { + color: #dc2626; + font-size: 0.875rem; + margin-bottom: 1rem; + padding: 0.5rem; + background-color: #fef2f2; + border: 1px solid #fecaca; + border-radius: 4px; +} + +.buttonContainerE { + display: flex; + gap: 0.75rem; + justify-content: flex-end; +} + +.cancelButtonE { + padding: 0.5rem 1rem; + border: 1px solid #d1d5db; + border-radius: 6px; + color: #374151; + background-color: white; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.cancelButtonE:hover { + background-color: #f9fafb; +} + +.saveButtonE { + padding: 0.5rem 1rem; + background-color: #16a34a; + color: white; + border: none; + border-radius: 6px; + font-weight: 500; + font-size: 0.875rem; + cursor: pointer; + transition: background-color 0.2s ease; +} + +.saveButtonE:hover { + background-color: #15803d; +} diff --git a/frontend/src/pages/admin/Admincopy.tsx b/frontend/src/pages/admin/Admincopy.tsx index eb97a795..8e8e1713 100644 --- a/frontend/src/pages/admin/Admincopy.tsx +++ b/frontend/src/pages/admin/Admincopy.tsx @@ -684,11 +684,12 @@ const Admin = () => { flexDirection: "row", justifyContent: "space-between", alignItems: "center", - marginBottom: "1.5rem" + marginBottom: "1.5rem", + gap: "5px" }} > -
    -
    +
    +
    { paddingBottom: "1px" }} > - + { justifyContent: "center" }} > - + ) : null } From f49e9ad336810f361f5da1628a530e42b1a927bd Mon Sep 17 00:00:00 2001 From: jonnykate <146135735+jonnykate@users.noreply.github.com> Date: Tue, 1 Jul 2025 23:42:28 -0400 Subject: [PATCH 576/820] FA-704-Scrollbar-Disappears-Knowledge-Source-Page (#477) * Removed overflow for scrollbar * Adjust padding and margin for scrolling --- .../KnowledgeSources.module.css | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/knowledgesources/KnowledgeSources.module.css b/frontend/src/pages/knowledgesources/KnowledgeSources.module.css index cb400484..118085ff 100644 --- a/frontend/src/pages/knowledgesources/KnowledgeSources.module.css +++ b/frontend/src/pages/knowledgesources/KnowledgeSources.module.css @@ -3,14 +3,14 @@ display: flex; flex-direction: column; width: 100%; - padding: 100px 40px 100px 80px; + padding: 100px 40px 200px 80px; background-color: #f9fafb; min-height: 100vh; } @media (max-width: 900px) { .pageContainer { - padding: 100px 16px 100px 50px; + padding: 100px 16px 200px 50px; } } @@ -259,7 +259,6 @@ background-color: white; border-radius: 8px; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); - overflow: hidden; } /* Card Header */ @@ -285,10 +284,10 @@ .cardsContainer { background-color: white; border-radius: 0 0 8px 8px; - max-height: min(600px, 60vh); /* Responsive height: smaller of 600px or 60% of viewport height */ + max-height: min(400px, 40vh); /* Responsive height: smaller of 600px or 60% of viewport height */ overflow-y: auto; /* Enable vertical scrolling */ scroll-behavior: smooth; /* Smooth scrolling animation */ - padding-bottom: 16px; /* Add bottom padding to ensure last item is fully visible */ + /* Add bottom padding to ensure last item is fully visible */ } /* Custom scrollbar styling */ @@ -320,7 +319,7 @@ .card:last-child { border-bottom: none; - margin-bottom: 42px; /* More space for desktop */ + margin-bottom: 16px; /* More space for desktop */ } .card:hover { @@ -481,7 +480,7 @@ @media (max-width: 768px) { .pageContainer { - padding: 100px 16px 100px 16px; + padding: 100px 16px 200px 16px; } .addUrlSection { @@ -537,14 +536,12 @@ gap: 8px; } - .card:last-child { - margin-bottom: 24px; /* Medium space for tablets */ - } + } @media (max-width: 480px) { .pageContainer { - padding: 80px 12px 80px 12px; + padding: 80px 12px 180px 12px; } .addUrlSection { @@ -601,9 +598,7 @@ gap: 6px; } - .card:last-child { - margin-bottom: 16px; /* Less space for small mobile devices */ - } + } /* Edit form styles */ From 2968638af84eee36dc8a23a1244076b1b23fcd13 Mon Sep 17 00:00:00 2001 From: ramirezmorac2 Date: Thu, 3 Jul 2025 08:56:52 -0400 Subject: [PATCH 577/820] * Delete all old files (#478) --- frontend/src/App.tsx | 11 - frontend/src/components/Navbar/NavBar.tsx | 161 ---- .../src/components/Navbar/Navbar.module.css | 116 --- .../ProfilePanel/Profile.module.css | 19 - .../src/components/ProfilePanel/Profile.tsx | 29 - .../QuestionInput/QuestionInput.module.css | 61 -- .../QuestionInput/QuestionInput.tsx | 302 ------- .../src/components/QuestionInput/index.ts | 2 +- .../SettingsPanel/SettingsModal.module.css | 190 ---- .../src/components/SettingsPanel/index.tsx | 305 ------- .../src/components/Sidebar/Sidebar.module.css | 314 ------- frontend/src/components/Sidebar/Sidebar.tsx | 368 -------- .../src/components/Sidebar/SidebarItem.tsx | 71 -- .../components/Sidebar/SidebarItemTypes.ts | 37 - .../components/Sidebar/SidebarSectionTypes.ts | 9 - frontend/src/pages/admin/Admin.module.css | 225 ----- frontend/src/pages/admin/Admin.tsx | 809 ------------------ frontend/src/pages/chat/Chat.module.css | 272 ------ frontend/src/pages/chat/Chat.tsx | 801 ----------------- .../FinancialAssistant.module.css | 94 -- .../financialassistant/FinancialAssistant.tsx | 149 ---- .../pages/invitations/Invitations.module.css | 89 -- .../src/pages/invitations/Invitations.tsx | 183 ---- frontend/src/pages/layout/Layout.module.css | 117 --- frontend/src/pages/layout/Layout.tsx | 75 -- frontend/src/pages/layout/_Layout.module.css | 78 -- frontend/src/pages/layout/_Layout.tsx | 37 - .../organization/Organization.module.css | 122 --- .../src/pages/organization/Organization.tsx | 179 ---- .../resources/UploadResources.module.css | 220 ----- .../src/pages/resources/UploadResources.tsx | 522 ----------- .../SubscriptionManagement.module.css | 477 ----------- .../SubscriptionManagement.tsx | 525 ------------ 33 files changed, 1 insertion(+), 6968 deletions(-) delete mode 100644 frontend/src/components/Navbar/NavBar.tsx delete mode 100644 frontend/src/components/Navbar/Navbar.module.css delete mode 100644 frontend/src/components/ProfilePanel/Profile.module.css delete mode 100644 frontend/src/components/ProfilePanel/Profile.tsx delete mode 100644 frontend/src/components/QuestionInput/QuestionInput.module.css delete mode 100644 frontend/src/components/QuestionInput/QuestionInput.tsx delete mode 100644 frontend/src/components/SettingsPanel/SettingsModal.module.css delete mode 100644 frontend/src/components/SettingsPanel/index.tsx delete mode 100644 frontend/src/components/Sidebar/Sidebar.module.css delete mode 100644 frontend/src/components/Sidebar/Sidebar.tsx delete mode 100644 frontend/src/components/Sidebar/SidebarItem.tsx delete mode 100644 frontend/src/components/Sidebar/SidebarItemTypes.ts delete mode 100644 frontend/src/components/Sidebar/SidebarSectionTypes.ts delete mode 100644 frontend/src/pages/admin/Admin.module.css delete mode 100644 frontend/src/pages/admin/Admin.tsx delete mode 100644 frontend/src/pages/chat/Chat.module.css delete mode 100644 frontend/src/pages/chat/Chat.tsx delete mode 100644 frontend/src/pages/financialassistant/FinancialAssistant.module.css delete mode 100644 frontend/src/pages/financialassistant/FinancialAssistant.tsx delete mode 100644 frontend/src/pages/invitations/Invitations.module.css delete mode 100644 frontend/src/pages/invitations/Invitations.tsx delete mode 100644 frontend/src/pages/layout/Layout.module.css delete mode 100644 frontend/src/pages/layout/Layout.tsx delete mode 100644 frontend/src/pages/layout/_Layout.module.css delete mode 100644 frontend/src/pages/layout/_Layout.tsx delete mode 100644 frontend/src/pages/organization/Organization.module.css delete mode 100644 frontend/src/pages/organization/Organization.tsx delete mode 100644 frontend/src/pages/resources/UploadResources.module.css delete mode 100644 frontend/src/pages/resources/UploadResources.tsx delete mode 100644 frontend/src/pages/subscriptionmanagement/SubscriptionManagement.module.css delete mode 100644 frontend/src/pages/subscriptionmanagement/SubscriptionManagement.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 82dd56d7..275d4ab0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,23 +1,13 @@ import { Routes, Route } from "react-router-dom"; import ProtectedRoute from "./router/ProtectedRoute"; -// import Layout from "./pages/layout/_Layout"; import NoPage from "./pages/NoPage"; import AccessDenied from "./pages/AccesDenied"; -// import Chat from "./pages/chat/Chat"; -// import Admin from "./pages/admin/Admin"; import Onboarding from "./pages/onboarding/Onboarding"; -import Invitations from "./pages/invitations/Invitations"; -// import Organization from "./pages/organization/Organization"; import HelpCenter from "./pages/helpcenter/HelpCenter"; -// import UploadResources from "./pages/resources/UploadResources"; import RequestStudies from "./pages/studies/RequestStudies"; import ReportManagement from "./pages/reports/ReportManagement"; -// import Reports from "./pages/reports/Reports"; -// import DistributionLists from "./pages/reports/DistributionLists"; import Logout from "./pages/logout/Logout"; import Notifications from "./pages/notifications/Notifications"; -// import SubscriptionManagement from "./pages/subscriptionmanagement/SubscriptionManagement"; - import UserManagement from "./pages/usermanagement/UserManagement"; import { PaymentGateway } from "./components/PaymentGateway/PaymentGateway"; import SuccessPayment from "./components/PaymentGateway/SuccessPayment"; @@ -118,7 +108,6 @@ export default function App() { > }> } /> - } /> } /> } /> } /> diff --git a/frontend/src/components/Navbar/NavBar.tsx b/frontend/src/components/Navbar/NavBar.tsx deleted file mode 100644 index f9acffa8..00000000 --- a/frontend/src/components/Navbar/NavBar.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import React, { useState } from "react"; -import styles from "./Navbar.module.css"; -import { IconMenu2, IconMessageCircle, IconHistory, IconSettings, IconAppsFilled } from "@tabler/icons-react"; -import { useAppContext } from "../../providers/AppProviders"; -import { Link, useLocation } from "react-router-dom"; -import { ProfilePanel } from "../ProfilePanel/Profile"; - -interface NavbarProps { - isCollapsed: boolean; - setIsCollapsed: React.Dispatch>; -} -function persistFinancialAssistantState(userId: string | undefined, state: boolean) { - localStorage.setItem(`financialAssistantActive_${userId}`, JSON.stringify(state)); -} - -const Navbar: React.FC = ({ isCollapsed, setIsCollapsed }) => { - const { - showHistoryPanel, - setShowHistoryPanel, - showFeedbackRatingPanel, - setShowFeedbackRatingPanel, - settingsPanel, - setSettingsPanel, - user, - subscriptionTiers, - isFinancialAssistantActive, - setIsFinancialAssistantActive - } = useAppContext(); - const historyContent = showHistoryPanel ? "Hide chat history" : "Show chat history"; - const feedbackContent = showFeedbackRatingPanel ? "Hide feedback panel" : "Show feedback panel"; - const userName = user?.name || ""; - const subscriptiontype = subscriptionTiers || " "; - const location = useLocation().pathname; - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - const fastatus = subscriptiontype.includes("Basic + Financial Assistant") - ? true - : false || subscriptiontype.includes("Premium + Financial Assistant") || subscriptiontype.includes("Custom + Financial Assistant"); - - const handleShowHistoryPanel = () => { - setShowHistoryPanel(!showHistoryPanel); - setShowFeedbackRatingPanel(false); - setSettingsPanel(false); - setIsDropdownOpen(false); - setIsCollapsed(true); - }; - - const handleShowFeedbackRatingPanel = () => { - setShowFeedbackRatingPanel(!showFeedbackRatingPanel); - setSettingsPanel(false); - setShowHistoryPanel(false); - setIsDropdownOpen(false); - setIsCollapsed(true); - }; - - const handleShowSettings = () => { - setSettingsPanel(!settingsPanel); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - setIsDropdownOpen(false); - setIsCollapsed(true); - }; - - const handleOnClickShowSidebar = () => { - setIsCollapsed(!isCollapsed); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - setIsDropdownOpen(false); - setSettingsPanel(false); - }; - - const handleOnClickProfileCard = () => { - setIsDropdownOpen(!isDropdownOpen); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - setSettingsPanel(false); - setIsCollapsed(true); - }; - - const handleFinancialAgent = () => { - const newState = !isFinancialAssistantActive; - setIsFinancialAssistantActive(newState); - persistFinancialAssistantState(user?.id, newState); - }; - - return ( - - ); -}; - -export default Navbar; diff --git a/frontend/src/components/Navbar/Navbar.module.css b/frontend/src/components/Navbar/Navbar.module.css deleted file mode 100644 index fb59195d..00000000 --- a/frontend/src/components/Navbar/Navbar.module.css +++ /dev/null @@ -1,116 +0,0 @@ -/* Icon Styles */ -.iconLarge { - font-size: 1.9em; - color: #a7c444; -} - -/* Profile Card Styles */ -.profileCard { - display: flex; - align-items: center; - padding: 8px 12px; - background-color: #dbe3ec8a; - border-radius: 8px; -} - -.userDetails { - line-height: 1.2; - margin-right: 5px; -} - -.userName { - font-size: 1em; - font-weight: 600; - color: #333; - margin: 0; -} - -.financialToggleText{ - font-size: 0.9rem; - font-weight: 410; - color: #000000; -} - -.financialToggle[type="checkbox"]{ - padding-top: 13%; - border-color: #85a717; -} - -.financialToggle[type="checkbox"]:checked { - background-color: #85a717; - border-color: #85a717; -} - -.financialToggle[type="checkbox"]:focus { - outline: none; - box-shadow: 0 0 0 0.2rem #aad12c; -} - - -/* Responsive User Details */ -@media (min-width: 577px) { - .userDetails { - display: flex; - flex-direction: column; - } - -} - -@media (max-width: 900px) { - .userDetails { - display: none; - } - .financialToggleText{ - display: none; - } -} - - - -/* Sidebartoggler */ -.sidebartoggler { - /* Define styles if needed */ -} - -/* Message Body */ -.messageBody { - /* Define styles if needed */ -} - -.headerNavbar { - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - display: flex; - justify-content: center; - align-items: center; - gap: 15px; -} - -@media (max-width: 991.98px) { - .headerNavbar { - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - display: flex; - justify-content: center; - align-items: center; - gap: 15px; - } - .profileCard{ - background-color:rgb(248, 249, 250); - } - .headerNavbarAlt{ - -ms-flex-wrap: nowrap !important; - flex-wrap: nowrap !important; - display: flex; - gap: 68%; - } -} - -.appHeader{ - position: fixed; - z-index: 10; - width: 100%; - background-color: var(--bs-tertiary-bg); - padding: 0 25px; - top: 0; -} \ No newline at end of file diff --git a/frontend/src/components/ProfilePanel/Profile.module.css b/frontend/src/components/ProfilePanel/Profile.module.css deleted file mode 100644 index 77bd11c4..00000000 --- a/frontend/src/components/ProfilePanel/Profile.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.messageBody { - position: fixed; - top: 110px; - right: 20px; - width: auto; - max-width: 300px; - height: auto; - display: flex; - flex-direction: column; - justify-content: flex-end; - align-items: flex-start; - z-index: 1000; - padding: 20px; - gap: 10px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 6px; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); -} \ No newline at end of file diff --git a/frontend/src/components/ProfilePanel/Profile.tsx b/frontend/src/components/ProfilePanel/Profile.tsx deleted file mode 100644 index d38c7a1d..00000000 --- a/frontend/src/components/ProfilePanel/Profile.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IconListCheck, IconMail, IconUser } from "@tabler/icons-react"; -import React from "react"; -import styles from "./Profile.module.css" -import { Link } from "react-router-dom"; - -export const ProfilePanel = () => { - - return ( - -
    - - -

    My Profile

    - - - -

    My Account

    - - - -

    My Task

    - - - Logout - -
    - - ); -}; \ No newline at end of file diff --git a/frontend/src/components/QuestionInput/QuestionInput.module.css b/frontend/src/components/QuestionInput/QuestionInput.module.css deleted file mode 100644 index 38919d05..00000000 --- a/frontend/src/components/QuestionInput/QuestionInput.module.css +++ /dev/null @@ -1,61 +0,0 @@ -.questionInputContainer { - display: flex; - flex-direction: column; - width: 100%; - background: transparent; - position: relative; - max-height: 200px; -} - -.attachmentContainer { - height: "100%"; - justify-content: "center"; - align-items: center; - display: flex; -} - -.questionInputTextArea { - width: 100%; - line-height: 40px; - resize: none; - padding-bottom: 5px; - flex-grow: 1; - min-height: 40px; - max-height: 150px; - overflow-y: auto; - overflow-x: hidden; -} - -.questionInputButtonsContainer { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 1px; - position: sticky; - bottom: 0; - background-color: white; - width: 100%; - z-index: 1; -} - -.questionInputSendButton { - cursor: pointer; -} - -.questionInputSendButtonDisabled { - opacity: 0.4; -} - -.attachmentButton { - transform: rotate(-135deg) scaleX(-1) translate(-5px, 0px); - cursor: pointer; - font-size: 24px; - color: #9f9c9c; -} - -.leftButtons { - display: flex; - flex-direction: row; - align-items: center; -} diff --git a/frontend/src/components/QuestionInput/QuestionInput.tsx b/frontend/src/components/QuestionInput/QuestionInput.tsx deleted file mode 100644 index cdd26228..00000000 --- a/frontend/src/components/QuestionInput/QuestionInput.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import { useState, useContext } from "react"; -import { useAppContext } from "../../providers/AppProviders"; -import { Stack, Spinner, TextField, IconButton } from "@fluentui/react"; -import { getTokenOrRefresh } from "./token_util"; -import { Send24Filled, Mic24Regular, AttachRegular, AddRegular, BroomRegular } from "@fluentui/react-icons"; -import { ResultReason, SpeechConfig, AudioConfig, SpeechRecognizer } from "microsoft-cognitiveservices-speech-sdk"; - -import { uploadFile } from "../../api"; - -import styles from "./QuestionInput.module.css"; -interface Props { - onSend: (question: string, fileBlobUrl: string | null) => void; - disabled: boolean; - placeholder?: string; - clearOnSend?: boolean; - extraButtonNewChat?: React.ReactNode; -} - -import { useFilePicker } from "use-file-picker"; - -export const FileAttachmentInput = ({ setFileBlobUrl }: { setFileBlobUrl: (url: string) => void }) => { - const [files, setFiles] = useState([]); - const [loadingFiles, setLoadingFiles] = useState(false); - const [error, setError] = useState(""); - - const { openFilePicker, filesContent, loading, errors } = useFilePicker({ - readAs: "DataURL", - accept: ["xls", "xlsx", "csv"], - multiple: false, - onFilesSelected: async ({ plainFiles, filesContent, errors }) => { - // this callback is always called, even if there are errors - setLoadingFiles(true); - try { - const data = await uploadFile(plainFiles[0]); - setLoadingFiles(false); - setError(""); - setFileBlobUrl(data.blob_url); - } catch (error) { - setLoadingFiles(false); - setError("Error uploading file"); - return; - } - }, - onFilesRejected: ({ errors }) => { - // this callback is called when there were validation errors - setError("Error with the file picker"); - setLoadingFiles(false); - }, - onFilesSuccessfullySelected: async ({ plainFiles, filesContent }) => { - // this callback is called when the files are successfully selected - setFiles(plainFiles); - } - }); - - if (loading) { - return ( -
    - -
    - ); - } - - if (errors.length) { - return ( -
    -
    Error with the file picker
    -
    - ); - } - - return ( - <> -
    - {files.map((file, index) => ( -
    - - -
    {file.name}
    -
    - ))} - {error &&
    {error}
    } - {loadingFiles && !error && ( -
    - Loading file... - -
    - )} -
    -
    -
    - -
    - - ); -}; - -export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend, extraButtonNewChat }: Props) => { - const { organization } = useAppContext(); - const [question, setQuestion] = useState(""); - - const [fileBlobUrl, setFileBlobUrl] = useState(null); - - const sendQuestion = () => { - if ( - disabled || - !question.trim() || - !organization || // Check if organization is null or undefined - organization.subscriptionStatus === "inactive" || - !organization.subscriptionId - ) { - return; - } - - onSend(question, fileBlobUrl); - - if (clearOnSend) { - setQuestion(""); - } - }; - - const sttFromMic = async () => { - const tokenObj = await getTokenOrRefresh(); - const speechConfig = SpeechConfig.fromAuthorizationToken(tokenObj.authToken, tokenObj.region); - speechConfig.speechRecognitionLanguage = tokenObj.speechRecognitionLanguage; - - const audioConfig = AudioConfig.fromDefaultMicrophoneInput(); - const recognizer = new SpeechRecognizer(speechConfig, audioConfig); - - const userLanguage = navigator.language; - let reiniciar_text = ""; - if (userLanguage.startsWith("pt")) { - reiniciar_text = "Pode falar usando seu microfone..."; - } else if (userLanguage.startsWith("es")) { - reiniciar_text = "Puedes hablar usando su micrófono..."; - } else { - reiniciar_text = "You can talk using your microphone..."; - } - - setQuestion(reiniciar_text); - - recognizer.recognizeOnceAsync(result => { - let displayText; - if (result.reason === ResultReason.RecognizedSpeech) { - displayText = result.text; - //setQuestion(displayText); - //onSend(question); - } else { - displayText = "ERROR: Voice recognition was canceled or the voice cannot be recognized. Make sure your microphone is working properly."; - //setQuestion(displayText); - } - setQuestion(displayText); - }); - }; - - const onEnterPress = (ev: React.KeyboardEvent) => { - if (ev.key === "Enter" && !ev.shiftKey) { - ev.preventDefault(); - sendQuestion(); - } - }; - - const onQuestionChange = (_ev: React.FormEvent, newValue?: string) => { - if (!newValue) { - setQuestion(""); - } else { - setQuestion(newValue); - } - }; - - const sendQuestionDisabled = - disabled || - !question.trim() || - !organization || // Check if organization is null - organization.subscriptionStatus === "inactive" || - !organization.subscriptionId; - - return ( - - -
    - {extraButtonNewChat} -
    - {/* -
    { - if (ev.key === "Enter") { - ev.preventDefault(); - sttFromMic(); - } - }} - tabIndex={0} - > - -
    - */} -
    { - if (ev.key === "Enter") { - ev.preventDefault(); - sendQuestion(); - } - }} - tabIndex={0} - > - -
    -
    -
    -
    - ); -}; diff --git a/frontend/src/components/QuestionInput/index.ts b/frontend/src/components/QuestionInput/index.ts index 964f611f..37f576a4 100644 --- a/frontend/src/components/QuestionInput/index.ts +++ b/frontend/src/components/QuestionInput/index.ts @@ -1 +1 @@ -export * from "./QuestionInput"; +export * from "./QuestionInputcopy"; diff --git a/frontend/src/components/SettingsPanel/SettingsModal.module.css b/frontend/src/components/SettingsPanel/SettingsModal.module.css deleted file mode 100644 index 55f57953..00000000 --- a/frontend/src/components/SettingsPanel/SettingsModal.module.css +++ /dev/null @@ -1,190 +0,0 @@ -.button { - display: flex; - align-items: center; - margin-top: 5px; - gap: 6px; - cursor: pointer; - border: 1.5px solid rgb(231, 231, 231); - border-radius: 5px; - padding: 4px 12px; - transition: 0.2s; - background-color: white; - font-size: 24px; - width: 100%; - height: 35px; -} - -.overlay { - position: fixed; - top: 90px; - right: 0; - width: auto; - height: auto; - display: flex; - justify-content: flex-end; - align-items: flex-start; - z-index: 1000; - padding: 20px; -} - -.item { - margin: 15px 0; - display: flex; - justify-content: space-between; - align-items: center; -} -.button:hover { - background-color: rgb(243, 243, 243); -} - -.button:active { - background-color: white; -} - -.buttonText { - font-weight: 500; - padding-left: 10px; -} - -.disabled { - opacity: 0.4; -} - -.answerContainer { - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 6px; - box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); - width: 250px; -} - -.content { - display: flex; - padding: 12px 0px 20px 28px; - flex-direction: column; - align-items: flex-end; - align-self: stretch; -} - -.saveButton { - display: flex; - padding: 10px 24px 8px 24px; - justify-content: center; - align-items: center; - background: #e3e3e3; - border-radius: 4px; - border: 1px solid #9f9c9c; - box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); - font-size: 12px; - cursor: pointer; - text-transform: uppercase; - width: 85%; - font-weight: 500; - margin-top: 20px; -} - -.saveButton:hover { - background: #f5f5f5; -} - -.saveIcon { - font-size: 15px; - margin-right: 5px; -} - -.closeButton { - background: rgb(249, 249, 249); - color: rgb(0, 0, 0); - font-size: 20px; - border: none; - cursor: pointer; - position: absolute; - top: 1rem; - right: 0; -} - -.closeButton:hover { - background: rgb(249, 249, 249); - color: rgb(0, 0, 0, 0.8); -} - -.closeButton:focus { - outline: none; - border: none; -} - -.header2 { - display: flex; - justify-content: space-between; - align-items: center; - position: sticky; - padding: 24px 15px 24px 24px; - border-bottom: 1px solid #e9e9e9; -} - -.modal-title { - font-size: 24px; - font-weight: 500; - padding-left: 10px; - color: red; -} - -.infoIcon { - font-size: 50; - height: 50; - width: 50; - margin: "0 25px"; -} - -.w-100 { - width: 100%; -} - -.header { - display: flex; - justify-content: space-between; - align-items: center; - position: sticky; -} - -.title { - font-weight: 700; - font-size: 18px; - color: #4f4f4f; - line-height: 120%; -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} - -.closeButtonContainer:active { - background-color: rgba(214, 214, 214, 0.315); - border-radius: 5px; -} - -.closeButton2 { - background-color: transparent; - border: none; - font-size: 20px; - color: #726c6c; - cursor: pointer; - transform: rotate(45deg); -} - -.closeButton2:hover { - color: #000; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: scale(0.95); - } - to { - opacity: 1; - transform: scale(1); - } -} \ No newline at end of file diff --git a/frontend/src/components/SettingsPanel/index.tsx b/frontend/src/components/SettingsPanel/index.tsx deleted file mode 100644 index a137cba5..00000000 --- a/frontend/src/components/SettingsPanel/index.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; -import { AddFilled, SaveFilled, ErrorCircleFilled } from "@fluentui/react-icons"; -import { TooltipHost, TooltipDelay, DirectionalHint, DefaultButton, Stack, Spinner, Slider, Dropdown, IDropdownOption } from "@fluentui/react"; -import styles from "./SettingsModal.module.css"; -import { getSettings, postSettings } from "../../api/api"; -import { mergeStyles } from "@fluentui/react/lib/Styling"; -import { useAppContext } from "../../providers/AppProviders"; -import { Dialog, DialogContent, PrimaryButton } from "@fluentui/react"; - -interface Props { - user: { - id: string; - name: string; - } | null; -} - -const iconClass = mergeStyles({ - fontSize: 25, - height: 25, - width: 25, - margin: "0", - padding: "5px 0 0 0px", - cursor: "pointer" -}); - -const itemClass = mergeStyles({ - display: "flex", - alignItems: "center", - justifyContent: "space-between", - padding: "10px 0", - width: "85%" -}); - -const ConfirmationDialog = ({ loading, isOpen, onDismiss, onConfirm }: { loading: boolean; isOpen: boolean; onDismiss: () => void; onConfirm: () => void }) => { - return ( - - ); -}; - -export const SettingsPanel = () => { - const { user, setSettingsPanel, settingsPanel } = useAppContext(); - - const [temperature, setTemperature] = useState("0"); - const [selectedModel, setSelectedModel] = useState("DeepSeek-V3-0324"); - const [loading, setLoading] = useState(true); - const [isLoadingSettings, setIsLoadingSettings] = useState(false); - - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const temperatureDialog = - "It adjusts the balance between creativity and predictability in responses. Lower settings yield straightforward answers, while higher settings introduce originality and diversity, perfect for creative tasks and factual inquiries."; - const modelDialog = "Select the underlying language model for generating responses."; - - const modelOptions: IDropdownOption[] = [ - { key: "DeepSeek-V3-0324", text: "DeepSeek-V3-0324" }, - { key: "gpt-4.1", text: "gpt-4.1" }, - { key: "Claude-4-Sonnet", text: "Claude-4-Sonnet" } - ]; - - useEffect(() => { - const fetchData = async () => { - if (!user) { - // User is not logged in; handle accordingly - setLoading(false); - return; - } - - setLoading(true); - - try { - const data = await getSettings({ - user: { - id: user.id, - name: user.name - } - }); - setTemperature(data.temperature); - setSelectedModel(data.model || "DeepSeek-V3-0324"); - } catch (error) { - console.error("Error fetching settings:", error); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, [user]); - - const handleSubmit = () => { - const parsedTemperature = parseFloat(temperature); - - if (parsedTemperature < 0 || parsedTemperature > 1) { - console.error("Invalid temperature, settings are not submitted."); - return; - } - - postSettings({ - user, - temperature: parsedTemperature, - model: selectedModel, - font_family: "", - font_size: "" - }) - .then(data => { - setTemperature(data.temperature); - setSelectedModel(data.model); - setIsDialogOpen(false); - setIsLoadingSettings(false); - }) - .catch(error => { - console.error("Error saving settings:", error); - setIsLoadingSettings(false); - }); - }; - - const validateValue = (val: any, func: any) => { - if (val.match(/^[0]+$/)) { - func("0"); - return false; - } - - if (val.match(/[^0-9.]/) || val < 0 || val > 1 || val.split(".").length > 2 || val === "1." || val.length > 4) { - return false; - } - - return true; - }; - - const handleSetTemperature = (val: any) => { - const value = String(val); - if (validateValue(value, setTemperature)) { - setTemperature(value); - } - }; - - const onRenderLabel = (dialog: string, title: string) => ( - ( -
    -

    {title}

    - {dialog} -
    - ) - }} - delay={TooltipDelay.zero} - directionalHint={DirectionalHint.bottomCenter} - styles={{ root: { display: "inline-block" } }} - > - -
    - ); - - const handleClosePanel = () => { - setSettingsPanel(false); - }; - - if (!user) { - setLoading(false); - // Display a message or render a different component - return
    Please log in to view your settings.
    ; - } - const panelRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (panelRef.current && !panelRef.current.contains(event.target as Node)) { - setSettingsPanel(false); - } - }; - document.addEventListener("mousedown", handleClickOutside); - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [setSettingsPanel]); - - return ( -
    - { - setIsDialogOpen(false); - }} - onConfirm={() => { - setIsLoadingSettings(true); - handleSubmit(); - }} - /> - - -
    -
    Configuration
    -
    -
    -
    - -
    -
    -
    - {loading ? ( -
    -

    Loading your settings

    - -
    - ) : ( -
    -
    -
    - Creativity Scale -
    - setTemperature(e.toString())} - aria-labelledby="temperature-slider" - /> -
    -
    -
    - Model Selection -
    - { - if (option) { - setSelectedModel(option.key as string); - } - }} - aria-labelledby="model-dropdown" - styles={{ root: { width: "100%" } }} - /> -
    -
    - setIsDialogOpen(true)} aria-label="Save settings"> - -   Save - -
    -
    - )} -
    -
    -
    - ); -}; diff --git a/frontend/src/components/Sidebar/Sidebar.module.css b/frontend/src/components/Sidebar/Sidebar.module.css deleted file mode 100644 index 4103d210..00000000 --- a/frontend/src/components/Sidebar/Sidebar.module.css +++ /dev/null @@ -1,314 +0,0 @@ -/*==================================== -= Sidebar Styles = -====================================*/ -.submenuLink:hover { - - background-color: #ffffff30; - color: #000000; -} - -.sidebarLink:hover, .submenuLink:hover { - color: #000000; -} - -.submenuLink { - display: inline-block; - padding: 4px 8px; - border-radius: 4px; - text-decoration: none; - color: #333; - font-size: 14px; - transition: background-color 0.3s ease; -} -/* Nav Styles */ -.nav { - - width: 100%; - } - - .navUl { - list-style-type: none; - padding-left: 0; - } - - .navLi { - width: 100%; - } - - .navLink { - color: #333; - font-size: 15px; - text-decoration: none; - padding: 10px 30px; - display: flex; - align-items: center; - width: 100%; - text-align: left; - transition: padding 0.3s ease; - } - - .navLinkCollapsed { - justify-content: center; - padding: 10px 0; - width: 85%; - } - - :global(.navLink:hover) { - background-color: #85a717; - } - - - .navLinkActive { - background-color: #f5f5f5; - border-left: 6px solid #0a0b0a; - font-weight: 700; - padding: 10px 25px; - } - - .navLinkActiveCollapsed { - border-left: 6px solid #0a0b0a; - padding: 10px 0; - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; - } - - /* Icon Styles */ - .icon { - color: rgb(63, 63, 63); - font-size: 25px; - margin-right: 15px; - transition: margin-right 0.3s ease; - } - - .iconCollapsed { - margin-right: 0; - } - - /* Collapse Button */ - .collapseButton { - color: rgb(63, 63, 63); - border-right: 2px solid #0a0b0a; - cursor: pointer; - font-size: 22px; - display: flex; - align-items: center; - position: absolute; - bottom: 20px; - right: 10px; - } - - .collapseButtonCollapsed { - position: absolute; - bottom: 20px; - left: 10px; - border-left: 2px solid #0a0b0a; - border-right: none; - } - - /* Sidebar Link */ - /* Change size of the letters*/ - .sidebarLink { - display: flex; - font-size: 14px; - white-space: nowrap; - align-items: center; - line-height: 26px; - position: relative; - padding: 9px 9px; - border-radius: 6px; - gap: 6px; - text-decoration: none; - font-weight: 500; - margin-bottom: 4px; - width: 100%; - cursor: pointer; - user-select: none; - } - - .sidebarLinkIcon { - font-size: 1.5rem; - color: var(--bs-heading-color); - opacity: 0.8; - } - - :global(.sidebarLink:hover) { - color: #000000; - } - - /* CHANGE COLOR PREMIUM TIERS BUTTON*/ - .sidebarLinkActive{ - background-color: #000000; - color: #fff; - box-shadow: 0px 17px 20px -8px rgba(77, 91, 236, 0.23); - cursor: pointer; - user-select: none; - } - - .sidebarLinkActive:hover{ - color: #fff; - box-shadow: 0px 17px 20px -8px rgba(77, 91, 236, 0.23); - } - - .sidebarLinkActiveIcon { - color: #a7c444; - } - - .sidebarLinkActiveI { - color: #A7C444; - } - - /* Nav Small Cap */ - .navSmallCap { - font-size: 12px; - font-weight: 600; - padding: 7px 0; - line-height: 26px; - letter-spacing: 1.5px; - color: #333; - transition: color 0.3s ease; - text-transform: uppercase; - } - - .navSmallCapIcon { - display: none; - } - - /* Submenu Styles */ - .submenu { - max-height: 0; - overflow: hidden; - transition: max-height 0.3s ease, padding 0.3s ease; - padding-left: 20px; - margin: 0; - border-radius: 0.4em; - background-color: #ffffff30; - } - - .submenuActive { - max-height: 200px; /* Adjust as needed */ - } - - .submenuArrow { - margin-left: auto; - transition: transform 0.3s ease; - } - - .submenuArrowActive { - transform: rotate(180deg); - } - - .submenuItem { - border-bottom: 1px solid #33333329; - padding: 4px 8px; - } - - /* Sidebar Divider */ - .sidebarDivider { - height: 0.5px; - display: block; - margin: 20px 0; - background: #00000029; - width: 100%; - - } - - /* Left Sidebar */ - .leftSidebar { - width: 280px; - background-color: #a7c444; - position: absolute; - transition: 0.2s ease-in; - height: 100%; - z-index: 11; - position: fixed; - top: 0; - } - - /* Brand Logo */ - .brandLogo { - min-height: 72px; - padding: 1.5em 50px 1.5em; - - } - - .brandLogoImg { - max-width: 11em; - } - - /* Close Button */ - .closeBtn { - background: none; /* Remove background */ - border: none; /* Replace #000 with your desired border color */ - color: none; /* Text color matching your border color or choose another */ - padding-left: 20px; /* Adjust padding as needed */ - border-radius: 0; /* Optional: for rounded corners */ - cursor: pointer; /* Pointer cursor on hover */ - transition: all 0.3s ease; /* Optional: smooth transition for hover effect */ - } - - /* Sidebar Toggler */ - .sidebarToggler { - /* Define styles if needed */ - } - - /* Scroll Sidebar */ - .scrollSidebar { - overflow-y: auto; - padding: 0 16px 0 28px; - height: calc(100vh - 128px); - border-radius: 12px; - } - .scrollSidebar::-webkit-scrollbar { - width: 4px; - background-color: transparent; -} - -.scrollSidebar::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.2); - border-radius: 10px; -} - -.scrollSidebar::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.4); -} - -.simplebarTrackHorizontal { - visibility: hidden !important; -} - -.cursorPointer { - cursor: pointer; -} - - /* LG Class */ - .lg { - /* Define styles if needed */ - } - - .leftSidebar { - left: -260px; - } - - .showSidebar { - left: 0; - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - } - - - /* Responsive Adjustments */ - @media (max-width: 1199px) { - .leftSidebar { - left: -260px; - } - - .showSidebar { - left: 0; - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - } - - .brandLogo, - .scrollSidebar { - padding: 0 16px; - } - } - \ No newline at end of file diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx deleted file mode 100644 index 61851130..00000000 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ /dev/null @@ -1,368 +0,0 @@ -// Sidebar.tsx -import React, { useCallback, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; -import { - IconX, - IconMessage, - IconBell, - IconRosetteDiscountCheck, - IconStar, - IconFileInvoice, - IconAddressBook, - IconUserCheck, - IconUsers, - IconChecklist, - IconHeadset, - IconDots, - IconSubtask, - IconReportMoney, - IconClipboardText, - IconAdjustments -} from "@tabler/icons-react"; -import salesLogo from "../../img/logo.png"; -import styles from "./Sidebar.module.css"; -import SidebarItem from "./SidebarItem"; -import { useAppContext } from "../../providers/AppProviders"; -import { SidebarSection } from "./SidebarSectionTypes"; -import { SidebarItem as SidebarItemType, Role, SubscriptionTier } from "./SidebarItemTypes"; -interface SidebarProps { - isCollapsed: boolean; - setIsCollapsed: React.Dispatch>; -} - -const Sidebar: React.FC = ({ isCollapsed, setIsCollapsed }) => { - const [activeItem, setActiveItem] = useState(null); - const handleItemClick = (itemTitle: string) => { - setActiveItem(itemTitle); - setIsCollapsed(true); - }; - - const handleOnClickCloseSideBar = () => { - setIsCollapsed(true); - }; - const handleSetActiveItem = (title: string) => { - setActiveItem(prev => (prev === title ? null : title)); - }; - - const { - subscriptionTiers: userSubscriptionTiers, - user - // ... other context values if needed - } = useAppContext(); - /** - * Determines if the current user has access to a sidebar item or link based on roles and subscription tiers. - * @param itemRoles - Array of roles that have access to the item/link. - * @param itemTiers - Array of subscription tiers that have access to the item/link. - * @returns Boolean indicating access permission. - */ - const hasAccess = useCallback( - (itemRoles: Role[], itemTiers: SubscriptionTier[]): boolean => { - if (!user || !user.role) return false; - const roleMatch = itemRoles.includes(user.role); - const tierMatch = itemTiers.some(tier => userSubscriptionTiers.includes(tier)); - - return roleMatch && tierMatch; - }, - [user, userSubscriptionTiers] - ); - - // Define your sidebar sections and items, including roles and tiers for links - const sidebarSections: SidebarSection[] = [ - { - section: "Agent", - items: [ - { - title: "AI Chat", - icon: , - to: "/", - tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - }, - { - title: "Notifications", - icon: , - to: "/notification-settings", - tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - } - ] - }, - { - divider: true - }, - { - items: [ - { - title: "Control Center", - icon: , - links: [ - { - title: "Team Member Roles", - href: "/admin", - tiers: [ - "Basic", - "Custom", - "Premium", - "Basic + Financial Assistant", - "Custom + Financial Assistant", - "Premium + Financial Assistant" - ], - roles: ["admin", "platformAdmin"] - }, - { - title: "Team Onboarding", - href: "/invitations", - tiers: [ - "Basic", - "Custom", - "Premium", - "Basic + Financial Assistant", - "Custom + Financial Assistant", - "Premium + Financial Assistant" - ], - roles: ["admin", "platformAdmin"] - }, - { - title: "Workspace Governance", - href: "/organization", - tiers: [ - "Basic", - "Custom", - "Premium", - "Basic + Financial Assistant", - "Custom + Financial Assistant", - "Premium + Financial Assistant" - ], - roles: ["admin", "platformAdmin"] - } - ], - tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - } - ] - }, - { - divider: true - }, - { - items: [ - { - title: "My Subscription", - icon: , - links: [ - { - title: "Subscription Management", - href: "/subscription-management", - tiers: [ - "Basic", - "Custom", - "Premium", - "Basic + Financial Assistant", - "Custom + Financial Assistant", - "Premium + Financial Assistant" - ], - roles: ["admin", "platformAdmin"] - }, - { - title: "Team Management", - href: "/manage-email-lists", - tiers: [ - "Basic", - "Custom", - "Premium", - "Basic + Financial Assistant", - "Custom + Financial Assistant", - "Premium + Financial Assistant" - ], - roles: ["admin", "platformAdmin"] - } - ], - tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - } - ] - }, - { - divider: true - }, - { - items: [ - { - title: "Premium Features", - icon: , - links: [ - { - title: "Upload Resources", - href: "/upload-resources", - roles: ["admin", "user", "platformAdmin"], - tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"] - }, - { - title: "Request Studies", - href: "/request-studies", - roles: ["admin", "user", "platformAdmin"], - tiers: ["Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"] - } - ], - tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - } - ] - }, - { - divider: true - }, - { - //It is only visible to platform administrators - items: [ - { - title: "Reports", - icon: , - links: [ - { - title: "Reports Dashboard", - href: "/view-reports", - tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "platformAdmin"] - }, - { - title: "Report Creation", - href: "/view-manage-reports", - tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "platformAdmin"] - }, - { - title: "Sharing & Distribution", - href: "/details-settings", - tiers: ["Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "platformAdmin"] - } - ], - tiers: ["Custom", "Premium", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - } - ] - }, - { - divider: true - }, - { - section: "Help Center", - items: [ - { - title: "Help Center", - icon: , - to: "/help-center", - tiers: ["Basic", "Custom", "Premium", "Basic + Financial Assistant", "Custom + Financial Assistant", "Premium + Financial Assistant"], - roles: ["admin", "user", "platformAdmin"] - } - ] - } - ]; - - const accessibleSidebarSections = useMemo(() => { - let previousSectionHasItems = false; - - return sidebarSections - .map((section, index) => { - if (section.divider) { - return previousSectionHasItems ? section : null; - } - - if (section.items) { - const accessibleItems = section.items - .map(item => { - if (item.links) { - const accessibleLinks = item.links.filter(link => hasAccess(link.roles, link.tiers)); - - if (accessibleLinks.length > 0) { - return { ...item, links: accessibleLinks }; - } else { - return null; - } - } else { - return hasAccess(item.roles, item.tiers) ? item : null; - } - }) - .filter(item => item !== null) as SidebarItemType[]; - - previousSectionHasItems = accessibleItems.length > 0; - - if (accessibleItems.length > 0) { - return { ...section, items: accessibleItems }; - } - } - - previousSectionHasItems = false; - return null; - }) - .filter(section => section !== null) as SidebarSection[]; - }, [sidebarSections, hasAccess]); - - return ( - - ); -}; - -export default Sidebar; diff --git a/frontend/src/components/Sidebar/SidebarItem.tsx b/frontend/src/components/Sidebar/SidebarItem.tsx deleted file mode 100644 index b2c78822..00000000 --- a/frontend/src/components/Sidebar/SidebarItem.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; -import styles from "./Sidebar.module.css"; - -interface SidebarItemProps { - title: string; - icon: JSX.Element; - to?: string; - links?: Array<{ title: string; href: string }>; - onClick: () => void; - isActive: boolean; - setIsActive: any; -} - -const SidebarItem: React.FC = ({ title, icon, to, links, onClick, isActive, setIsActive }) => { - const toggleSubmenu = (e: React.MouseEvent) => { - if (links) { - e.preventDefault(); - } - setIsActive(isActive); - }; - - return ( -
  • - {links ? ( - <> - -
      - {links.map((linkItem, index) => ( -
    • - - {linkItem.title} - -
    • - ))} -
    - - ) : ( - - {React.cloneElement(icon, { - className: isActive ? styles.sidebarLinkActiveIcon : styles.sidebarLinkIcon - })} - {title} - - )} -
  • - ); -}; - -export default SidebarItem; diff --git a/frontend/src/components/Sidebar/SidebarItemTypes.ts b/frontend/src/components/Sidebar/SidebarItemTypes.ts deleted file mode 100644 index cf771a71..00000000 --- a/frontend/src/components/Sidebar/SidebarItemTypes.ts +++ /dev/null @@ -1,37 +0,0 @@ -// SidebarItemTypes.ts - -export type Role = "platformAdmin"|"admin" | "user"; - -export type SubscriptionTier = - | "Basic" - | "Custom" - | "Premium" - | "Basic + Financial Assistant" - | "Custom + Financial Assistant" - | "Premium + Financial Assistant"; - -export interface SidebarLink { - title: string; - href: string; - tiers: SubscriptionTier[]; - roles: Role[]; -} - -export interface BaseSidebarItem { - title: string; - icon: JSX.Element; - tiers: SubscriptionTier[]; - roles: Role[]; -} - -export interface LinkSidebarItem extends BaseSidebarItem { - to: string; - links?: never; // Ensures that 'links' is not present -} - -export interface SubmenuSidebarItem extends BaseSidebarItem { - to?: never; // Ensures that 'to' is not present - links: SidebarLink[]; -} - -export type SidebarItem = LinkSidebarItem | SubmenuSidebarItem; diff --git a/frontend/src/components/Sidebar/SidebarSectionTypes.ts b/frontend/src/components/Sidebar/SidebarSectionTypes.ts deleted file mode 100644 index 856e557a..00000000 --- a/frontend/src/components/Sidebar/SidebarSectionTypes.ts +++ /dev/null @@ -1,9 +0,0 @@ -// SidebarSectionTypes.ts - -import { SidebarItem } from "./SidebarItemTypes"; - -export interface SidebarSection { - section?: string; // Optional, present for section titles - divider?: boolean; // Optional, present for dividers - items?: SidebarItem[]; // Optional, present for items within a section -} diff --git a/frontend/src/pages/admin/Admin.module.css b/frontend/src/pages/admin/Admin.module.css deleted file mode 100644 index 38704dc7..00000000 --- a/frontend/src/pages/admin/Admin.module.css +++ /dev/null @@ -1,225 +0,0 @@ -.page_container { - display: flex; - flex-direction: column; - width: 100%; - padding: 50px 50px 50px 50px; - background-color: white; -} - -.separator { - font-size: "24px"; - height: "24px"; - width: "24px"; -} - -.row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 10px; - align-items: center; - justify-content: space-between; - justify-items: center; - border-bottom: 2px solid #d9d9d9; - margin-bottom: 10px; -} - -.title { - margin-bottom: 10px; - margin-left: 20px; -} - - - -.closeButton { - position: fixed; - top: 0; - right: 0px; - border: none; - font-size: 25px; - color: black; - background-color: transparent; - cursor: pointer; - transform: rotate(45deg); - margin-bottom: -5px; - padding-top: 6px; -} - -.closeButtonContainer:active { - background-color: rgba(214, 214, 214, 0.315); - border-radius: 5px; -} -.questionInputContainer { - border-radius: 8px; - box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.14), 0px 0px 2px rgba(0, 0, 0, 0.12); - height: 90px; - width: 100%; - padding: 15px; - background: white; - border-bottom: 5px solid #a2e700; -} - -.questionInputTextArea { - width: 100%; - line-height: 40px; -} - -.table { - text-align: left; - background-color: white; - border-collapse: collapse; - width: 100%; -} - -.tableContainer { - border-radius: 10px; - overflow: hidden; - overflow-x: auto; - width: 100%; - border: 1px solid #d9d9d9; - margin-top: 35px; -} - -.table thead { - background-color: #9fc51d; - color: white; -} - -.button { - background: none; - justify-content: center; - align-items: center; - border: none; - border-radius: 6px; - padding: 5px 5px 2px 5px; - cursor: pointer; - font-size: 20px; - margin-top: 5px; -} - -.button:hover { - background-color: #e3e3e3; -} - -.addIcon { - font-size: 15px; - margin-right: 5px; -} - -.searchIcon { - font-size: 18px; - margin-right: 5px; - transform: scaleX(-1); -} - -@media (max-width: 768px) { - .button { - width: 100%; - padding: 0px 0px 0px 0px; - font-size: 18px; - justify-content: flex-start; - } - .option{ - font-size: 12px; - } - .title{ - margin-bottom: 10px; - margin-left: 0px; - } -} - -.modal{ - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 30em; - height: auto; - z-index: 500; - padding: 50px 100px 100px 100px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 6px; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); - gap: 30px; -} - -.input{ - width: 17.5em; - height: 1.8em; -} - -.modalTitle{ - margin-bottom: 40px; - text-align: center; - -} - -@media (max-width: 900px) { - .modal{ - width: 20em; - } -} - -.modalButtonContainer{ - - gap: 50px; -} - -.modalError{ - color: red; - font-style: italic; -} - -.modalSuccess{ - position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: auto; - height: auto; - z-index: 500; - padding: 100px; - background-color: #cef1a5; - border: 1px solid #87f083; - border-radius: 6px; - box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.07), 0px 2px 4px rgba(0, 0, 0, 0.05); - gap: 30px; -} - -.roleAdmin{ - width: 100px; - background-color: #d7e9f4; - padding: 5px; - color: #064789; - border-radius: 15px; -} - -.roleUser{ - width: 100px; - background-color: #d7e5be; - padding: 5px; - color: #1b4332; - border-radius: 15px; -} - -.rolePlatformAdmin{ - width: 100px; - font-size: 0.8em; - background-color: #d9ff98; - padding: 5px; - color: #1b4332; - border-radius: 15px; -} - -@media (max-width: 900px){ - .roleAdmin{ - width: 50px; - } - .roleUser{ - width: 50px; - } - .rolePlatformAdmin{ - width: 50px; - } -} \ No newline at end of file diff --git a/frontend/src/pages/admin/Admin.tsx b/frontend/src/pages/admin/Admin.tsx deleted file mode 100644 index cee62b77..00000000 --- a/frontend/src/pages/admin/Admin.tsx +++ /dev/null @@ -1,809 +0,0 @@ -import React, { useEffect, useState, useContext } from "react"; -import { PrimaryButton, Spinner, Dialog, DialogContent, Label, Dropdown, DefaultButton, MessageBar, ResponsiveMode } from "@fluentui/react"; -import { ToastContainer, toast } from "react-toastify"; -import { TextField, ITextFieldStyles } from "@fluentui/react/lib/TextField"; -import { AddFilled, DeleteRegular, EditRegular, SearchRegular } from "@fluentui/react-icons"; - -import { useAppContext } from "../../providers/AppProviders"; -import DOMPurify from "dompurify"; - -import { getUsers, inviteUser, createInvitation, deleteUser, updateUserData } from "../../api"; - -import styles from "./Admin.module.css"; - -const textFieldStyles: Partial = { root: { maxWidth: "900px" } }; -export const CreateUserForm = ({ isOpen, setIsOpen, users }: { isOpen: boolean; setIsOpen: React.Dispatch>; users: never[] }) => { - const { user, organization } = useAppContext(); - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); - const [role, setRole] = useState("user"); - const [errorMessage, setErrorMessage] = useState(""); - const [loading, setLoading] = useState(false); - const [success, setSuccess] = useState(false); - - const isValidated = (sanitizedUsername: string, sanitizedEmail: string) => { - if (!sanitizedUsername || !sanitizedEmail) { - setErrorMessage("Please fill in all fields"); - return false; - } else if (!/\S+@\S+\.\S+/.test(email)) { - setErrorMessage("Please provide a valid email address"); - return false; - } - return true; - }; - - const alreadyExists = (sanitizedEmail: string) => { - return users.some((existingUser: any) => existingUser.data.email === sanitizedEmail); - }; - - const handleSubmit = async () => { - // Sanitize inputs - const sanitizedUsername = DOMPurify.sanitize(username); - const sanitizedEmail = DOMPurify.sanitize(email).replace(/\s+/g, "").toLowerCase(); - - // Validate inputs - if (!isValidated(sanitizedUsername, sanitizedEmail)) return; - - // Check if user already exists - if (alreadyExists(sanitizedEmail)) { - setErrorMessage("User with this email already exists"); - return; - } - - // Check if `user` is not null - if (!user) { - // Handle unauthenticated user - setErrorMessage("You must be logged in to invite a user."); - return; - } - - setLoading(true); - - try { - const organizationId = user.organizationId; - const organizationName = organization?.name; - - const invitationResponse = await createInvitation({ - organizationId, - invitedUserEmail: sanitizedEmail, - userId: user.id, - role - }); - - if (invitationResponse?.error) { - setErrorMessage(invitationResponse.error); - } else { - setErrorMessage(""); - } - - const inviteResponse = await inviteUser({ - username: sanitizedUsername, - email: sanitizedEmail, - role, - organizationId, - organizationName - }); - - if (inviteResponse.error) { - setErrorMessage(inviteResponse.error); - setLoading(false); - return; - } else { - setSuccess(true); - } - } catch (error) { - console.error("Error during invitation process:", error); - setErrorMessage("An unexpected error occurred. Please try again."); - } finally { - setLoading(false); - } - }; - - const onUserNameChange = (_ev: React.FormEvent, newValue?: string) => { - setUsername(newValue || ""); - }; - - const onEmailChange = (_ev: React.FormEvent, newValue?: string) => { - setEmail(newValue || ""); - }; - - const onEnterPress = (ev: React.KeyboardEvent) => { - if (ev.key === "Enter" && !ev.shiftKey) { - ev.preventDefault(); - handleSubmit(); - } - }; - - const roleOptions = [ - { key: "1", text: "user" }, - { key: "2", text: "admin" } - ]; - - const handleRoleChange = (event: any, selectedOption: any) => { - setRole(selectedOption.text); - }; - - const onDismiss = () => { - setEmail(""); - setUsername(""); - setRole("user"); - setErrorMessage(""); - setLoading(false); - setIsOpen(false); - setSuccess(false); - }; - - const onConfirm = () => { - handleSubmit(); - }; - - return ( - - ); -}; - -export const DeleteUserDialog = ({ - isOpen, - onDismiss, - onConfirm, - isDeletingUser -}: { - isOpen: boolean; - onDismiss: any; - onConfirm: any; - isDeletingUser: boolean; -}) => { - return ( - - ); -}; - -const Admin = () => { - const { user, organization } = useAppContext(); - const [search, setSearch] = useState(""); - const [filteredUsers, setFilteredUsers] = useState([]); - const [selectedUser, setSelectedUser] = useState({ - id: "", - data: { - name: "", - email: "", - role: "" - } - }); - - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - - const [isOpen, setIsOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [isDeletingUser, setIsDeletingUser] = useState(false); - const [isEditing, setIsEditing] = useState(false); - const [inputUserName, setInputUserName] = useState(""); - const [inputEmailName, setInputEmailName] = useState(""); - const roleOptions = [ - { key: "1", text: "user" }, - { key: "2", text: "admin" } - ]; - const [categorySelection, setCategorySelection] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); - const [isError, setIsError] = useState(false); - const [dataLoad, setDataLoad] = useState(false); - const [isEditSuccess, setIsEditSuccess] = useState(false); - const roleStyles: { [key in "admin" | "user" | "platformAdmin"]: string } = { - admin: styles.roleAdmin, - user: styles.roleUser, - platformAdmin: styles.rolePlatformAdmin - }; - - if (!user) { - return
    Please log in to view the user list.
    ; - } - - useEffect(() => { - const getUserList = async () => { - if (!user) { - // User is not logged in - setUsers([]); - setFilteredUsers([]); - setLoading(false); - return; - } - - setLoading(true); - - try { - let usersList = await getUsers({ - user: { - id: user.id, - name: user.name, - organizationId: user.organizationId - } - }); - - if (!Array.isArray(usersList)) { - usersList = []; - } - - setUsers(usersList); - setFilteredUsers(usersList); - } catch (error) { - console.error("Error fetching user list:", error); - setUsers([]); - setFilteredUsers([]); - } finally { - setLoading(false); - } - }; - - getUserList(); - }, [dataLoad]); - - useEffect(() => { - if (!search) { - setFilteredUsers(users); - } else { - const filtered = users.filter((user: any) => { - return user.data.name.toLowerCase().includes(search.toLowerCase()) || user.data.email.toLowerCase().includes(search.toLowerCase()); - }); - setFilteredUsers(filtered); - } - }, [search]); - - const handleDeleteClick = (user: any) => { - setSelectedUser(user); - setIsDeleting(true); - }; - - const deleteUserFromOrganization = (id: string) => { - setIsDeletingUser(true); - deleteUser({ user, userId: id }).then(res => { - if (res.error) { - console.log("error", res.error); - toast("There was an error deleting the user", { type: "error" }); - setIsDeleting(false); - } else { - const updatedUsers = users.filter((user: any) => user.id !== id); - setUsers(updatedUsers); - setFilteredUsers(updatedUsers); - setIsDeleting(false); - toast("User deleted successfully", { type: "success" }); - } - setIsDeletingUser(false); - }); - }; - - const handleEditClick = (user: any) => { - setSelectedUser(user); - setIsEditing(true); - }; - - const handleInputName = (event: React.ChangeEvent) => { - setInputUserName(event.target.value); - }; - - const handleInputEmail = (event: React.ChangeEvent) => { - setInputEmailName(event.target.value); - }; - - const handleTypeDropdownChange = (event: any, selectedOption: any) => { - setCategorySelection(selectedOption.text); - }; - - const editUser = async (userID: string) => { - if (inputUserName == "") { - setErrorMessage("Please type the Username"); - setIsError(true); - return; - } - if (inputEmailName == "") { - setErrorMessage("Please type the Email"); - setIsError(true); - return; - } - if (categorySelection == "") { - setErrorMessage("Please select the Report Category"); - setIsError(true); - return; - } - - let timer: NodeJS.Timeout; - - try { - await updateUserData({ - userId: userID, - patchData: { - name: inputUserName, - email: inputEmailName, - role: categorySelection - } - }); - setIsEditing(false); - setIsEditSuccess(true); - timer = setTimeout(() => { - setIsEditSuccess(false); - }, 3000); - } catch (error) { - console.error("Error trying to update the state: ", error); - } finally { - setDataLoad(!dataLoad); - } - }; - - return ( -
    - - <> -
    -

    Roles and access

    -
    -
    - { - setIsOpen(true); - }} - > - - Create user - - { - setSearch(newValue || ""); - }} - iconProps={{ - iconName: "Search", - children: - }} - /> -
    - - {loading ? null : } - { - setIsDeleting(false); - }} - onConfirm={() => { - deleteUserFromOrganization(selectedUser?.id); - }} - isDeletingUser={isDeletingUser} - /> - {isEditing && ( -
    - -
    - - - - - - - {isError && {errorMessage}} - - setIsEditing(false)} text="Cancel" /> - { - editUser(selectedUser.id); - }} - text="Edit User" - /> - -
    - )} - {isEditSuccess && ( -
    - -
    - )} - {loading ? ( - - ) : ( -
    - - - - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - - - - - - - ); - })} - -
    - Name - EmailRoleActions
    - {user.data.name} - - {user.data.email} - -
    -
    - {user.data.role} -
    -
    -
    - { -
    - - -
    - } -
    -
    - )} - -
    - ); -}; - -export default Admin; diff --git a/frontend/src/pages/chat/Chat.module.css b/frontend/src/pages/chat/Chat.module.css deleted file mode 100644 index 6eae15c0..00000000 --- a/frontend/src/pages/chat/Chat.module.css +++ /dev/null @@ -1,272 +0,0 @@ -.container { - flex: 1; - display: flex; - flex-direction: column; - background-color: white; - margin: 0px 10px 0px 0px; -} - -.mainContainer { - display: flex; - flex-direction: row-reverse; - flex: 1; - overflow: hidden; -} - -.chatRoot { - flex: 1; - display: flex; - height: 100%; -} - -.chatContainer { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -.chatEmptyState { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - max-height: 1024px; - padding-top: 60px; -} - -.chatEmptyStateTitle { - font-size: 4rem; - font-weight: 600; - margin-top: 0; - margin-bottom: 30px; -} - -.chatEmptyStateSubtitle { - font-weight: 600; - margin-bottom: 10px; -} - -@media only screen and (max-height: 780px) { - .chatEmptyState { - padding-top: 0; - } - - .chatEmptyStateTitle { - font-size: 4rem; - margin-bottom: 0px; - } -} - -.noneDisplay { - display: none; -} - -.flexDescription { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.conversationIsLoading { - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - max-height: 1024px; - padding-top: 60px; -} - -.chatMessageStream { - flex-grow: 1; - max-height: 1024px; - max-width: 80.5%; - width: 90%; - overflow-y: auto; - padding-left: 24px; - padding-right: 24px; - padding-top: 15px; - display: flex; - flex-direction: column; -} - -.chatMessageGpt { - margin-bottom: 20px; - max-width: 80%; - display: flex; - min-width: 300px; -} - -.chatMessageGptMinWidth { - max-width: 500px; - margin-bottom: 20px; -} - -.chatInput { - position: sticky; - bottom: 0; - flex: 0 0 auto; - padding: 10px 5px 5px 10px; - display: flex; - align-items: start; - border-radius: 6px; - border: 2px solid #9fc51d; - width: 80%; - background: white; - transition: height 0.2s ease; - margin-left: 30px; -} - -.newChatButton { - display: flex; - padding: 10px; - justify-content: center; - align-items: center; - font-size: 24px; - border: 1px solid #979797; - border-radius: 6px; - color: #979797; - background-color: #f5f5f5; - transition: 0.3s; - margin-right: 5px; -} - -.newChatButton:hover { - color: #000000; -} - -.newChatButtonDisabled { - display: flex; - padding: 10px; - justify-content: center; - align-items: center; - font-size: 24px; - border: 1px dashed #bfbfbf; - border-radius: 6px; - color: #bfbfbf; - background-color: #f0f0f0; - cursor: not-allowed; - opacity: 0.5; - transition: 0.3s; - margin-right: 5px; - font-style: italic; -} - -.clearChatButtonDisabled { - display: flex; - padding: 10px; - justify-content: center; - align-items: center; - font-size: 24px; - border: 1px dashed #bfbfbf; - border-radius: 6px; - color: #bfbfbf; - background-color: #f0f0f0; - cursor: not-allowed; - opacity: 0.5; - transition: 0.3s; - margin-right: 5px; - font-style: italic; -} - -.buttonsActions { - display: flex; - flex-direction: row; - padding-right: 10px; -} - -.clearChatButton { - display: flex; - padding: 10px; - justify-content: center; - align-items: center; - font-size: 24px; - border: 1px solid #979797; - border-radius: 6px; - color: #9f9c9c; - background-color: #f5f5f5; - transition: 0.3s; -} - -.clearChatButton:hover { - color: #000000; -} -.chatAnalysisPanel { - flex: 1; - overflow-y: auto; - max-height: 89vh; - margin-left: 20px; - margin-right: 20px; -} - -.chatSettingsSeparator { - margin-top: 15px; -} - -.chatDisclaimer{ - margin-top: 5px; - color: #8d8d8d; - font-size: 0.8em; - text-align: center; - -} - -@media only screen and (max-height: 999px) { - .chatDisclaimer{ - margin-left: 30px; - } -} - -.loadingLogo { - font-size: 28px; -} - -.commandsContainer { - display: flex; - align-self: flex-start; - height: 95%; - overflow-y: auto; - margin-top: 10px; - margin-right: 0px; -} - -.loadingData { - display: flex; - align-self: flex-start; - height: 95%; - overflow-y: hidden; -} - -.spinnerStyles { - position: absolute; -} - -.commandsContainer::-webkit-scrollbar { - width: 8px; -} - -.commandsContainer::-webkit-scrollbar-track { - background: transparent; -} - -.commandsContainer::-webkit-scrollbar-thumb { - background: #888; - border-radius: 4px; -} - -.commandsContainer::-webkit-scrollbar-thumb:hover { - background: #555; -} - -.commandButton { - margin-right: 20px; - margin-bottom: 20px; -} - -.hidden { - display: none; -} diff --git a/frontend/src/pages/chat/Chat.tsx b/frontend/src/pages/chat/Chat.tsx deleted file mode 100644 index 83ea25ed..00000000 --- a/frontend/src/pages/chat/Chat.tsx +++ /dev/null @@ -1,801 +0,0 @@ -import { useRef, useState, useEffect, useContext } from "react"; -import { Checkbox, Panel, DefaultButton, TextField, SpinButton, Spinner } from "@fluentui/react"; - -import styles from "./Chat.module.css"; - -import { chatApiGpt, Approaches, AskResponse, ChatRequestGpt, ChatTurn } from "../../api"; -import { Answer, AnswerError, AnswerLoading } from "../../components/Answer"; -import { QuestionInput } from "../../components/QuestionInput"; -import { ExampleList } from "../../components/Example"; -import { UserChatMessage } from "../../components/UserChatMessage"; -import { AnalysisPanel, AnalysisPanelTabs } from "../../components/AnalysisPanel"; -import { ClearChatButton } from "../../components/ClearChatButton"; -import { getTokenOrRefresh } from "../../components/QuestionInput/token_util"; -import { SpeechConfig, AudioConfig, SpeechSynthesizer, ResultReason } from "microsoft-cognitiveservices-speech-sdk"; -import { getFileType } from "../../utils/functions"; -import salesLogo from "../../img/logo.png"; -import { useAppContext } from "../../providers/AppProviders"; -import { ChatHistoryPanel } from "../../components/HistoryPannel/ChatHistoryPanel"; -import { FeedbackRating } from "../../components/FeedbackRating/FeedbackRating"; -import { SettingsPanel } from "../../components/SettingsPanel"; -import StartNewChatButton from "../../components/StartNewChatButton/StartNewChatButton"; -import FinancialPopup from "../../components/FinancialAssistantPopup/FinancialAssistantPopup"; - -const userLanguage = navigator.language; -let error_message_text = ""; -if (userLanguage.startsWith("pt")) { - error_message_text = "Desculpe, tive um problema técnico com a solicitação. Por favor informar o erro a equipe de suporte. "; -} else if (userLanguage.startsWith("es")) { - error_message_text = "Lo siento, yo tuve un problema con la solicitud. Por favor informe el error al equipo de soporte. "; -} else { - error_message_text = "I'm sorry, I had a problem with the request. Please report the error to the support team. "; -} - -const Chat = () => { - // speech synthesis is disabled by default - - const { organization } = useAppContext(); - const speechSynthesisEnabled = false; - - const [placeholderText, setPlaceholderText] = useState(""); - const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false); - const [promptTemplate, setPromptTemplate] = useState(""); - const [retrieveCount, setRetrieveCount] = useState(3); - const [useSemanticRanker, setUseSemanticRanker] = useState(true); - const [useSemanticCaptions, setUseSemanticCaptions] = useState(false); - const [excludeCategory, setExcludeCategory] = useState(""); - const [useSuggestFollowupQuestions, setUseSuggestFollowupQuestions] = useState(false); - - const chatContainerRef = useRef(null); - - const { - showHistoryPanel, - showFeedbackRatingPanel, - dataConversation, - setDataConversation, - chatId, - conversationIsLoading, - setRefreshFetchHistory, - setChatId, - setChatSelected, - setChatIsCleaned, - chatIsCleaned, - settingsPanel, - user, - isFinancialAssistantActive, - documentName - } = useAppContext(); - - const lastQuestionRef = useRef(""); - const lastFileBlobUrl = useRef(""); - const chatMessageStreamEnd = useRef(null); - const [fileType, setFileType] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(); - - const [activeCitation, setActiveCitation] = useState(); - const [activeAnalysisPanelTab, setActiveAnalysisPanelTab] = useState(undefined); - - const [selectedAnswer, setSelectedAnswer] = useState(0); - const [answers, setAnswers] = useState<[user: string, response: AskResponse][]>([]); - - const [userId, setUserId] = useState(""); // this is more like a conversation id instead of a user id - const triggered = useRef(false); - - const [lastAnswer, setLastAnswer] = useState(""); - const restartChat = useRef(false); - - const streamResponse = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { - let agent; - lastQuestionRef.current = question; - lastFileBlobUrl.current = fileBlobUrl; - restartChat.current = false; - error && setError(undefined); - setIsLoading(true); - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - setLastAnswer(""); - - agent = isFinancialAssistantActive ? "financial" : "consumer"; - - let history: ChatTurn[] = []; - if (dataConversation.length > 0) { - history.push(...dataConversation); - } else { - history.push(...answers.map(a => ({ user: a[0], bot: { message: a[1]?.answer, thoughts: a[1]?.thoughts || [] } }))); - } - history.push({ user: question, bot: undefined }); - const request: ChatRequestGpt = { - history: history, - approach: Approaches.ReadRetrieveRead, - conversation_id: chatId !== null ? chatId : userId, - query: question, - file_blob_url: fileBlobUrl || "", - documentName, - agent, - overrides: { - promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, - excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, - top: retrieveCount, - semanticRanker: useSemanticRanker, - semanticCaptions: useSemanticCaptions, - suggestFollowupQuestions: useSuggestFollowupQuestions - } - }; - - try { - const response = await fetch("/stream_chatgpt", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-MS-CLIENT-PRINCIPAL-ID": user?.id || "", - "X-MS-CLIENT-PRINCIPAL-NAME": user?.name || "", - "X-MS-CLIENT-PRINCIPAL-ORGANIZATION": user?.organizationId || "" - }, - body: JSON.stringify({ - question: request.query, - conversation_id: request.conversation_id, - url: request.file_blob_url, - agent: request.agent, - documentName: request.documentName - }) - }); - - if (!response.body) { - throw new Error("ReadableStream not supported in this browser."); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let result = ""; - let resultObj = { - conversation_id: "", - answer: "", - thoughts: "" - }; - let jsonBuffer = ""; // Buffer for incomplete JSON structures - - while (true) { - if (restartChat.current) { - handleNewChat(); - return; - } - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - - // Check if chunk contains a JSON structure - if (chunk.includes("{") || jsonBuffer) { - // Add chunk to buffer - jsonBuffer += chunk; - - // Try to find complete JSON structure - const startIndex = jsonBuffer.indexOf("{"); - const endIndex = jsonBuffer.lastIndexOf("}"); - - if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) { - try { - const jsonString = jsonBuffer.substring(startIndex, endIndex + 1); - const parsedObj = JSON.parse(jsonString); - - // Update resultObj if we have a valid conversation_id - if (parsedObj.conversation_id) { - resultObj = parsedObj; - const conditionOne = answers.map(a => ({ user: a[0] })); - if (conditionOne.length <= 0) { - setRefreshFetchHistory(true); - setChatId(resultObj.conversation_id); - } else { - setRefreshFetchHistory(false); - } - setUserId(resultObj.conversation_id); - } - - // Handle any text after the JSON structure - const remainingText = jsonBuffer.substring(endIndex + 1); - if (remainingText) { - result += remainingText; - setLastAnswer(result); - } - - // Clear the buffer after successful parsing - jsonBuffer = ""; - } catch (e) { - // If parsing fails, keep the buffer for next iteration - console.log("Incomplete JSON structure, waiting for more data"); - } - } - } else { - // Regular text content - result += chunk; - setLastAnswer(result); - } - } - - const regex = /(Source:\s?\/?)?(source:)?(https:\/\/)?([^/]+\.blob\.core\.windows\.net)?(\/?documents\/?)?/g; - // Function to clean citations - const cleanCitation = (citation: string) => { - return citation.replace(regex, ""); - }; - - // Find and replace citations in the text - const citationRegex = /\[\[(\d+)\]\]\((.*?)\)/g; - result = result.replace(citationRegex, (match, number, citation) => { - const cleanedCitation = cleanCitation(citation); - return `[[${number}]](${cleanedCitation})`; - }); - setAnswers([ - ...answers, - [ - question, - { - answer: result || "", - conversation_id: resultObj.conversation_id, - data_points: [""], - thoughts: resultObj.thoughts || [] - } as AskResponse - ] - ]); - const botResponse = { - answer: result || "", - conversation_id: resultObj.conversation_id, - data_points: [""], - thoughts: resultObj.thoughts || [] - } as AskResponse; - setDataConversation([...dataConversation, { user: question, bot: { message: botResponse.answer, thoughts: botResponse.thoughts } }]); - lastQuestionRef.current = ""; - } catch (error) { - console.error("Error fetching streamed response:", error); - setError(error); - } finally { - setIsLoading(false); - } - }; - - const makeApiRequestGpt = async (question: string, chatId: string | null, fileBlobUrl: string | null) => { - let agent = null; - lastQuestionRef.current = question; - lastFileBlobUrl.current = fileBlobUrl; - - error && setError(undefined); - setIsLoading(true); - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - - if (isFinancialAssistantActive == true) { - agent = "financial"; - } else { - agent = "consumer"; - } - - try { - let history: ChatTurn[] = []; - if (dataConversation.length > 0) { - history.push(...dataConversation); - } else { - history.push(...answers.map(a => ({ user: a[0], bot: { message: a[1]?.answer, thoughts: a[1]?.thoughts || [] } }))); - } - history.push({ user: question, bot: undefined }); - const request: ChatRequestGpt = { - history: history, - approach: Approaches.ReadRetrieveRead, - conversation_id: chatId !== null ? chatId : userId, - query: question, - file_blob_url: fileBlobUrl || "", - documentName, - agent, - overrides: { - promptTemplate: promptTemplate.length === 0 ? undefined : promptTemplate, - excludeCategory: excludeCategory.length === 0 ? undefined : excludeCategory, - top: retrieveCount, - semanticRanker: useSemanticRanker, - semanticCaptions: useSemanticCaptions, - suggestFollowupQuestions: useSuggestFollowupQuestions - } - }; - const result = await chatApiGpt(request, user); - const conditionOne = answers.map(a => ({ user: a[0] })); - if (conditionOne.length <= 0) { - setRefreshFetchHistory(true); - setChatId(result.conversation_id); - } else { - setRefreshFetchHistory(false); - } - setAnswers([...answers, [question, result]]); - setUserId(result.conversation_id); - const response = { - answer: result.answer || "", - conversation_id: chatId, - data_points: [""], - thoughts: result.thoughts || [] - } as AskResponse; - setDataConversation([...dataConversation, { user: question, bot: { message: response.answer, thoughts: response.thoughts } }]); - lastQuestionRef.current = ""; - - // Voice Synthesis - if (speechSynthesisEnabled) { - const tokenObj = await getTokenOrRefresh(); - const speechConfig = SpeechConfig.fromAuthorizationToken(tokenObj.authToken, tokenObj.region); - const audioConfig = AudioConfig.fromDefaultSpeakerOutput(); - speechConfig.speechSynthesisLanguage = tokenObj.speechSynthesisLanguage; - speechConfig.speechSynthesisVoiceName = tokenObj.speechSynthesisVoiceName; - const synthesizer = new SpeechSynthesizer(speechConfig, audioConfig); - - synthesizer.speakTextAsync( - result.answer.replace(/ *\[[^)]*\] */g, ""), - function (result) { - if (result.reason === ResultReason.SynthesizingAudioCompleted) { - console.log("synthesis finished."); - } else { - console.error("Speech synthesis canceled, " + result.errorDetails + "\nDid you update the subscription info?"); - } - synthesizer.close(); - }, - function (err) { - console.trace("err - " + err); - synthesizer.close(); - } - ); - } - } catch (e) { - setError(e); - console.log(e); - } finally { - setIsLoading(false); - } - }; - - const clearChat = () => { - if (lastQuestionRef.current || dataConversation.length > 0 || !chatIsCleaned) { - lastQuestionRef.current = ""; - lastFileBlobUrl.current = ""; - error && setError(undefined); - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - setAnswers([]); - setDataConversation([]); - setChatIsCleaned(true); - } else { - return; - } - }; - - const handleNewChat = () => { - if (lastQuestionRef.current || dataConversation.length > 0 || chatIsCleaned) { - restartChat.current = true; - lastQuestionRef.current = ""; - lastFileBlobUrl.current = ""; - error && setError(undefined); - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - setAnswers([]); - setDataConversation([]); - setChatId(""); - setUserId(""); - setChatSelected(""); - setChatIsCleaned(false); - } else { - return; - } - }; - - /**Get Pdf */ - const getPdf = async (pdfName: string) => { - /** get file type */ - let type = getFileType(pdfName); - setFileType(type); - try { - const response = await fetch("/api/get-blob", { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - blob_name: pdfName - }) - }); - - if (!response.ok) { - throw new Error(`Error fetching DOC: ${response.status}`); - } - - return await response.blob(); - } catch (error) { - console.error(error); - throw new Error("Error fetching DOC."); - } - }; - - useEffect(() => { - chatMessageStreamEnd.current?.scrollIntoView({ behavior: "smooth" }); - if (dataConversation.length > 0) { - chatContainerRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); - } - if (triggered.current === false) { - triggered.current = true; - } - const language = navigator.language; - if (language.startsWith("pt")) { - setPlaceholderText("Escreva aqui sua pergunta"); - } - if (language.startsWith("es")) { - setPlaceholderText("Escribe tu pregunta aqui"); - } else { - setPlaceholderText("Write your question here"); - } - - // fill answers with data from dataConversation - setAnswers( - dataConversation.map(data => [ - data.user, - { - answer: data?.bot?.message || "", - data_points: [], - thoughts: data?.bot?.thoughts || [] - } - ]) - ); - }, [isLoading, dataConversation]); - - const onPromptTemplateChange = (_ev?: React.FormEvent, newValue?: string) => { - setPromptTemplate(newValue || ""); - }; - - const onRetrieveCountChange = (_ev?: React.SyntheticEvent, newValue?: string) => { - setRetrieveCount(parseInt(newValue || "3")); - }; - - const onUseSemanticRankerChange = (_ev?: React.FormEvent, checked?: boolean) => { - setUseSemanticRanker(!!checked); - }; - - const onUseSemanticCaptionsChange = (_ev?: React.FormEvent, checked?: boolean) => { - setUseSemanticCaptions(!!checked); - }; - - const onExcludeCategoryChanged = (_ev?: React.FormEvent, newValue?: string) => { - setExcludeCategory(newValue || ""); - }; - - const onUseSuggestFollowupQuestionsChange = (_ev?: React.FormEvent, checked?: boolean) => { - setUseSuggestFollowupQuestions(!!checked); - }; - - const extractAfterDomain = (url: string) => { - const extensions = [".net", ".com"]; - - for (const ext of extensions) { - const index = url.lastIndexOf(ext); - if (index !== -1) { - let currentUrl = url.substring(index + ext.length); - if (currentUrl.startsWith("/")) { - currentUrl = currentUrl.substring(1); - } - return currentUrl; - } - } - return url; - }; - - const onShowCitation = async (citation: string, fileName: string, index: number) => { - if (!citation.endsWith(".pdf") && !citation.endsWith(".doc") && !citation.endsWith(".docx") && !citation.endsWith(".html")) { - return window.open(citation, "_blank"); - } - // Extract filepath if necessary - const modifiedFilename = extractAfterDomain(fileName); - - const response = await getPdf(modifiedFilename); - if (activeCitation === citation && activeAnalysisPanelTab === AnalysisPanelTabs.CitationTab && selectedAnswer === index) { - setActiveAnalysisPanelTab(undefined); - } else { - var file = new Blob([response as BlobPart]); - readFile(file); - - function readFile(input: Blob) { - const fr = new FileReader(); - fr.readAsDataURL(input); - fr.onload = function (event) { - const res: any = event.target ? event.target.result : undefined; - setActiveCitation(res); - }; - } - setActiveAnalysisPanelTab(AnalysisPanelTabs.CitationTab); - } - - setSelectedAnswer(index); - }; - - const answerFromHistory = dataConversation.map(data => data.bot?.message); - const thoughtsFromHistory = dataConversation.map(data => data.bot?.thoughts); - - const responseForPreviewPanel = { - answer: answerFromHistory.toString(), - conversation_id: chatId, - data_points: [""], - thoughts: thoughtsFromHistory.toString() - } as AskResponse; - - const onToggleTab = (tab: AnalysisPanelTabs, index: number) => { - if (index !== selectedAnswer) { - setActiveCitation(undefined); - setActiveAnalysisPanelTab(undefined); - } - if (activeAnalysisPanelTab === tab && selectedAnswer === index) { - setActiveAnalysisPanelTab(undefined); - setSelectedAnswer(-1); - setActiveCitation(undefined); - } else { - setActiveAnalysisPanelTab(tab); - } - setSelectedAnswer(index); - }; - - const hideTab = () => { - setActiveAnalysisPanelTab(undefined); - }; - - const handleKeyDown = (event: KeyboardEvent) => { - const isCtrlOrCmd = event.ctrlKey || event.metaKey; - const isAlt = event.altKey; - - if (event.code === "KeyO" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - clearChat(); - } else if (event.code === "KeyY" && isCtrlOrCmd && isAlt) { - event.preventDefault(); - handleNewChat(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - //If I add this on a useEffect it doesn't work, I don't know why - //maybe because it's a global event listener and is called multiple times - - const isButtonEnabled = !!(lastQuestionRef.current || dataConversation.length > 0 || chatIsCleaned); - - return ( -
    -
    -
    - {showHistoryPanel && } -
    -
    -
    -
    {showFeedbackRatingPanel && }
    -
    -
    -
    {settingsPanel && }
    -
    - -
    -
    -
    - {!lastQuestionRef.current && dataConversation.length <= 0 ? ( -
    0 && !conversationIsLoading ? styles.chatMessageStream : styles.chatEmptyState}> - {conversationIsLoading && } - {!isFinancialAssistantActive && ( -
    - Sales Factory logo -

    FreddAid

    - -

    - Your AI-driven Home Improvement expert who boosts marketing performance by synthesizing multiple data sources to - deliver actionable insights. -

    -
    - )} - {isFinancialAssistantActive && ( -
    - Sales Factory logo -

    FinlAI

    - -

    - Your financial ally, delivering real-time insights and strategic guidance to help you stay ahead of opportunities - and threats in an ever-changing financial landscape. -

    -
    - )} -
    - ) : ( -
    - {conversationIsLoading && } - {dataConversation.length > 0 - ? dataConversation.map((item, index) => { - const response = { - answer: item.bot?.message || "", - conversation_id: chatId, - data_points: [""], - thoughts: item.bot?.thoughts || [] - } as AskResponse; - return ( -
    - -
    - onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} - showFollowupQuestions={false} - showSources={true} - /> -
    -
    - ); - }) - : answers.map((answer, index) => { - return ( -
    - -
    - onShowCitation(c, n, index)} - onThoughtProcessClicked={() => onToggleTab(AnalysisPanelTabs.ThoughtProcessTab, index)} - onSupportingContentClicked={() => onToggleTab(AnalysisPanelTabs.SupportingContentTab, index)} - onFollowupQuestionClicked={q => makeApiRequestGpt(q, null, null)} - showFollowupQuestions={false} - showSources={true} - /> -
    -
    - ); - })} - {error ? ( - <> - -
    - { - streamResponse(lastQuestionRef.current, chatId !== "" ? chatId : null, lastFileBlobUrl.current); - }} - /> -
    - - ) : null} - {lastQuestionRef.current !== "" && ( - <> - -
    - {}} - onThoughtProcessClicked={() => {}} - onSupportingContentClicked={() => {}} - onFollowupQuestionClicked={q => {}} - showFollowupQuestions={false} - showSources={true} - /> -
    - - )} -
    -
    - )} -
    - { - streamResponse(question, chatId !== "" ? chatId : null, fileBlobUrl || null); - }} - extraButtonNewChat={} - /> -
    -
    -

    This app is in beta. Responses may not be fully accurate.

    -
    -
    - {(answers.length > 0 && activeAnalysisPanelTab && answers[selectedAnswer] && ( - { - onToggleTab(x, selectedAnswer); - }} - citationHeight="810px" - answer={answers[selectedAnswer]?.[1]} - activeTab={activeAnalysisPanelTab} - fileType={fileType} - onHideTab={hideTab} - /> - )) || - (dataConversation.length > 0 && fileType !== "" && activeAnalysisPanelTab && ( - { - onToggleTab(x, selectedAnswer); - }} - citationHeight="810px" - answer={responseForPreviewPanel} - activeTab={activeAnalysisPanelTab} - fileType={fileType} - onHideTab={hideTab} - /> - ))} - - setIsConfigPanelOpen(false)} - closeButtonAriaLabel="Close" - onRenderFooterContent={() => setIsConfigPanelOpen(false)}>Close} - isFooterAtBottom={true} - > - - - - - - - - -
    -
    -
    - ); -}; - -export default Chat; diff --git a/frontend/src/pages/financialassistant/FinancialAssistant.module.css b/frontend/src/pages/financialassistant/FinancialAssistant.module.css deleted file mode 100644 index 3c4313a5..00000000 --- a/frontend/src/pages/financialassistant/FinancialAssistant.module.css +++ /dev/null @@ -1,94 +0,0 @@ -.page_container { - display: flex; - flex-direction: column; - width: 100%; - padding: 50px 50px 50px 50px; -} - -.row { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - justify-items: center; - border-bottom: 2px solid #d9d9d9; - margin-bottom: 10px; -} - -.title { - margin-bottom: 10px; - margin-left: 20px; -} - -.table { - text-align: left; - background-color: white; - border-collapse: collapse; - width: 100%; -} - -.tableContainer { - border-radius: 10px; - overflow: hidden; - border: 1px solid #d9d9d9; - margin-top: 35px; -} - -.table thead { - background-color: #9fc51d; - color: white; -} - -.table th, -.table td { - padding: 10px; -} - -.searchContainer { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; -} - -.spinnerContainer { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; -} - -.spinner { - font-size: 36px; - width: 150px; - height: 150px; -} - -.messageBarText{ - font-size: 16px; -} - -.searchIcon { - font-size: 18px; - margin-right: 5px; - transform: scaleX(-1); -} - -.pillRole { - width: 100px; - padding: 5px; - border-radius: 15px; -} - -.roleContainer { - width: 100%; - justify-content: flex-start; - justify-items: center; - text-align: center; - display: flex; - text-transform: capitalize; -} - -.textJustify { - text-align: justify; -} diff --git a/frontend/src/pages/financialassistant/FinancialAssistant.tsx b/frontend/src/pages/financialassistant/FinancialAssistant.tsx deleted file mode 100644 index ec46ae33..00000000 --- a/frontend/src/pages/financialassistant/FinancialAssistant.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { useAppContext } from "../../providers/AppProviders"; -import { getFinancialAssistant, upgradeSubscription, removeFinancialAssistant } from "../../api"; // Asegúrate de importar la función -import { Spinner, PrimaryButton, DefaultButton, Dialog, DialogType, DialogFooter, MessageBar, MessageBarType } from "@fluentui/react"; -import styles from "./FinancialAssistant.module.css"; - -const FinancialAssistant = () => { - const { user, organization } = useAppContext(); - const [subscriptionStatus, setSubscriptionStatus] = useState(null); - const [loading, setLoading] = useState(true); - const [showSubscribeDialog, setShowSubscribeDialog] = useState(false); - const [showUnsubscribeDialog, setShowUnsubscribeDialog] = useState(false); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchStatus = async () => { - setLoading(true); - try { - if (!user?.organizationId) { - throw new Error("Organization ID is required"); - } - const { financial_assistant_active } = await getFinancialAssistant({ - user: { - ...user, - organizationId: user.organizationId - }, - subscriptionId: organization?.subscriptionId ?? "default-org-id" - }); - setSubscriptionStatus(financial_assistant_active); - } catch (error: any) { - console.log(error); - if (error.status === false) { - setSubscriptionStatus(false); - setError("Financial Assistant feature is not present in this subscription."); - } else if (error.status === null) { - setError("Bad request: unable to retrieve subscription status."); - } else { - setError("An error occurred while fetching subscription status."); - } - } finally { - setLoading(false); - } - }; - - fetchStatus(); - }, [user, organization]); - - const handleSubscribe = async () => { - try { - setLoading(true); - const userObj = user ? { id: user.id, name: user.name, organizationId: user.organizationId ?? "default-org-id" } : undefined; - await upgradeSubscription({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); - setSubscriptionStatus(true); - setShowSubscribeDialog(false); - //This reloads the page so the financial assistant toggle appears after click - window.location.reload(); - } catch { - setError("An error occurred while subscribing to the Financial Assistant feature."); - } finally { - setLoading(false); - } - }; - - const handleUnsubscribe = async () => { - try { - setLoading(true); - const userObj = user ? { id: user.id, name: user.name, organizationId: user.organizationId ?? "default-org-id" } : undefined; - await removeFinancialAssistant({ user: userObj, subscriptionId: organization?.subscriptionId ?? "default-org-id" }); - setSubscriptionStatus(false); - setShowUnsubscribeDialog(false); - //This reloads the page so the financial assistant toggle disappears after click - window.location.reload(); - } catch { - setError("An error occurred while unsubscribing from the Financial Assistant feature."); - } finally { - setLoading(false); - } - }; - - if (!user) { - return
    Please log in to manage your subscription.
    ; - } - - if (loading) { - return ( -
    - -
    - ); - } - - return ( -
    -

    Financial Assistant Subscription

    - - {error && {error}} - -
    - {subscriptionStatus ? ( - <> - - You are subscribed to the Financial Assistant feature. - - setShowUnsubscribeDialog(true)} /> - - ) : ( - <> - - You are not subscribed to the Financial Assistant feature. - - setShowSubscribeDialog(true)} /> - - )} -
    - - - - -
    - ); -}; - -export default FinancialAssistant; diff --git a/frontend/src/pages/invitations/Invitations.module.css b/frontend/src/pages/invitations/Invitations.module.css deleted file mode 100644 index 6bade078..00000000 --- a/frontend/src/pages/invitations/Invitations.module.css +++ /dev/null @@ -1,89 +0,0 @@ -.page_container { - display: flex; - flex-direction: column; - width: 100%; - padding: 50px 50px 50px 50px; - background-color: white; -} - -.row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 10px; - align-items: center; - justify-content: space-between; - justify-items: center; - border-bottom: 2px solid #d9d9d9; - margin-bottom: 10px; -} - -.title { - margin-bottom: 10px; - margin-left: 20px; -} - -.table { - text-align: left; - background-color: white; - border-collapse: collapse; - width: 100%; -} - -.tableContainer { - border-radius: 10px; - overflow: hidden; - border: 1px solid #d9d9d9; - margin-top: 35px; - overflow-x: auto; - width: 100%; -} - -.table thead { - background-color: #9fc51d; - color: white; -} - -.table th, -.table td { - padding: 10px; -} - -.searchContainer { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-end; -} - -.searchIcon { - font-size: 18px; - margin-right: 5px; - transform: scaleX(-1); -} - -.pillRole { - width: 100px; - padding: 5px; - border-radius: 15px; -} - -.roleContainer { - width: 100%; - justify-content: flex-start; - justify-items: center; - text-align: center; - display: flex; - text-transform: capitalize; -} - -.textJustify { - text-align: justify; -} - -@media (max-width: 768px) { - .title{ - margin-bottom: 10px; - margin-left: 0px; - } -} \ No newline at end of file diff --git a/frontend/src/pages/invitations/Invitations.tsx b/frontend/src/pages/invitations/Invitations.tsx deleted file mode 100644 index 11fff0b0..00000000 --- a/frontend/src/pages/invitations/Invitations.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import React, { useEffect, useState, useContext } from "react"; -import { Spinner } from "@fluentui/react"; -import { TextField } from "@fluentui/react/lib/TextField"; -import { SearchRegular } from "@fluentui/react-icons"; - -import { useAppContext } from "../../providers/AppProviders"; - -import { getInvitations } from "../../api"; - -import styles from "./Invitations.module.css"; - -interface User { - id: number; - invited_user_email: string; - role: string; - active: boolean; -} - -const Invitations = () => { - const { user } = useAppContext(); - const [search, setSearch] = useState(""); - const [filteredUsers, setFilteredUsers] = useState([]); - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const getUserList = async () => { - if (!user) { - setUsers([]); - setFilteredUsers([]); - setLoading(false); - return; - } - - setLoading(true); - - try { - let invitationsList = await getInvitations({ - user: { - id: user.id, - username: user.name, - organizationId: user.organizationId - } - }); - - if (!Array.isArray(invitationsList)) { - invitationsList = []; - } - - setUsers(invitationsList); - setFilteredUsers(invitationsList); - } catch (error) { - console.error("Error fetching invitations:", error); - setUsers([]); - setFilteredUsers([]); - } finally { - setLoading(false); - } - }; - - getUserList(); - }, [user]); - - useEffect(() => { - if (!search) { - setFilteredUsers(users); - } else { - const filtered = users.filter((user: any) => { - return user.invited_user_email.toLowerCase().includes(search.toLowerCase()); - }); - setFilteredUsers(filtered); - } - }, [search]); - - if (!user) { - return
    Please log in to view your invitations.
    ; - } - - return ( -
    - <> -
    -

    Invitations

    -
    -
    - { - setSearch(newValue || ""); - }} - iconProps={{ - iconName: "Search", - children: - }} - /> -
    - {loading ? ( - - ) : ( -
    - - - - - - - - - - {filteredUsers.map((user: any, index) => { - return ( - - - - - - ); - })} - -
    EmailRoleStatus
    {user.invited_user_email} -
    -
    - {user.role} -
    -
    -
    -
    -
    - {user.active ? "Active" : "Inactive"} -
    -
    -
    -
    - )} - -
    - ); -}; - -export default Invitations; diff --git a/frontend/src/pages/layout/Layout.module.css b/frontend/src/pages/layout/Layout.module.css deleted file mode 100644 index fa2216f0..00000000 --- a/frontend/src/pages/layout/Layout.module.css +++ /dev/null @@ -1,117 +0,0 @@ -.layout { - display: grid; - grid-template-columns: 200px 1fr; - height: 100vh; - overflow: hidden; - transition: grid-template-columns 0.3s ease; -} - -.sidebar { - overflow-y: auto; - transition: width 0.3s ease; -} - -.sidebar.collapsed { - width: 80px; -} - -.collapsedContent { - grid-template-columns: 80px 1fr; -} - -.content { - overflow-y: auto; - background-color: white; -} - -.header { - background-color: #f1f1f1; - color: #f2f2f2; - grid-column: span 2; - top: 0; - z-index: 1000; -} - -.headerContainer { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 0px; -} - -.headerTitleContainer { - display: flex; - align-items: center; - margin-right: 40px; - color: #f2f2f2; - text-decoration: none; -} - -.headerLogo { - height: 40px; -} - -.headerTitle { - margin-left: 12px; - font-weight: 600; -} - -.headerNavList { - display: flex; - list-style: none; - padding-left: 0; -} - -.headerNavPageLink { - color: #f2f2f2; - text-decoration: none; - opacity: 0.75; - - transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); - transition-duration: 500ms; - transition-property: opacity; -} - -.headerNavPageLink:hover { - opacity: 1; -} - -.headerNavPageLinkActive { - color: #f2f2f2; - text-decoration: none; -} - -.headerNavLeftMargin { - margin-left: 20px; -} - -.headerRightText { - font-weight: normal; - margin-left: 40px; - color: #002f87; -} - -.microsoftLogo { - height: 23px; - font-weight: 600; -} - -.githubLogo { - height: 20px; -} - -.layoutOptions { - display: flex; - align-items: center; -} - -.profileButtonContainer { - display: flex; - align-items: center; - cursor: pointer; - border-left: 2px solid #b5b3b3; - height: 50px; - width: 260px; - background-color: transparent; - margin-left: 10px; -} diff --git a/frontend/src/pages/layout/Layout.tsx b/frontend/src/pages/layout/Layout.tsx deleted file mode 100644 index 6383be45..00000000 --- a/frontend/src/pages/layout/Layout.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useContext, useEffect, useState } from "react"; -import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react"; - -import { Outlet, NavLink, Link, useLocation } from "react-router-dom"; -import { getChatHistory } from "../../api"; //FUNCION DE LA API -import salesLogo from "../../img/logo.png"; - -import github from "../../assets/github.svg"; - -import styles from "./Layout.module.css"; -import { ChatHistoryButton } from "../../components/ChatHistoryButton/ChatHistoryButton"; -import { FeedbackRatingButton } from "../../components/FeedbackRating/FeedbackRatingButton"; -import { useAppContext } from "../../providers/AppProviders"; -import { SettingsButton } from "../../components/SettingsButton"; -import { ButtonPaymentGateway } from "../../components/PaymentGateway/ButtonPaymentGateway"; -import { SideMenu } from "../../components/SideMenu/SideMenu"; -import { ProfileButton } from "../../components/Profile"; - -const Layout = () => { - const { showHistoryPanel, setShowHistoryPanel, showFeedbackRatingPanel, setShowFeedbackRatingPanel, settingsPanel, setSettingsPanel } = useAppContext(); - - const { pathname } = useLocation(); - const [isCollapsed, setIsCollapsed] = useState(false); - - const handleShowHistoryPanel = () => { - setShowHistoryPanel(!showHistoryPanel); - setShowFeedbackRatingPanel(false); - setSettingsPanel(false); - }; - - const handleShowFeedbackRatingPanel = () => { - setShowFeedbackRatingPanel(!showFeedbackRatingPanel); - setSettingsPanel(false); - setShowHistoryPanel(false); - }; - - const handleShowSettings = () => { - setSettingsPanel(!settingsPanel); - setShowHistoryPanel(false); - setShowFeedbackRatingPanel(false); - }; - - return ( - <> -
    - -
    -
    -
    - -
    - {pathname === "/" && ( - <> - - - - - )} -
    - -
    -
    -
    -
    - - -
    -
    - - ); -}; - -export default Layout; diff --git a/frontend/src/pages/layout/_Layout.module.css b/frontend/src/pages/layout/_Layout.module.css deleted file mode 100644 index 4fc41bd1..00000000 --- a/frontend/src/pages/layout/_Layout.module.css +++ /dev/null @@ -1,78 +0,0 @@ - - - -/* Layout Styles */ - -.bodyWrapper { - position: relative; - height: 88vh; - transition: margin-left 0.2s ease-in-out; - } - - .bodyWrapper.expanded { - margin-left: 260px; - } - - .bodyWrapper.collapsed { - margin-left: auto; - } - - @media (max-width: 900px) { - .bodyWrapper { - margin-left: 0 !important; - } -} - - .bodyWrapperInner { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - - background-color: white; - - } - - .bodyWrapperContainer { - margin: 0 auto; - padding: 24px; - transition: 0.2s ease-in; - } - - /* Responsive Layout Adjustments */ - - @media (max-width: 767.98px) { - .bodyWrapperContainer { - padding: 30px 20px; - } - } - - @media (min-width: 1200px) { - .bodyWrapperFull { - margin-left: 260px; - } - - - } - - /* Fixed Header Position */ - - .bodyWrapperFixedHeader { - padding-top: calc(72px + 30px); - } - - /* App Header Styles */ - - .appHeader { - z-index: 50; - background-color: var(--bs-tertiary-bg); - padding: 0 25px; - top: 0; - } - - .appHeaderContainer { - max-width: 1200px; - margin: 0 auto; - padding: 0 30px; - } - \ No newline at end of file diff --git a/frontend/src/pages/layout/_Layout.tsx b/frontend/src/pages/layout/_Layout.tsx deleted file mode 100644 index 52e6c984..00000000 --- a/frontend/src/pages/layout/_Layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// Layout.tsx - -import React, { useState } from "react"; -import { Outlet, useLocation } from "react-router-dom"; -import styles from "./_Layout.module.css"; -import Sidebar from "../../components/Sidebar/Sidebar"; -import Navbar from "../../components/Navbar/NavBar"; - -const Layout: React.FC = () => { - const { pathname } = useLocation(); - const [isCollapsed, setIsCollapsed] = useState(true); - - return ( - <> - {/* Sidebar Start */} - - {/* Sidebar End */} - - {/* Main Wrapper */} -
    - {/* Header Start */} -
    - -
    - {/* Header End */} - - {/* Main Content */} -
    - {/* Content goes here */} - -
    -
    - - ); -}; - -export default Layout; diff --git a/frontend/src/pages/organization/Organization.module.css b/frontend/src/pages/organization/Organization.module.css deleted file mode 100644 index bf3e3203..00000000 --- a/frontend/src/pages/organization/Organization.module.css +++ /dev/null @@ -1,122 +0,0 @@ -.page_container { - display: flex; - flex-direction: column; - width: 100%; - padding: 50px 50px 50px 50px; - background-color: white; -} - -.row { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - justify-items: center; - border-bottom: 2px solid #d9d9d9; - margin-bottom: 10px; -} - -.title { - margin-bottom: 10px; - margin-left: 20px; -} - -.card { - display: flex; - flex-direction: column; - align-items: center; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 6px; - box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.07), 0px 2px 4px 0px rgba(0, 0, 0, 0.05); - padding: 20px 20px 30px 20px; - gap: 15px; - margin-top: 15px; - flex-grow: 2; - max-width: 1080px; - width: 100%; -} - -.infoContainer { - display: flex; - justify-content: space-between; - width: 100%; -} - -.infoItem { - display: flex; - flex-direction: column; - flex: 1; - margin: 0 10px; -} - -.info { - display: flex; - padding: 5px 10px; - border: 2px solid #9fc51d; - border-radius: 6px; - height: 40px; - align-items: center; - background-color: #ffffff; -} - -.center { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; -} - -.editableContainer { - display: flex; - flex-direction: column; - width: 100%; - gap: 15px; - margin-top: 20px; -} - -.textArea { - width: 100%; - padding: 10px; - border: 2px solid #9fc51d; - border-radius: 6px; - resize: none; - background-color: #ffffff; - font-family: inherit; - font-size: 1rem; - overflow: hidden; - line-height: 1.5; - min-height: 80px; -} - -.saveButton { - margin-top: 10px; - padding: 10px 20px; - background-color: #9fc51d; - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 1rem; - align-self: flex-end; -} - -.saveButton:hover { - background-color: #87b31a; -} - - -@media (max-width: 768px) { - .title{ - margin-bottom: 10px; - margin-left: 0px; - } - - .center { - margin-left: 20px; - } - - .info{ - overflow-x: auto; - } -} \ No newline at end of file diff --git a/frontend/src/pages/organization/Organization.tsx b/frontend/src/pages/organization/Organization.tsx deleted file mode 100644 index 7656a149..00000000 --- a/frontend/src/pages/organization/Organization.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; -import { Label } from "@fluentui/react"; -import { Globe32Regular } from "@fluentui/react-icons"; -import { useAppContext } from "../../providers/AppProviders"; -import styles from "./Organization.module.css"; -import { updateOrganizationInfo } from "../../api"; -import { ToastContainer, toast } from "react-toastify"; -import { Spinner, SpinnerSize } from "@fluentui/react"; - -const Organization = () => { - const { organization, setOrganization } = useAppContext(); - const expirationDate = new Date((organization?.subscriptionExpirationDate || 0) * 1000).toLocaleDateString(); - - const [brandInformation, setBrandInformation] = useState(organization?.brandInformation || ""); - const [segmentSynonyms, setSegmentSynonyms] = useState(organization?.segmentSynonyms || ""); - const [industryInformation, setIndustryInformation] = useState(organization?.industryInformation || ""); - const [additionalInstructions, setAdditionalInstructions] = useState(organization?.additionalInstructions || ""); - const [isLoading, setIsLoading] = useState(false); - - const brandRef = useRef(null); - const industryRef = useRef(null); - const synonymRef = useRef(null); - const additionRef = useRef(null); - - if (!organization) { - return ( -
    -

    No Organization

    -
    - ); - } - - const handleSaveChanges = async () => { - setIsLoading(true); - - const patchData: any = {}; - patchData.brandInformation = brandInformation; - patchData.industryInformation = industryInformation; - patchData.segmentSynonyms = segmentSynonyms; - patchData.additionalInstructions = additionalInstructions; - try { - await updateOrganizationInfo({ orgId: organization.id, patchData }); - toast("Changes saved correctly", { type: "success" }); - - // Update the organization context with the new values - if (organization && setOrganization) { - setOrganization({ - ...organization, - brandInformation: brandInformation, - industryInformation: industryInformation, - segmentSynonyms: segmentSynonyms, - additionalInstructions: additionalInstructions - }); - } - } catch (err: any) { - toast("Error saving changes", { type: "error" }); - } finally { - setIsLoading(false); - } - }; - - const autoResize = (ref: React.RefObject) => { - if (ref.current) { - ref.current.style.height = "auto"; - ref.current.style.height = `${ref.current.scrollHeight}px`; - } - }; - - useEffect(() => { - autoResize(brandRef); - }, [brandInformation]); - - useEffect(() => { - autoResize(industryRef); - }, [industryInformation]); - - useEffect(() => { - autoResize(synonymRef); - }, [segmentSynonyms]); - - useEffect(() => { - autoResize(additionRef); - }, [additionalInstructions]); - - return ( -
    - -
    -

    Organization

    -
    -
    -
    - -
    -
    - - {organization?.id} -
    -
    -
    -
    - - {organization?.name} -
    -
    - - {organization?.owner} -
    -
    -
    -
    - - {organization?.subscriptionId} -
    -
    -
    -
    - - {organization?.subscriptionStatus} -
    -
    - - {expirationDate} -
    -
    -
    -
    -
    -
    - -