From 7f3e602bb301e30128ed5dc6302b140dc59ea5b2 Mon Sep 17 00:00:00 2001 From: ayangweb <75017711+ayangweb@users.noreply.github.com> Date: Fri, 16 May 2025 16:21:57 +0800 Subject: [PATCH] feat: add a component for text reading aloud (#522) * feat: add a component for text reading aloud * docs: update changelog --- docs/content.en/docs/release-notes/_index.md | 1 + src/components/Assistant/Chat.tsx | 3 + .../Assistant/ReadAloud/imgs/back-dark.png | Bin 0 -> 1294 bytes .../Assistant/ReadAloud/imgs/back-light.png | Bin 0 -> 1312 bytes .../Assistant/ReadAloud/imgs/close-dark.png | Bin 0 -> 346 bytes .../Assistant/ReadAloud/imgs/close-light.png | Bin 0 -> 347 bytes .../Assistant/ReadAloud/imgs/forward-dark.png | Bin 0 -> 1273 bytes .../ReadAloud/imgs/forward-light.png | Bin 0 -> 1275 bytes .../Assistant/ReadAloud/imgs/loading-dark.png | Bin 0 -> 485 bytes .../ReadAloud/imgs/loading-light.png | Bin 0 -> 491 bytes .../Assistant/ReadAloud/imgs/pause-dark.png | Bin 0 -> 504 bytes .../Assistant/ReadAloud/imgs/pause-light.png | Bin 0 -> 500 bytes .../Assistant/ReadAloud/imgs/play-dark.png | Bin 0 -> 203 bytes .../Assistant/ReadAloud/imgs/play-light.png | Bin 0 -> 196 bytes src/components/Assistant/ReadAloud/index.tsx | 134 ++++++++++++++++++ 15 files changed, 138 insertions(+) create mode 100755 src/components/Assistant/ReadAloud/imgs/back-dark.png create mode 100755 src/components/Assistant/ReadAloud/imgs/back-light.png create mode 100755 src/components/Assistant/ReadAloud/imgs/close-dark.png create mode 100755 src/components/Assistant/ReadAloud/imgs/close-light.png create mode 100755 src/components/Assistant/ReadAloud/imgs/forward-dark.png create mode 100755 src/components/Assistant/ReadAloud/imgs/forward-light.png create mode 100755 src/components/Assistant/ReadAloud/imgs/loading-dark.png create mode 100755 src/components/Assistant/ReadAloud/imgs/loading-light.png create mode 100755 src/components/Assistant/ReadAloud/imgs/pause-dark.png create mode 100755 src/components/Assistant/ReadAloud/imgs/pause-light.png create mode 100755 src/components/Assistant/ReadAloud/imgs/play-dark.png create mode 100755 src/components/Assistant/ReadAloud/imgs/play-light.png create mode 100644 src/components/Assistant/ReadAloud/index.tsx diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index 1ce3d2db..24b20e9f 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -49,6 +49,7 @@ Information about release notes of Coco Server is provided here. - style: chat input icons show #515 - refactor: refactoring icon component #514 - refactor: optimizing list styles in markdown content #520 +- feat: add a component for text reading aloud #522 ## 0.4.0 (2025-04-27) diff --git a/src/components/Assistant/Chat.tsx b/src/components/Assistant/Chat.tsx index bd5aeb13..c0c5022d 100644 --- a/src/components/Assistant/Chat.tsx +++ b/src/components/Assistant/Chat.tsx @@ -22,6 +22,7 @@ import ConnectPrompt from "./ConnectPrompt"; import type { Chat } from "./types"; import PrevSuggestion from "@/components/ChatMessage/PrevSuggestion"; import { useAppStore } from "@/stores/appStore"; +// import ReadAloud from "./ReadAloud"; interface ChatAIProps { isSearchActive?: boolean; @@ -378,6 +379,8 @@ const ChatAI = memo( {!activeChat?._id && !visibleStartPage && ( )} + + {/* */} ); } diff --git a/src/components/Assistant/ReadAloud/imgs/back-dark.png b/src/components/Assistant/ReadAloud/imgs/back-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..c1530df0a9888806791e4890941973acbb2ed5af GIT binary patch literal 1294 zcmV+p1@ZccP)Px($Vo&&R9HvFS80eGRS^Darn8F~H6H7+>IN6L_f_`}D69&iNHiu<12J9+CMud> zK#VBMNse{=sLUIZ)R4};DMrOJfd!QmtD)uGEC;! zNieB@)9<~i?|t=E)f>WLHag7lanJz5FkDSU8{#;=#P|I|rx!Y40HsptggB0$0&s-} zO_F4x@B6zB2>@&DWdOE{$Vt_JMD#=)$D2B3p<@O{Mn;_8-rk=89MkBwA~H@y4;y1% zAfmV(BW(ec%jLc(itZ+&EykFS)nOwB0K5&r18{mJ=!XEVXXdY3foKV!R4Vy#9KQqL zGywN7^ZKeYYi&=GB%6uo%32LrYnO`1BLED5Ux{c@6?h{cO#y^qs1EcN#{t;U-QB&c zzrX+QdM7~;+(bm{lO#Fc_x-PRePUu_&gA6es{k$n@Ed>&nR#@^04jOb;OB_Qc4l5R zlYG_&C=?1^vu4fOCL-4X2y(gHa9>~FzIqT183@C09e@XkXe^V-47;xTPdhO+TCcr` z`eWvM+X7JJj^lg_php>C=1;4KM1;!aa*yk}S_RXlfvP{|&6_8+Y*}m16p_y~71?Zd zuIsvcYtQb-Kp2M0QZ>H>-~(p9pt^Z%Y-}iXuth;-v)R1sy5qVQhT&TPv@cBKB%-U0 zF{*(n3&T)_yb!=DX5QKkz>8_lmJ`t$V~ln>jE|2mNK>%`z#jmP2QZ(R-xZOpwRSRX zgi7rfx>rP!IF3j1`MmC(vLFc75YZ+8FEjJH$=GMW~>N=ur_l zkBIIPk(Hk3Jzt}Nqck{xznD4K5`eX~Bq9TuOy-Qi!ND(EF`#ROLgD10p&?B}A|e@U z?H&<%-}5}J|EdNCiO3RB*%;%t1F%y>7HEq##=O=FK&8aiMp1NIKA-=$0$_rO-ZRGB zT*H8-=M?}unR!vW6x@+cy2prUTdfm4RG)$%2!@GBMXR8<6Vat_xsK!L1lyM+iR=6R_tgXCa{0t4ir#b_=c<8$fjyNHTR}t* z0O$vx=Qh;Vy0!MyR1+lt-BA?v=kxh*S^}sb)`p`LZeixt&C09xyV9>ZsNWj|!Lvkk zV>4e3XF&yZ2;fKntC?8=b!^t!TSR0tfZqWyGk;g_q}4cH0zliTzG`k|=4U!VPznBI zI)t+6ELiG!o_5mxtmT6w2!gdlq@q!Jw*gpZjL|{W)Res>Ngk@uo)FRP#+Z%G4zzue z6^q5iNs?$&Ia-A&B3e$b1K902&Xho-DC$igEyL-zCFuyi7QhiY{fKCVF-C_~Q)>%A zPinJq9OpjO1Ayt#+r(>>KH;8@qUip7KL10zw9N=W)rr!p{{uv1F@OO8r&QKdT;+6# zy`k(h659c%pxq4)cD*A84mMcFRq%fVoN*sG6u~vde+B6?Ke->*761SM07*qoM6N<$ Ef?v8^KL7v# literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/imgs/back-light.png b/src/components/Assistant/ReadAloud/imgs/back-light.png new file mode 100755 index 0000000000000000000000000000000000000000..403f1ebf0f812a1d44f46c4e82620944f4092672 GIT binary patch literal 1312 zcmV+*1>gFKP)Px(+DSw~R9HvFS8HrkRS^E>c5_W}eYd_PNfUp=iK#It0-_0`Qi%A7&`4s$ zX#6V@6;OZpLlY$dwW2Y;`a=l{5fW)kgmm|ud#%d)sx?Y{1f#JJ?W)}~?lgCsb?NSQ zA;SFId(O;!GxN>NS;BD^I?n!a)Bt?n?_}mpM0Bmz`m(s{Q--kCuX%Gy_VhbpSR1I50RksQUZ+|Efl& z$<9jx*3dX6C#cZKN=ewxj0*iM55JkoR6gx zu~=++Z*T8VvF>3R@H}sMp3QFp_{bP@W#Mu%nY`3m+XWzLt-UM^!;GAHp0|^T78e<1 zX1*l|0^z{8D5ZqR3z>O!5Ck$mCgd;xzVE-m%qy6AO%Mb!>7?xzF!K%oBLL1|=J`Po ze9+O+Q8zF!@DG6XMD(fLwbt5fHoG^I$;jPt@jP!05p4nRmN8~UH2`U5_64{xCWs7N z<2cTI0DG8uZvj9wjt^ODFAu}8uN+UJc0K^$D`SkbXZZ{$rKG+Mb#-+OsZ{FM;>*78 zw=r{X0f6UubBHKIL|d788GtMiZPr?ED%NXhX*tbx-QNHV8DpeHD+2ffz%< zlF8)R>2&&#lplbjlsd@F9|l1n{XZ^kZEY9la=9-7^c!PlRRgd)-vJ9`#2RDXsRSTu zv9+V4qxWVqnZF|d-vH<_#;hvn`o6!6nY#h(HpVQf2H^hurh7WSRJIix5v55upol0f zhGk+h^L0dY2N7Mbwcb^5q?CG9%97s=4;W*fsRm#=0NG$NHfI@Qekv%;ojdnT*LB|` zqMOs{^g;QqlvPy$q_NTnR&alHW(Qh87F8w9zQ1*iz#Mal3(E+ z0P1D?5z$Jm_0CFws{#;Z88*3G?h)YwfQi*x#_(AF3HO5IIFF{&>7T2WZAt(NMxyk} z{{YF#%>0}1+~`cm)t?^~?^jj`qB!3>>Yqn!Dita5&{Ta4ejQkADH} W2}M7ITAsB40000Px$6iGxuR9HvtR>2K}Fbs_0-z|_}gKp3XIzhV!bdPp|PS6dy0Z44oKcq^fNC+A5 zxf139&&a;J*gj%%#vd8;yIsI`E6}#>q3gPHp68E$=A>zwOPZ$7EXzI|4qJgh-uL~> zdw=xa--@EBG$QJ{u8624qIZ%cCr$(_AgFfE-6N?+L?j;wzjD=_oPjX_A_B>s9YO>s z-W1JQbO8d0#74o30z#k!SP~<40T(z&pd}AHq6*CQ_Xk2$Y8?X!;My^)7UC3W2F5L6 zJnl5)a0{kd5zbc=z};Tkf4BnNw68XVrZ$xLToR-D5h1`xtjLBMKs0BY&$0PJV!o1v s0Wn=lH4CUV2YwB;L{oXY0P_lb0kax1FpyENe*gdg07*qoM6N<$g1W?uF#rGn literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/imgs/close-light.png b/src/components/Assistant/ReadAloud/imgs/close-light.png new file mode 100755 index 0000000000000000000000000000000000000000..487c1e9012d6941f8e0343a521670aa715d9159b GIT binary patch literal 347 zcmV-h0i^zkP)Px$6-h)vR9HvtR?!WDKn!gI-%fK0Zr}t?pnHgW=mbvS23~U0_Mi3#h=U0@~uWI5`YV?KggFtk$ZXEmm;@F=04ue*leLMGTBF z(fC|6xk!Y=HBl^RrvJYcP-_2~2$R~7<8w}o>W@eQEQw{gKm#bvS>`!T{vb*IB#Qz` t`Y4rL$honqV>Gwu+Xek5mPx(vq?ljR9HvtmR*QdRTRhn>zpH#W>(llCKSWWb+6MLDl7wi(!@R_5i2DPE3GUr zDthWeD=Oc5sGuUl4z@CYtK@Az|fI)0}fxcga1&?Rr1v z+Ji6cm&1Xz*8c6a|7)#HIKzw1@cuY0fG`XL*L6P@ktdjW%YQk<=>jZr9A|%JCGtx5viD?h1n7?{@B1bz`mFN4=E*0@%pR&(+2`1t6c#PfOGE8%;eCU1^N@B6AWEDijJA)Yg1oU!RxR%%4AB zs)4PwSL;Cl97~d9W^E=<0kGDt1)$w+A2Z*O1wMN8Xs_$KTLH{*T{p;tWv%_Nx*`B5 zz%9m@omHYdRH>pB%=~V37U}`)DC=y6h}`G<{^pA4RP`SKI00ZffQ8KbNu@uG0eBL? zmuf2_r8G_Zf*?@alPij%bwu=9SyMZh`Ly08CK`X8yeyfVI{Dup)Aph@RJaC!$A0eSB4TQrM{Nc)(7?dJMZLYfdJQEJapE{07LkL# z@2{*X$L=&u3q({f#^hQ7>=%(m0G2cJd(AQs$MF>+@<5U#>$SWp0ACT&r^c8ynQt7& z%S7aTB06ApU-9LL*5-S%)5x~W& zZUE;Hk&fEeTN2bUsOqXGh={ARnVB~n$59k5DL0s1K&rhgJivkyi|T%IC>q9<2+vP6g5JfsXC~Lu0%u+8e?8=l);uyvN(fY|_+647!M9Dv^($5}ON)~vnFuCFCP)@(e_d#e1zx(C4c z=xq>KAEjH8BzZarf*)FGXF>p35NoZ9b7^^~+*IxpJplAy*Y^Ol)9n?JUCgZi6IyKn jOekWTM*jnx@eKR}%!)QYQ=@G!00000NkvXXu0mjf5lK`E literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/imgs/forward-light.png b/src/components/Assistant/ReadAloud/imgs/forward-light.png new file mode 100755 index 0000000000000000000000000000000000000000..d5b93de38eba4fd8817e2b706425733c34a8d7d2 GIT binary patch literal 1275 zcmVPx(wMj%lR9HvtmTibsRT#(r&%KMS=I$1@kqU+EikY(8eVLK<(~?*wNm#9L;}T4-YmvR0gw@n++nw_?C+?N&I6Jdr zANekod0tOXL!*WJ|CwA5QgDm5&4ja9#l&0_%A`6F2HgTc|Sh*Eo01W z`F#H8(*bE3z(x%9bUI6C+ z_zb{(%)BQI!*mb431Aa5A4?7jf?z9vM*)0mjJYD8&le_xm=wVG{T?FP51<3UJ^-tk z`B&@6^Sp~3#~BfkEn4fBs-5@u_qP^{#n(h+6@Xt{*X=ua@ZjgQAZh~i_4Q4kn3!My z3jw@ZDwS?76bci~)YkUR;lqbt1aPB>c+R4jafK09%>)@oGP(04!Ouq&O0@x-`|qh zbar-18rbu^&~cm`fMcanX>N5UPXP$SaHEJ=b=%9#D-y?E0+=Nt%e2-X z#dZE0z(Yjzv9&eEnDO!PeT727+9m*-i0FAD+O3qjz7c@!*7?SmMX4w+a~x+GfDc4u zUjpEHUZ>+YM{AVoq*4b#-~(VHQc9^zBLI8V+RNqgxrYuNI-WL;b>Tn);QRhUA}SEk zb`iO*q7&mp^sG{9TY6tMo1IlIm#t15W9H6A0Dl0O(c0SDJ~A@$dp&?K3>S#VrvMHR z(GwNX>H}~ufE$?km6R~e0AT(8$;@Xr0{8+zx9hqKs>P!^1J+R{ler+5%N@1luc)6B z1i=vz*{`);n*!(p@TL7R^ZZ5tcFZm(q5-AU>-93=d0wyMICqvxrOom6vH)WMhM9Ro zstH#U(HpjhnECQX0C!ik|4Aa+rIcFVpa%jXvUl7z=aj#uK@fNMbZ4x(b4ac4LvvRC~nAwWliB(gIec#_fL>p~X%xuZ0vYfaNPO?pW5z$Vil=XSistMqE-Wy6`8M6{+y7Hq lO#w{FW0OYz1Dx>;`~z-?S3hhvuiO9t002ovPDHLkV1kS2ab5rb literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/imgs/loading-dark.png b/src/components/Assistant/ReadAloud/imgs/loading-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..c09b85ac29e2acd7044229f451bd1d22872a9c69 GIT binary patch literal 485 zcmVPx$pGibPR9HvdS3OGvK@3f%uolEdMYPy+X7MM8f{m5cf#6>htgLJV#h)NI<6WVM z+K6CnF_FNntjBz0SC;{=-Q?xv>rD=!@DB^ehu#F7b87(bJWpUlYyH|Mncf7fwOfez zz(I*<+ZglszX(`skBI1{uIsx{X3`2wCX-!6TsOvChH!!{;NXaO0RSJ~`@;|k2>}kS zl)3_dWgX-&JPx$rAb6VR9HvdmpeLl^m6Q0)I>6daL`y!WrfHse&XS-2pt3C6^w}cf*Yxg~0Te~S!M!7~ z)*c#TZY2O(>jS0KO~95WlZpDVgvkMtk3iDeA#4^phSu6$p9XY<>+g?YSBMbt%%=r| z2vJfgPI?)|E|x*B<5+TDxIrHz$)99gVbVvbL~vpwki-x_NPhDykAaAla6VA-zh<$@ hq~CjYK$;+C_YHP2!Z5#)BcA{O002ovPDHLkV1j}Z*@gfB literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/imgs/pause-dark.png b/src/components/Assistant/ReadAloud/imgs/pause-dark.png new file mode 100755 index 0000000000000000000000000000000000000000..bbaa1adcfb39d499714631f5f5a5518c428067bb GIT binary patch literal 504 zcmVPx$vPnciR9Hvt*F8(aP!tB>^O91e{)P^vP3}#G_IEfrI`|_jRB6$nAShCb4i35q zf{213=;Y$yAc#0v5ZVqBR762U#Lo!!*p>!Fv`v#VUCf%xeV?3j@&)Sifj;NcKcHg; zOw$~Q#bWQhYE@f6wOSov<~3$s42Q$h@p$~vZ%A7}xm?ZwD7HfH08D9`wgRB$7o;12 zoq5X4XpYJWzD5hsTVg8uK+vqx{k>SA=UwK zHxR!7%}fACL^LLZIChHg70?Rh)~%Z}4CAH~q!$1?^O>1vL!r>Ts;bsskX{1pOp}?% zbzR^74G{#u&fS$#4*x_13D}cT4)p_Y2_P$k*bKS_uK)^>NTd*rM&AM*LI4|zqGS_^ z#8uF@;Rz9?rId%Ag6;Eu4dE>jO$s4agMJi$5YYk=%}6PqoL=?!bzUm4qbN#7Rn>Dh z!CqQ$1|Thj*!L3b25?{RZ(h^1WdNUkgg4)y?KY}ZDx*Zy7_24BvhvAf^1-iQM-^x+ uDwRruZohV%U_1U7IN8L@ntwpwEAR!Otg#`~8+yh70000Px$u1Q2eR9Hvt*S|}{Kor37_p&tLpV7TT`ww(=a&+*Iuu!E%hk~F;5gi$O_#tyf#s5s=MhM-g!Y0G0t@hI8(yhI9nva=9V^EPO}rh-g{}u?7I1Do8f~zj>l* zTEVicLluZ90KaQ^o;PXR_C*{>9DwY~!@1CIx91y;#&Z-%T!7!aCZe(sViN$U8;B}^ z-#kLZamLtjScGbT?8*tqN4Ly5zX^i$0^m2F0AS8>oW)kF_5L5EmjJ(cPDB$zh@HO> zNdUgOM6@S_82ODz41kFHjIrT<04@=+#2DL7dJA5NsG3Tps`YyPHR(gRrDjeI1|_ffFkGoEJ`@e z7MyyXSFmmSAWk?6;GT#o>2!M4FpLisae0rRm(nprlm~0Yb=^v{*?dqH4mAO}D3i$y qMSa@|gAM#&5auRv)(ilBpTH;TAh#iKKw5_Y0000rC7 z*!zC#wb=&qu1ajKd0%`^>sW=yV;@c5Yf&q&2mcdK&}W&!&3MkKVIl*ELIVR6lnBr* vSbtry#W8C0+~P~S;$r`QdzO7I<^j`Z*$#us-&JBj2QhfM`njxgN@xNA`}|9# literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/imgs/play-light.png b/src/components/Assistant/ReadAloud/imgs/play-light.png new file mode 100755 index 0000000000000000000000000000000000000000..a5378e9f0748219be4728d2fb0d3fac4714150ad GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}b)GJcArbCx zuN!hT81S%OU=xY`VzkrY9e-r?L@riWLDuc^3xlWM-`Miy^QI+}tmI$*sY_zeNJw+m zOl5z3^G+@6_eHDJrP9}yp0ivWm;W=qL78I%3*$P414kG{7BH}CI52VrG%$%QaM*lc tbKL4z9oKTo-CtJ~|Ghpte)Bsf^FOOQ46cdqkN~=b!PC{xWt~$(69E73M|l7M literal 0 HcmV?d00001 diff --git a/src/components/Assistant/ReadAloud/index.tsx b/src/components/Assistant/ReadAloud/index.tsx new file mode 100644 index 00000000..8d2ef94e --- /dev/null +++ b/src/components/Assistant/ReadAloud/index.tsx @@ -0,0 +1,134 @@ +import { useThemeStore } from "@/stores/themeStore"; +import loadingLight from "./imgs/loading-light.png"; +import loadingDark from "./imgs/loading-dark.png"; +import playLight from "./imgs/play-light.png"; +import playDark from "./imgs/play-dark.png"; +import pauseLight from "./imgs/pause-light.png"; +import pauseDark from "./imgs/pause-dark.png"; +import backLight from "./imgs/back-light.png"; +import backDark from "./imgs/back-dark.png"; +import forwardLight from "./imgs/forward-light.png"; +import forwardDark from "./imgs/forward-dark.png"; +import closeLight from "./imgs/close-light.png"; +import closeDark from "./imgs/close-dark.png"; +import { useEffect, useMemo, useRef } from "react"; +import { useReactive } from "ahooks"; +import dayjs from "dayjs"; +import durationPlugin from "dayjs/plugin/duration"; + +dayjs.extend(durationPlugin); + +interface State { + loading: boolean; + playing: boolean; + totalDuration: number; + currentDuration: number; +} + +const ReadAloud = () => { + const isDark = useThemeStore((state) => state.isDark); + const state = useReactive({ + loading: false, + playing: true, + totalDuration: 300, + currentDuration: 0, + }); + const timerRef = useRef>(); + + const formatTime = useMemo(() => { + return dayjs.duration(state.currentDuration * 1000).format("mm:ss"); + }, [state.currentDuration]); + + useEffect(() => { + if (state.playing && state.currentDuration >= state.totalDuration) { + state.currentDuration = 0; + } + + changeCurrentDuration(); + }, [state.playing]); + + const changeCurrentDuration = (duration = state.currentDuration) => { + clearTimeout(timerRef.current); + + let nextDuration = duration; + + if (duration < 0) { + nextDuration = 0; + } + + if (duration >= state.totalDuration) { + state.currentDuration = state.totalDuration; + + state.playing = false; + } + + if (!state.playing) return; + + state.currentDuration = nextDuration; + + timerRef.current = setTimeout(() => { + changeCurrentDuration(duration + 1); + }, 1000); + }; + + return ( +
+
+ {state.loading ? ( + + ) : ( +
{ + state.playing = !state.playing; + }} + > + {state.playing ? ( + + ) : ( + + )} +
+ )} + + {formatTime} +
+
+ {!state.loading && ( + <> + { + changeCurrentDuration(state.currentDuration - 15); + }} + /> + + { + changeCurrentDuration(state.currentDuration + 15); + }} + /> + + )} + + +
+
+ ); +}; + +export default ReadAloud;