Zl`lM!g_
z;D(o5f7@Q1;Z`ITyYOfMQeQo}JP~mZV9s*QZn*iwM=nL2%Xwi(s_}+|GUAG_w*ZcZ
zaaPn!?;C+Yk_mr-R7(`$i3rgV-jh6vC8qfukSkY?n)Q;vzomNT@>f?
zGn`f?fgL`aPj*avu#b6QiV=%3biKP0D_387xC3(@Vl!TdM>-Sc{Fl(*oO?01A!%Rt
zCeb33Bg8EBe&6u)5N&Pjl|?MytC)?Mjhy
zF4yGaiZA7$*mSvKW@79&=HI3P+?xZ6IvtT1BKQ|1#fCsgj3#sLM+CD0UKi@J*Hu1y
z+9<4tOuUw3#fF_3c0H6DT?Yi_`7(o8M^fAD(
zfIoj6B*@yxDp~32+-kb@87uF%4{w*b8Pec^-sVt4yV_n1@Zx^_(XZdx;B_A~wiom#
z48`fUf*r=ntV5-et(Bm>e@Ubm9t2-gv}K@7w}@+53I$gU65?=b$<7$1pNJ!QQ7c4V
z<$beQ?8CJgy(Kb?Q`!k*sU(4D4ZCKq1NQDVq$+w;9m=V{saH#l^Z0A^Q~#?Q@%x0V
zEl@B<`a4Fk=_b)3;JEW*#dVF~>GKH)hfw`f`h#@pQ1bvuFPY@?!SUsrloo+e$-FWS
zq1`+I4%HkOcJ#|aCL(4rfoZ9~Ch6%8QK0JZm$1oMuLJsfAn~b@gj3jc!M
z%Cm6VyG<{bXBnz8;4O
zbUjUv8*$#Q&qTxqk0UKdi!G_J;V=KKe|c-|3m(y9yQPJ{fdb#GJL|>W)#adE;I!
zfB71l?^H58IvIWEo5i-raXh$6&HynZDGK32h@=KSBk91~c~G;fBe5w#3cd_N^aZ81tYBgS%;I_Q3*1
z=nREfjmeMvi}{ldZx?4z%uc~od0VnXVE2%Vz1&_P;agf-Cztt(yR|hJ5KIV&q%gVB
zo0MI3E6xu??`_1X%aT<5d5e_);OO%@&^)czew1K6SA)b)87XCD(12uT!@*jx(c6{<
zr%Dz&TS9veziF!+J2i-}^%869MEiT5+*XV^;J&@MbHDUhigj7#NtE{y@;YrgxiiW;
zP^f~zXY?HP_^CBN49@4WWd}Ztkn8LaUvhzGR&iEQn8-|Hy90J>gZF2I$|b?u#bB88
ziS$cTPjGpzvsy6a+~Fd;>u6U$wx04Ro+G73vKHE6)7Mw%)E}>bngm7P5}B}KZS!vw
z9mvoN2bW|>vml;OP5EPMMn?CpDB!#SL-j#kEri;r4sHz4KE>;s(_X^zMw4RrX^6S5+}J3#ic4KDN$Ua@M_g3uf)?352*H4`
zsmu*z+IpHk7UDI7d9p_*^Z|FzR&>q&!1u>OrXtn~%HvL%UW(^y@yq{!=p#X&l
z)y{u9(r_*JgijCoL*;qYLN)HGi{o`4QpMS)xwP-`3(&fy5M-Mi2wh?+E)&eNl=+ld
z%2>!1p_^IPrKZ*m)3o{vqEVk#df&wMkbH&&poS!J~=9Y}Uh_9U_?q
znq)h_T=-mltT`n@KJD!8oS>7AUB-4YT={zPI4tj*hH)>w1(>-E)xb!_o`xS~e%Gk(
z7k$siU`dbu;9yPfzBjbuZfl@-ku3bBo4K?>pU8@xz|CZUh35>NjC_I_It5-SCGIdX
zk*7CM1XS-pp#L_=2_FPq$I#GWD)_h+1RHx0xOR;v<>e?gvSHB+KV{v-qn%^xiU6Y3
zo}ZL`>^!5*`P%J=%6PRk8BgsmDR||P&a?ismh-%8>#$2u++5|koII|T+~1f%au;1$
zWsbkEu+AqS?DjhO;Qz5JBq%F1X%psv1|ZxSl#xH3=UG-rS|DX44xs2UN|lQbB3liU
zHzKonUNiz*zb}$2`X66R7ibsjK4N^v7u)z{!mxB=9g(9EH#VBA!@xDv
zcSDrAaFUo{UrSd((pFg(I$_^v5@hj}<)>Z;Vf?|j4NgDUq3}5&F`B!N7uT$lQgp+n
zg9{t|3F9}}$tFvn^hhiCwdapS@&tx+G*1YQF_%kb?1Zy0iO2Q4_D7gS65Y{{{teSt
z98;?e{#DMGj#0@c!_^H8lZNAP_pA2vViL}bo7{fmThhxTkI^UyJ0Qs?aIr06=*uc{
z6~Hpm0~L;|M6&E)K_j?mGk1~6`r8RN3z9l9R?RoYqg%P$9m^~UBJDRsWMTU}b<;IC
z+nZ?b-N$A`HR$J=y6s+VygpGCm-KkNv$cni5u_}F0a=*rz4{&FYK6fJ$t;C6fn~P^
zHNt4TA1%Lp-S6H|vO%XgXC21;f{s+P4k_e0V3-_Po$eZ%9;jYF-Vf>|a0-l_JQ^X2
z3v|I^Iz6k|z(}Ook*U(PN{^L=>#AS(`EP)j^h~LW{Lo#x&J(4TYt)%qGm<)`SY)9-
zKu+O01(L5AY&^EKJwep_ZArS8D~~+PK7q`_&IpYMDu8fsyKGV*Ln)?Un@)9Fkc5JK#K(nj5Yu=hYOROfyRcQcu+JBf(3i
z0Fu*xtbB?5KOB;os<`mEiq&FoJehi1$fwb$FC+Ai0$4^`SOgQPCH(4w9$ZU}QL$~x
z>ErPujv4Y}=bhV3z_^4_UjQh77=I?AixP+%oI|#`v16AEC^;f{Uc+PYy~Mlk1yd6^
z9P;f0&7gIf*%)qfXB9J_W2z6wSvCY8t5T?pX8A3~d@NnkisATK~!8B&s9o@i41ID^ZwgX+R_p^7#PvQgv=#FD~~
zLq8H#*_#Oosn`dmB(d4v)8IeN3puJ%xfqKbK181i3oCav;&-)({FruiS3x?gO;m3{
z7CN<%P&v&HhYVZ!sf*qfQqy!eSum}ysrT6hC^tIbgdwsyoWe6lq0;NY>R!%z>=MFD
zY3Z#W(g|iOOCoFZt*F77kWlcRwI6U1h#US`G0^&mj&*j@d6}8OtaxZc^9CYGj>1+L
zLsT1azKgCl$*}`08Nc(-Z$G1pu7Z<=tS>M&u_fu*hiDCjNtO(eoS&hB5zB4-&<=*&
zDo#Gsdr#NfNw@b;>~NJ;6v+tuyT+PIyCTjcg;{<{1pB)Fa}`t=gz}O&eihRm<|YWC
z^UAMN4^2HYWpf9#MvxM-K$d|0)Dk;!f8b+W2lRQ3+u8hd_{iCm5*x+;9>7XRj
z+6$ld9*~m88B0N{tX@_eo)nMe=u8JV9Qlon`oJbRTb&eCC~1YD@JuuPD}S(B(v-9@
z7Z5=|f>3t7*%_p?oBXEHv^ah;p$i5@Myr3VP%^4UbwE-`j@?9|=p0UhEfEJE(
zuAsOuocY!-czOo7{4T=coH^7T882GkK$nKGrSf28+Ds_$3iw2EFUfX5+p|4Cz{ErNiYXxpF&EWJqhrwa!Wk)#Okeg;0gYW$l31cXe1G>7EgQL@@#4&vR#nu}_2INN=gw$D=?
zcX>^1weuB0Rg1C0eI*3RK@>A=+?yDOPdkhr7(RzNylTH$u{7r{r?Vq5%bxjmBxx
zsW&b{!$sKPg4rhf@rxS~Z)x?5+NNiJrT3%kS#Ev~>e4;LOp-_ua4xsFj`tBUoyd|s
zu-QswoCLi;*}kZAIa4<5eYv<}5QvM
zY?N0s;Q%bWfdqt%wbiq->?W_e=hJ#Z&d;9-aJur<4KbE?v*@I>ovlqbQMSutwD#*!
zXC{%I?o@*mxqM
z@Uqltq=u0+4|27Aul{ySrc?^1K!`xyDne=5k!9qy&j-x`7FQ=usw^6!DPf=PXpPIb
zLQd3{Y`9mhZ&ef@J>f4yEp&RQxzi-GkV$uhvWddKVMkb0I|($m{Nw`56?|9}dJv}Y
z;f{5g#iyE;s)DRL56_mjPWdBWyOzvMk!!A=)LiW&snLc|ynzmxrO^^Eys6-wphPpl
zpColwE$nXu#=RRSMaS3R@(o1b+7hj-w3|DlgHpiy4F(!&7L#Jx7co`UH2!xxGi+f@e+hV}DcKT3|5P*nVj3#+
zaU5h3a0xx@1kz}3MihI4GKNS(gjmVhKJw&yulHik;&^7jebys=&~eDN`%qX;12uCg
z>J`oS1qNwK-ZYCx`fKz?`aOI)JdL?v4El4^S|Yg6Z=T7z9o`Iyiykc0%SuXV+3>jf
zTA>H!yh~_sVxXHjwW);)M;le4Dw|Rvt+IPjHQ~rFpRY4mC3OwhuDG-zg0zff7+5>I
zc=55_i2P$kC>M9-``Y5cg)~~|HIktFXDbhW%#eaj!HDz797-%*O+o{y66T*|)0qFh3;sHuA*3wr{?QkF`K&13PuN
zf_iXCKO|Iz6&j0e)lBZgH;tC|9l_?7(kbs}BR9bH1)=uadf*auGt2ytFm<0!n%(Dr
zHt^)OuUS*~MC69qJmJ>L^tor`)5L;{2~LU)
z1GgM3dk7GdUl!*Atp-7UU*T~aOu$^+P$~TgP0bFax59v|GQtG_2xZg6lvuD>f`LTt
z3NR`CWM+NyKq1BDrmm0<0W$-Kv)S?fJ)?+uGv{U-A73~3v*Q+3@3<@Q6v?uzIa%&fL2jFqqTlpBiF5|yE=Wd7$#U#^;c9hyi{~)b
z??`0j{oC^x_?ZhA7Kd`NIg}{FOe&%xUr(vt(LhgNCB2xfz8Kn9g=8Bd!Du-A$e+xd
zT7+8%@xKG`d(2To6HfEu-QZ}W1t`WtR^e?`O%N*N8?Q0^De#g4Wghft4d{HFF{u&x
zfsc{doA=jC2ycS|LXIG_uJe@KZC@v(N$#B6s^a&f8dje#VWCdE+A7f(Y|ug#1rgRr
zMiEjCj9$tVQY09)&M>DBJ%;pKGnLa^p(@zK4KDLTt@mAxUgKVJ0F1|Q8Iy7KZJ|^j
z9IA0e$sol$D7hH6#Tj{G=srg6^=EZTLp_2~d7bMJS@vNZsp-GdD?H1x>nEdxN^~De
z3*QA&%xr+%tAE+n>o01#NLfsP{HKJVZg*IBx0g0v;$V}o++la@@7w+f4_2N8Z4R8#jm}7$;U(TKV^jt)HeV;t@_e`iV
zM&EKNg$<>(StuGpgFeCc$SNBNv|hID;ZYeo=^xA6K17T4)t~3;A7s_lcco^7shM9d
zpAxC;eD4k2wyc)izhADKg*9(Y%4D6L{7yv06r*>^N_N&%siJG>odugTxtF0J{yI^T
z;Cf4VK6?M7k5le5_AFI>y6OYuG!efy^(e-z_w*U5|Lm;#D%#bB$fT#Nb*}iJh912n^E`^L-H9;1<3w6?ap>MrAs(&@I`z|;Zigxm?<0db)
zi=>J9#sIm6r$5w+w;I`x5{HMWv?izsTnXM88wVeMbCHrka;@PI*k%BI-AVpA{a$53~Pk5Ce+b1R$&PUMKJC~2QgvtdQ~}L
z3!I$;Q1~gob?@c=>J}}e{bCA}U!zQ~;^N)sl47=Xy&C0>GqU+HM4cwpBat4Qz7KyRkwdQ#2%791wBu?xt4?)azzMQeQ)ad4cvBr4NLujN7jBDh(r5oMyWmz
z7D2+mwD;V{tu?=^xh!s?mdo1E+i#=Br**aJq{Rpu1H0-d2!d9t$zT
zv%aw^=F+2NF8HB
zk|paF+gpLsEaUIb@Q|%#Ze7uqxF7xo)0dskMi8YFfRUNin`|Z(__Cui-GkKWzGXo^
zywS-R_?TBXF1)%B3L77J-8XsSpTBw8y!wnXi%;Z<&nkQ5p&%0UEv2It$iBf@I&zm2@1)&8mE!VJZ
zS65=fO}s&)}pk~|ss7bSPC(HPJ;iaYdsaZ&K%;Q}@{cg72OYA>3F&0Y@%%MamoEgR5kjk!`1~UlBkuFk4!a
z;e22!z``QxbuerMMzvX2u1)K8n+N(DeSM^a?j1fAg^%Hd+%lVY8K8vorz<{w-!8Qe
zqub5bkm}k}b;`0r*9g>}_gsP!fklQ5Ayb&494*Ez^<;&@wSs#!o_nGnPxO7N3Ne_j
zlJ2$&d17|jA3R3$z=>pr=nyO!83CI4LZbLm=B
z&)BE+wJ7i#AHq`?06NqRIhg2PpYrbQ$HVjIZiB91}IZqj{bk?zG>vk4t>-zoOye6yO?x@C|#(Cd?-Su@fZF7tYEinVZ<6rz&n#D{5^?#G%tdLL8uB!~3M*u*x
z`m_$#st6NWNs%7WL5WsJY2c3W7LnB=cUjMsNWNW%Y7B(Tr(wF)LeI}vEovbo?mNe>
zl{uczN*x{taF7@4ugOI)*UCWomP1yPV*XSHUad4Y2R5%$C0@x047C})L}`v5=)li8
zhekzoVzVzq0+$5u{RG^dW!vohCa&T7zpv?jivEBYd@exY)2yYo(J@i^G&^5mkwV~x
zWGWlL!JM|egLR3!$c)=W348}h8P4|I4(-kT=yu@i4a+0jlL*mJLHXHOT39ZL=>Q>f
z_@xe5cS&oPbqs^40Fb>y48|2aEH}ll^BxQ+#G-51E8d?cs0|L+{R`tL-*gq2wieXo0uQo5~kqS
z+WsThNyLX*$zoMg@;jR~)d?pVirB~iw334>!nk$Vi%$HSP+1{|I;uoOLf%twner#C
zOdiU_qP1DPbt%_ujZqHz(nSBO9Cr7|TDAAo_5rT9-=&P;nP!L0fJCl%P3QC8t^u{T
z`)*nFb5(QpduWdP+}zp{y$8u`cZThCkYa0YL&&J6`JN@DB$UZ5ZEBJ|le5O|x2FjL
z;{Fl0-sLNykNEEG_-%P7kYAI7lHzr7Ie)v|wNWF}tiz&VvE!+Qlw+TEH28j(V`gZH
z4eBo{+(W50QCz>i;U4u(R9h0xiyqsJM-}#W
zMxR#%X0nM0Exottz77^Y|R4GkDmkCyz$4XjFF$mAT5n+QmyG^&*Vn43<1LB>$5arx~PmE
zD&Mk#)Q`}RqL8AG;LZ2@A64zIRN6ce`{IO0N6hb7S=l4iu4l3}y{gANeE5xgJeur1
zt>|WvR>&9%mrT6-x@DxhO*>7Ua6WF2EXG?#o@lsN#c)$9i
z&w7>u(_6RR?>Q3MH8ohyavZK%nJadL+`6+DjE42rv;_o?ws|za&Rm$T=^v`;d(c}N
zaky!KPa&bb6CK>Wh86S(Bp4~ShQ;kHjh$uu6DOVcAdOq44
zrZcU3qp5BR$3__}^UGW?`e%#OLAi->r&vm|&ZVw02H!K&Cy5OL%pP~z8i$bmJfVaL
z?yl*P88%I>chmOl5Tm7QzM&ItxpUgl>~A>i(4WcMDxjc+NlEjdLz*eH_C37_>8n3K
zUa4zy3Ow#PfiXN!dEULWt8-WRsk!wX(7q?&g)!lbyo$mA0ZI8WrVhxd{N>CEvLTLI
z4`B+6g)SuBW8zQsH=^=E5CFrSEE(D{x%e)%*q>714AUlfg#+VglTzBhsNV=(R=%cZ
z^`Ef!>_yC;GpOb2})W~eV+x}7w?VNfdqe{g$1
z3C8-1gXtFJHHLu~$+;JQnUw2%_4#zL*X}OYe51wVyf312HHCwU@($@`lLp6*FENIV
zc1{AV`c}xB`kNo^7DijtlSVBH6e~dDby;R8De_ohv(&h3REh(bax~K09{UE^nG+-|
ziR1Dd#Dq3G_qKPxqKO_FEIW
zYh!-@owfYf>_I47nJM7Z>o$Jw!}OfZColj7Urmq8AP5OIg?rI5f>33Zl%f(0Y+b5W
zfy*bp6>9&%-NHAfm}%GIQsIc7sT)%UEoj_O*;2wkmjF3poe$4;tv9tN
z{`d$F-;mOdH|xoSD?C*Bki0FxjvrweK>>l^BU4%5vGP^PItMg|WEMy;wf>3L)p@?V
zqZn#p8;19OBJ_6+*(#(t{El!HI^ND}b%Op*fU|^rPmNgd$lXcSSQ5nHBM5Hf357}{
z`TOM@E{Xna2SRb1Xx9OY7(plfIwgY1<}IpdXGF}Qt(8upu2(RBa^~%%3@4z_6(&P&
zdDpb9Pcy9UiCH;et1l3l0T{#7Ghq~melHpHupiaO4B(@@H+v-56zl)#O-37CuwiWA
zE`lAZv)SoSlH%Q%eMH>L{`ZS%WYhb!QFltCxs5D<&;?SNzO&QeanAjkcb7H|o>&Yu
z3}AaO;31#eQO#|Js35qD@N%A6S?0$sPM&b@ID+alM>-#gP+@Ol*AAAtN$4V+PT>{K
zh%G@R=%4d+bHCuK(G-dq_5rUv7-1GHi}yXm75glW^=lf8CGO(jaP`lBh3oEU)#g4N
z|Ks(T`3)5B=>6<+OeoPM;}EvzkTvVEx&O4iHks1plE%ajY#SDMQV&XA>_X#+uKW=E3Hi(Pc^svVo#mjeE{}8tAn##hgkWGQkWWvAO?&+?!cdz
zE?S(3V!NI=ZLvaPzg{tS!p#y9#e7)e)dnB*2)ID3Q8enFng*-7&?qa%uedgd*z34QbjgsP9vZ??2U~T@Zi74dEa8x;UQTN
ztq=KAOFywQP7C;0cNEg~|511=pC5$m*0w?Ds@NiW*=n^_@lyap`eXl72`r3F5V2DP
zQDwK%4@M6XJG$Yg=n=K^C1Z3LXkQM1jU+gQ2rEjQDrx2`J!p75%+SBv=o{3dtB*6D
zPXswN7C$=F>Dj`{YJS@bjqhK_yy)z0irTJ2P=``|qTiw0bo+ZpuC)8}URM7jh%`Xv
z)7%^|ToW(#HkogfyG2#Hvux>?rnit27q8gRjQaw)V(q$GAieOyo1v_av3GYmPsB&>
zw=Oa)^9Lk%leg@#9$9#Ojhc%-DC8)U^5wfNf13(6&es!ij}d%gYyK9SiQy_FYU}e|
zN&qN~N1xgpK1w4fQP@g9FdHyvdP@oXksgkMM|^W#j4UqzOawSbDt4R3Vus(o%MJb*S*`7jUS$gkhlWjmg6uZJEGS%dJQ2Jtg1Su97oQy0Xp_=zE97sp{
zuFavnLWNo^CmfUKo@Dg|Kt0OC!{86jXz&{KAgZw33&e`iKgQrC3V9=X5}E|@q{2p0
z^J98_1m35jny9v8i}g*TcZ%%?hslt7=#NQf`${_3wI%mye*2d`I4;z6Yx?^cZ2W0S
zVb+tbXb=s9e`oHM5I*gdCbhW3*9)%Ee!IdAtDk+4_+AxTEPI-RwmFK9T
z;C;xKtxMz2E83={YBDA=(E93U**?3SpE7`(?e|JqZO-2N;pInVUJk0?dOz7LPo!;j
zZZCGU2^g`v!3L-u7?ThCE|n3!S*cQ=KqhuV7YQMM;pGqlxeu}j!MlQU?+rJ)*PI~!
zkaXWmk)AJh?n60l9X7k+krw&76-HB^a9389HM#Yh1xzR*Azsq|L+La{btX^KM+^FQ
zf>t5m0kEvJCFp+==_*03X9~`cck*sF=N=MOBVk?>to>#Zo1p;2(t*=4NZ=qW3#!2TXBnNALZUPAR+M0n5MjXw1??A3e@ldNhTaS`qZjY@2GbyNt7W_eF1gHW
zdk|=>pqZG^qp)4a3ry}u*vL>jtdaQU7YWslOe0YI5)WFu`m3(N%7e(!Wl@2
zJI@A~1i|S@&i?FB(EErOWW6@*!Q^_rf1;Rvl{^1N20x{K?wzsd32%Dd3$=5Puw@x2
z@eO=}PM6Y&0(YyX{`__Bc5O>ah8{^)Ul^e@?EJ8(Uj+6xvJ_ZH=F?^%=ig^ij(eSj
zxALYAXEZ37*z?tmKhwqs_gkR))}1sxiD2KvB;Lk`6Z9u`N?Nv%#M7kN_c9uyV-zFnt_c12Qhr2r(hQE*Qer
z3G7>%2p%UnN@rx^JaLe%sC%%pE})8+{dU>u+J%#A!gjqaS^vs|K9`&+h+FmJfuQ?_
zOZ>yAyCKj}!5Nw?EISqhN%z<2?=sQXLr2bj=)
zZJ`jmh-{!128DqLUzcMK3@7u(8uyvPiq|Vl%LuzsqvADn21Jg-K|!{QWZU$ZJ?_Y!
z;u8g{W5;q$CcNnv?ihEXT$r`twy*$PdPK9rtPlGh8#vpOA-;Q0#c90z*KcQS3wV(+M&QBG<~Rr2(hQBw{e76l&iF@F-9=o&MFJNh$EdpEfZd@7FG1J~217Izl@Tjh+24Br@UEA
z)idUgeuF19#Y50~*KTAnw8u`1U`bzUA>B3H^CN4AX+Tt>3^bWmWVhC20oOHe7Z`iE
z2|pJV7zc;nT~o)qs^3|p82nN~Z!UG7J&J>HGsVb{ZiGd%dZEtCdwSatKp~6FgON?B
z?Ui1UD}uJR-5D3x_HVO`F+Ok{fe2Hmxt(#Yd}VHP$c=-QCRwZ(#l7!fW{BRBLMaVR
zrCB=}3_adBB340(1X7JG8++$FdqO1?JlXXSP*+#Q)S!*UVq4I}=@ml<&_aK<`zCd{
zeY9K7Gdrq0_hx|gsa)rMk!;2!LW&Y%+DvCet%v1HfYCPL_kcd;hGc4A^x&cJ^-L4(
zWzq6a`%v4oyVWBD7!A)w<9vW1yK^GV-SY1^a-Dd0yTt+^_=>k<
z+u!~EcfUlG3qA9S;lZcPDx@r81v%RnG
zci8>hv-*3Z>VqPg@yx&(p>Jq@J=X}mP;WK66={D!5Ho+W=~dLS>OAJ8gmQPHqFo(9
z$f~(K#ID@DXOXtUay+lIP}AUq@@)$SMTv2UhBXcV6xNkw{46Sndu12cLvZYj!Bb(R
z`=8@O`QG+WsHN?rQs^i9e_c^!I!D%LG;h_jXtw+!*2APqB&V-x60eE^hIOYC?Op!t
za?j(aEWmS(mtjmWO0~BKG~)d{A19tV{Yjdf_i*EH23}>}M-uE|n1u2h3fFcgu)o*V
z4Bh?>^ir(rj3^yvB-r~o_;jJW#r|&4-Emf1->Cgex3(+Ml{SaucVn}8-7FMKM{)om
zyvPf&vxj2&DuB5J4#y%w0k}gvi_S9yj_%ZwxE}^u7hIO~uN23xnnqyDqiqss2=VM+H#Q=$h
z3R2<~DU_c#*nE8Ah^F6nlAbLso|@N9~@7cU0qJ6a1V|1`BZp`|DI~m+^~WYWI~J
zip~~uHNS1s7!Grky$l3(T1`y7}2D0f!HBI?(I@f;wP{;vnCqs+JI
zloUQwaLiy34u9Ca5CIeSzK>*DKuk;&+ya5@xX&^5*ZcTcbah$!gdg-RxHI@v9=N@|
zzs31{D6hXLs6*C-j?{O;+!ZufWDJq0vhT?uW(a^+M^&H
zNxzjn*BmGPJy*o|87c85Q4!3P<1BlXZ0up>2#)b~Y;X47Vaw;C)$KetI$_TL)RSCA
ztG{-`NInI4m$RFGD)C06aW~n+jvZxxnaqBc-CV-)#kp1#De8Ckm%E>!;Hx8X36oun
zu6zwjIH_gcPed$g$-PbX3{}7Qd#nOx1)bK%){+7mCcE;v0IiGQMAEwD`gMNm1t+UI
zN@$EEIn&8X&Sv@%ba~Ykca`Q#T^v$SvnDkvIhT-sA2X?n!D!zb!G2&`U@!@hwXqU^
zbVn{f%e6%uz-BLrja;{MEP@hi=PcWv>J0|5&3@qMI?Y=na*>j@bj8h()AIMA&XLuj
z;62-yXXtcVp(hgT)?71EX~9G_~we|~wxeXM=@
z>{c&u{0g-O4K($%9_HT{A(NpkD^$<2=?$H$i}e*VN-}R-YAUMOkQTRZ|qE=h`4*#MshasnT-qg$(Y&5<0
z*xA?f1U2=#rD|}HQ2^W4a`RcrJ0DUp!RhENQ-`1pS3~FWwfh-)TKW};ePWplEby*Gp~-Hq=6^P0RFez;Z=(@aD5G5_OZp&7HFAUZ0P
zAndD^Wzwai-7Hw+*tQ7ygspoT1s?B`l-8*SFzJ>bBToGR!+vTVHeJZHv$vn%%v;DV
zNZtIdrCof)Xt5;4vq=>fNhxixbVP+S2>_L5E^w8<^H!X=EErZFgyWdxDMaEsuwFir
z{RxBfciR>@(P3hY`oxIPD&Qw%}KlG;HiO8lktcl-_>vGw&W9%nV{jgVmfT=%Kjd
z&BfWtrGDxF)0G2?c`I*09)~GVUZ4N#dA}%rf#g0ApWI|&7=Yj=9vWTJRJ&OnJc$!i
z3yU}y0Sx0ydyR0aohh`<-NmWhi)SeH@`R0kc$Yp7b8jq7Q3$L3MyK@5B9(bNoX`L3
z>u!c4aUfs6ae2QPH9aN`~X(NHuAA7Af#5j_LBn^ie|9f!$U?2Z_MH5PxkX>f=etp
z_t?Yb!rd8}Lb#kMqU(PJtr)XuTE9uJ8+C#Z3D#o_)jyD^=LKD)+$P}dy5~#slJI1I
zHCFnL_1V9MY)43=1bmS9WK_d5KPMl|C4HrcZEe_9<8cjXxR99D!A(b2wdzJf@nva9
zXpU1{Hcj1fzwPFyq#3TA6?3CmIe&V1`s&BnN%w+BAf)o*7P2Q`bx#OT
zULc2Ed-JjEb&@oLW9Hc2RD&W*U2sdJl@j@7YSuH~Ap`L>Er%QM$UVIby9;}~0W+K<2*`ZWK&8IypRNwK|qa7X}yOa0?d0+3*6F6AaBd9~ybl_xk?M==8=Y
zifT-n1{Xg34;CHwNU()gNR$t#H9nr{;;=FBYqVx4gcif(ETiOFj|Gk^URnO_gMP0*
zo7a!W`9p8dnKWeiNMsAp)ckoy`VVCh)RK>C)m%wR1~JUt2?&9!(=p1L-K#kR55F6M
z)tmOGYIbEY`|G~v{BHMJCwVdT6t53k^KOf#w7n(XL2aU{=rI
zM8G*Jyn84;GkS(D8BQKJLHk7`x~T;wtw0#AhY7@ACLmV5s-}qHd#svULv#clA3%Jf
zY;(F&HkOcD#zp@DYYrBhi*x~BN1R)qQDEwaIk(4~pV+;ZQb;1oGQ@iXrYr0ANT_0K
z|7?NWphcV4VML;)l(&oO?4Pi=pD?M3+Eb>szq_2~>_8~jX?dEU7}syEh=)@cJ(~XG
z{mc69A279Nesaql91}one0r&5XL#Znc1!)?zkg_y3yXHeRb6NF&)!obvNx5x8K=DQ
zmwY4nfG^C(6B$KTdt14yRnh%TXR`2%sjh9+&O$G=WQQvFXT*?ZPnVL%Pc}6f
zOwIeF17oh%`)(7elUKh!D3m~m;+RiM{9KZH&FQGvUQ9;q{p4MLLWFLUyJ
z7Suc8_9zqCFPI7fwy4Y`Qr1(|kzu}t;n-0#$_U4O+p?
zciPC5q!mz!81Zzq;O`}spCv@{tr#cY9%lB$$TJNTm_YHix3O=mR~Lv#RT%Euq?Tfe
z(Uw|+ieM&6jpSiS5}o;fNG4G_g7jOK2{F*A`5z}
ziupnE?)PV}At8xuFF#3MZ|smY_Y*6Vp&|4{;K2XC067%K>voSm$@DCqys=l>^iknI
zZj=Z829f{#d9lS&4{m%Gnc`DPvZ+)0!SJ>+eYUsd0vUB8yw7H_@*|2XGRNDikNS*{obd1eBIAEVS(9L+}Hq&wy9(BN5F|yY2#RL|NsKWGgf)+P
ztsrs?o-lQ+q-lu&+flwEG6R?-VGxqU%C;O#MxGMl{zBKk>#?)m*FvB9`w*UqJ%`JL
zD3(E72C4!DF_9Fe0^&Kj+E=Md(BTdz!Ph9x=
z(GTJUERfo==leB&wdKl}b!|G{r0Jnx0y>8P+Sp8u)|4#ioZ1=#HN?4iyeK#R&v
zX-!cpEn$d~YBz8d=C8uzZr&VO0_+fa-NLQUpB}pHH~YmEzA5X4af{k&X7&Y2X!I1Y
z8hz!3Ac%#(hlmuk(Xl*QR;`0$TTJ#^Nmfaod59q-}D;+V>jCxF^`f#|{u
z*SXC89nFyoCyoV*=K?8nm`7HC*zVOATYWbf25R2;5hL?orKurF4eB?b4-H0
zt7m;GaD0CEB*BplUJ5X!4-MA6t%Wxh%m6ld7)zzA+T`B4mQ
zlOl%~^H@BPGDvBMUqz{KBe6f1O`(c_@7cRk&!_SL(2ppK!UR4oc*+Pmu=nY
z&)&Au4`2PE-5XY;lD1F%#x+uzxlr
z35NbD-R8y(Bmt)ZJf)*I;}iBN*lVDg{}ZJIPTL5%clWAdBAc)_>L$aNtbcCPgU|HA=t0O2M@!4h_{7PFiRh8w~``
zb0yshW8t=wyM+ThOYniO=K+&{p^IlzyTg1aP1R&&>hcqgnn|qGtDO$W)7SCC8m@6EI03Hbp?l
zF{CJfLfB}M4Adj50_3>Mrv!8CNn*$iJhNhn1v?AF$t6GA4c9)EHNrdTc_>!hR;Gm8
zhzuwrze$YcQ81Za&Pbn5Fg5Re8qf=*`9X$Tn7KWpF>U`|QFJAEM{l1sK
z_RAP;7xDG7GdV1n1U_Vk)_T+#01-&^m~8A*4372eg|x*UxOf*g8Wqs(50PGyW1|6{
zcAHTvHp0B0j~wDsxaPWWld(cgLwWG6S;pgeMC{NhIw)&?7^7~Rbs4Ae=1sMg5?ySn
zH%Plgqe!c|QD}tOVLcsSpz$sg
zS)IL=Uk`rjO?$6-ANZV}dt>6r%JIzrqYQ%IU3S^Rb>IAiza%5i52cqH%nz^pMk5E_SR4g9CG&37Vb!A
zGpveA9I>Q7-mThM2ynu^jblr3F8CD@G{sGn&}OWJ%v@tU>`8I
zp|#(OMJB=ZB*E{zh)b&ffk(}*LhuRU*EBBt2%s9ZY*6BdxF>S$gT{Z$BBzL181`En
z4(ejj5iblIkK%Nj%-koo=Qv
z@>M4b?dTU#$=BGo-N)g;SMkr@zN=q2?}NKP_yO?m^1gvm?*78UFbhOD@B8)Fe;rPJ
zLBFtk|Jep#*YNW3)R&oH2u|}ac8zrdJvx7GG&nj~?6yy?B-p~8$k>Mw_23xf8ptCp
zc!5>FK8GeVQln2lKznjlVpSRu#RpIPB#1(kIoe=dHN*!v>L3zdJmXw&!RM3tvdz2u
zgTwc3{9OO~*~aca9u}9+ov!nx18*pl90ky%ghy2?;o4*)o>k?!wW`i!7;A#TBw&s5
zRX5aif{(c-(DJ*#cy?g%TERCx?jpXJ@fa3`zspra{JLcDdGNl>Bt~)JT!|R118^2b
zm30j}?ZV8ck3Ym6OVLT?G?M+;<$h#ltpt=K!vavtiP52%bdx=z2Wk$$+@%f3&H@xO
zBrFYet|k_?{9wJ_FE03XB+_8(rTxb6CI9+@{?Z%z?jy6s)6bu7ZoOex@;l14aEmH3
zl~=L1g)ejr8dWx@O22^gv$3Dx^J=+Wb#}1his7B!JDAHGx!mo0zrDMk^#NtyumAOP
z7(w4NoVfR)(_3!h9Lb+3LUBe_oh4{wjX1#-0BeBbY)7|d%dJ-0HA?hWqa!xX)S*ee
z7L#?Q53))yIG8=sfGI)bg{pj9co5L#+?`lbw+%&Rg4Au&pDTj)Hodu54Ax}Wv(!y*
z-Q>nV|KlJ2;e`WW^Zk9!nhr$d*2%Mz_YNDgKjdy7Us?Bkbl(3%y}5h|)S>q7O3Q+j
zu_Qw55K;XYa=V9x*`e-jE5qdc5AFKU`@#OGhXb+i`BH}jb)TL7^1pg0!{>YW66$lf
zW`L@Eg(t=u&>SUWph3=xUV}oZsx4Tyh2!|~67%*YS;Cnl32ws!mt>IUcBgd-oIv_K
zBXHjW8UE1=SoV@P9NY7R08G313N}f}DBickZz8T@#0wVsg<=YB!7`#`sk`c;6cy%w9S>d$kTB%{
z1~PCtJhp7CavS$$e9z#Q|MiEL{}CiEn$4IA>%1@Ranq9El-Vh#^lQ^gdGzbtZp}PG
zwzkO_T*a!pTft>-Yew&M1`w32Rrob{l3=#l?OYh%E(zp#@0PlyOth<<_A7sLKCh|$
zQAXb94a<8boGmyj_)1Rf#G8q}&hX&doS?x;cHvaSjQyGuI^m85;Q&b{i!dBhxWed~
zFX}|Cr1K)r?WXw1qc6cIzR-HAWx@{q!b+5zZ5;jxZ?!qVPq9A#ga38K%YIdA2E5(K
z8@sY^)AQch-jlnHtv@1<&Yf;BjjKr07iD7&aw`KQv?f#^B~8+%PG2mx!iDJ-_^fqr
zUztt5s@*`E9}a>c*1cPd1QHEmD`&jy@BYu}*6?gJ|IA_Kgfn8@;Y0LuZ(+*uD^ba4
zLudXCN=l8&SRGiwGbP(j0%h$0wqozVBO16C4sQ1Cnr@Q7vw?BcgF_HG*whOGm-{7t
zaCMzGtj=avGg-ZMGMzsE!~gZfE2Ea1%0PL}?VR`Bv4Uc#FijF{&MxJ*wLBc~9D1(o
z?HKX!f?H;W1ik5HWIY=FAJd<3D(AMhaZ7!jm-zWL8c&z_PHX}OZH~(Z(1JORXdUFS*f!30
zgya!&6K)*wV{*7JJc&|=g6bTuGac#P?_L-t{H*=*G7Gz#ebVF2q|+a6u6KWQ@JCO|
z&)_lD&rgBnc2uP$K@JBS(@U5M9y{CUeCCWdnmBQJyw?Da59+7NIItZlYbK;G8C(4$
z9*EpP%5{Rb@qOC!Z)^+05i86-t{V>xSMDPbYK?H_%l>{JGs0sD*tv|ZFCA9)d;!-I
zy4hj;?s|bQYa2{cz!4imSwklE1tx+_<(66>*wyUKbY5_i!X#)o75vaZ>tTX-(jb?2
z3w5(YwjV!)
zDl4hgXio*0{}k-N6#xTrUp`5P9udAd?CM<;yp;!yzvjccKK$Oo3-Tw(#+CBE%lmP$
zs8*-F={WiFBo>6!i6Qiz3il19^m*+#kv8-U`Ik*2h3q{1pT7T
z6UPLS#*07Hb0(HLw=*^lUBhDZa%5b_O9%h@Sigvn7(+dOCk|E$}Pb;Fxyo5DdGqdmWzVjywdtAM75_XeebrWft(7{RliBf)_>t!w_iZNFxiDl+z??^XYbG<;X`)5NoLx-9knFYBiX}6h0M+)%
zBED6-I(%JPLOy{DhlTxpcg{KdWGgSVoR9a9qVWsgwY+!MpUwM?_AE{}PTT4h_kqqz
zjgl1d5jv7V7jAVovghfHf#(|+-o0n{t=GTj*`LODzQ_3_RNEV;k_#^$x`$t|eICEv
zd(YH%E(|$4;9}9atO+(*hVqqYLQ5jd1DN$e3vUv{o=Ssx5@2xh$J8OaIf%Y(PJho>
zaF1}s^p@6U^*vhR=H}IVTGpKyGA;~n!2=h;EKcfbjv_fxXK;Rv;3
zG>>7ueXz?=`#^0;0P!cweZm9n%OE%RH{Up09C#ADdt7cipPnO8{3F|+pn9W7z83sZ-|_6g
zyNFyi=C!ep?bcZu`ZhPCnuTBueG_3Uqy`s)c!c!u?2R|B-0*}?t^r2R+}HNr*T1Y=
zxT(MCrs-nO&))EJaZ8M9Zx%4dT?=SZgB28FcasS)VNTT@SP`
zgSar9EG~W3Y;F1yo+sd)+1@Sk)V~&cE(&=HvmcguKxBRPH@x-kaR-+VzLRe6Dt)rt
z=RMF)5w&CQ#@=&>>DCXi1b8eLJx}H?VpfJ+?EX70lfQDdJbSqbL-4ai_}@<1V4wtt
Rg-`$h002ovPDHLkV1h8wFem^3
diff --git a/web_extension/Veracity_Extension/public/manifest.json b/web_extension/Veracity_Extension/public/manifest.json
deleted file mode 100644
index 6a15409..0000000
--- a/web_extension/Veracity_Extension/public/manifest.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "manifest_version": 3,
- "name": "Veracity",
- "description": "Veracity helps you verify claims, evaluate reliability, and discuss evidence with confidence.",
- "version": "0.0.1",
- "icons": {
- "128": "icons/icon128.png"
- },
- "action": {
- "default_title": "Veracity"
- },
- "permissions": ["sidePanel", "activeTab", "storage", "contextMenus", "tabs", "identity"],
- "host_permissions": ["https://www.veri-fact.ai/*", "https://api.veri-fact.ai/*", "https://veri-fact.ca.auth0.com/*"],
- "side_panel": {
- "default_path": "index.html"
- },
- "background": {
- "service_worker": "background.js",
- "type": "module"
- },
- "content_security_policy": {
- "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://www.veri-fact.ai https://api.veri-fact.ai https://veri-fact.ca.auth0.com;"
- },
- "content_scripts": [
- {
- "matches": ["https://*/*", "http://*/*"],
- "js": ["content.js"],
- "run_at": "document_idle"
- }
- ],
- "web_accessible_resources": [
- {
- "resources": ["next/**", "icons/*", "panel.js", "index.html", "content.js", "background.js"],
- "matches": [""]
- }
- ]
-}
-
diff --git a/web_extension/Veracity_Extension/public/panel.css b/web_extension/Veracity_Extension/public/panel.css
deleted file mode 100644
index 4764acc..0000000
--- a/web_extension/Veracity_Extension/public/panel.css
+++ /dev/null
@@ -1,1020 +0,0 @@
-#root {
- font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- background: #fff;
- outline: 2px solid transparent;
-}
-
-/* Logout + Read more/Show less: force blue link style, no button chrome */
-#root button#logoutBtn.logout-link,
-#root #logoutBtn.logout-link,
-#root #discussionDescriptionToggle.logout-link {
- color: #1683ff !important;
- background: none !important;
- border: none !important;
- box-shadow: none !important;
- padding: 0 !important;
- font: inherit !important;
- cursor: pointer !important;
- text-decoration: none !important;
- min-width: 0 !important;
- border-radius: 0 !important;
-}
-#root button#logoutBtn.logout-link:hover,
-#root button#logoutBtn.logout-link:focus,
-#root #logoutBtn.logout-link:hover,
-#root #logoutBtn.logout-link:focus,
-#root #discussionDescriptionToggle.logout-link:hover,
-#root #discussionDescriptionToggle.logout-link:focus {
- color: #1683ff !important;
- background: none !important;
- border: none !important;
- box-shadow: none !important;
- text-decoration: underline !important;
- transform: none !important;
-}
-
-#root .verifyButton {
- border: none !important;
- outline: none !important;
- box-shadow: none !important;
-}
-
-#root .Home_primaryBtn__nO8b8.verifyButton {
- background-image: var(--gradient-brand);
- color: #fff;
- border: none;
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
-}
-
-:root {
- --color-border: #e5e7eb;
- --color-surface: #ffffff;
- --color-ink: #0f172a;
- --color-muted: #6b7280;
- --gradient-brand: linear-gradient(135deg, #5B8CFF, #7CE7AC);
- --credibility-gradient: linear-gradient(268.88deg, #11F90E -27.27%, #1683FF 94.82%);
- --shadow-soft: 0 8px 20px rgba(0, 0, 0, 0.08);
-}
-
-.Home_panel__UGulu {
- font-family: inherit;
- background: #fff;
- padding: 10px 10px 14px;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.Home_tabList__81_8M {
- display: flex;
- gap: 8px;
- flex-wrap: nowrap;
- overflow: visible;
- padding: 0;
- width: 100%;
- margin-top: 14px;
-}
-
-.Home_tabButton__yY1n3 {
- border: 1px solid var(--color-border);
- background: var(--color-surface);
- padding: 8px 8px;
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.2s ease;
- min-width: 0;
- flex: 1;
- white-space: normal;
- text-align: center;
- overflow: visible;
-}
-
-.Home_tabButton__yY1n3:hover {
- box-shadow: var(--shadow-soft);
- transform: translateY(-1px);
-}
-
-.Home_tabButtonActive__zVobV {
- border-color: transparent;
- background-image: var(--gradient-brand);
- color: #ffffff;
- box-shadow: var(--shadow-soft);
-}
-
-.Home_tabLabel__IC30v {
- display: block;
- font-weight: 700;
- font-size: 12px;
- line-height: 16px;
-}
-
-.Home_card__E5spL {
- background: var(--color-surface);
- border: 1px solid var(--color-border);
- border-radius: 10px;
- box-shadow: none;
- padding: 12px;
- display: flex;
- flex-direction: column;
- gap: 12px;
- width: 100%;
-}
-
-.Home_headingRow__XUO2e {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
-}
-
-#root .expertHeadingRow {
- justify-content: center;
- text-align: center;
-}
-
-#root .expertHeroHeading {
- text-align: center;
- color: var(--color-ink);
- margin: 10px 0 16px;
-}
-
-#root .expertErrorText {
- font-size: 12px;
- color: #b91c1c;
- text-align: center;
- min-height: 16px;
-}
-
-#root .discussionHub {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-#root .discussionHeader {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
-}
-
-#root .discussionCreateRow {
- display: flex;
- justify-content: center;
- margin-top: 12px;
-}
-
-#root .createDiscussionCta {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 6px;
- margin-top: 12px;
-}
-
-#root .createDiscussionHelper {
- font-size: 12px;
- font-weight: 400;
- color: #6b7280;
- text-align: center;
-}
-
-#root .createDiscussionForm {
- margin-top: 10px;
- width: 100%;
-}
-
-#root .discussionTitle {
- font-weight: 600;
- margin-bottom: 6px;
-}
-
-#root .discussionDetailHeader {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-#root .discussionDetailTitle {
- font-weight: 600;
- font-size: 16px;
- margin: 0;
-}
-
-#root .discussionDetailDescription {
- margin: 0;
- font-size: 14px;
- color: #6b7280;
- white-space: pre-line;
-}
-
-#root .discussionDescriptionPreview {
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-#root .discussionActionBtn {
- min-width: 140px;
-}
-
-#root .discussionScroll {
- max-height: calc(100vh - 260px);
- overflow-y: auto;
- padding-right: 4px;
-}
-
-#root .discussionList,
-#root .postsList {
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
-
-#root .discussionItem {
- border: 1px solid var(--color-border);
- border-radius: 12px;
- padding: 12px;
- background: #fff;
- cursor: pointer;
- display: flex;
- flex-direction: column;
- gap: 6px;
- transition: box-shadow 0.15s ease, transform 0.15s ease;
-}
-
-#root .discussionItem:hover {
- box-shadow: var(--shadow-soft);
- transform: translateY(-1px);
-}
-
-#root .discussionTitle {
- font-size: 15px;
- font-weight: 700;
- color: var(--color-ink);
-}
-
-#root .discussionDescription {
- font-size: 13px;
- color: #3a3f4b;
- line-height: 18px;
-}
-
-#root .discussionMeta {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #6b7280;
-}
-
-#root .discussionStats {
- display: flex;
- gap: 10px;
- font-size: 12px;
- color: #0f172a;
- font-weight: 600;
-}
-
-#root .discussionStat {
- background: #f5f6f8;
- border-radius: 999px;
- padding: 4px 8px;
-}
-
-#root .discussionLoading,
-#root .discussionEnd {
- text-align: center;
- font-size: 12px;
- color: #6b7280;
- padding: 8px 0;
-}
-
-#root .discussionForm {
- display: flex;
- flex-direction: column;
- gap: 8px;
- padding: 10px;
- border: 1px solid var(--color-border);
- border-radius: 12px;
- background: #fff;
-}
-
-#root .discussionFormActions {
- display: flex;
- gap: 8px;
- flex-wrap: wrap;
-}
-
-#root .discussionError {
- font-size: 12px;
- color: #b91c1c;
-}
-
-#root .discussionBackBtn {
- align-self: flex-start;
- background: transparent;
- border: none;
- font-size: 13px;
- font-weight: 600;
- color: #0f172a;
- cursor: pointer;
-}
-
-#root .discussionDetailHeader {
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: 4px 0 2px;
-}
-
-#root .discussionDetailTitle {
- font-size: 18px;
- font-weight: 800;
- color: var(--color-ink);
- line-height: 22px;
-}
-
-#root .discussionDetailDescription {
- font-size: 13px;
- color: #3a3f4b;
- line-height: 18px;
-}
-
-#root .discussionControls {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 10px;
- flex-wrap: wrap;
-}
-
-#root .discussionSort {
- display: flex;
- gap: 8px;
- flex-wrap: nowrap;
-}
-
-#root .sortPill {
- border-radius: 999px;
- padding: 6px 12px;
- font-size: 12px;
- font-weight: 700;
- box-shadow: none;
- min-width: 64px;
-}
-
-#root .sortPillActive {
- opacity: 0.95;
- box-shadow: var(--shadow-soft);
-}
-
-#root .postCard {
- border: 1px solid var(--color-border);
- border-radius: 12px;
- padding: 12px;
- background: #fff;
- display: flex;
- gap: 12px;
- align-items: flex-start;
-}
-
-#root .postContent {
- display: flex;
- flex-direction: column;
- gap: 8px;
- flex: 1 1 auto;
- min-width: 0;
-}
-
-#root .postBody {
- font-size: 13px;
- color: #3a3f4b;
- line-height: 18px;
- word-break: break-word;
-}
-
-#root .postMeta {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 12px;
- color: #6b7280;
-}
-
-#root .postVotes {
- display: flex;
- align-items: center;
- gap: 14px;
-}
-
-#root .voteGroup {
- display: inline-flex;
- align-items: center;
- gap: 6px;
-}
-
-#root .voteBtn {
- border: 1px solid var(--color-border);
- background: #fff;
- border-radius: 8px;
- padding: 4px 6px;
- font-size: 12px;
- cursor: pointer;
- color: #6b7280;
- transition: box-shadow 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease;
-}
-
-#root .voteBtn:hover {
- border-color: #cbd5f5;
- color: #0f172a;
- box-shadow: var(--shadow-soft);
- transform: translateY(-1px);
-}
-
-#root .voteBtn:focus-visible {
- outline: none;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
-}
-
-#root .voteBtn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- box-shadow: none;
- transform: none;
-}
-
-#root .voteBtn:disabled:hover {
- border-color: var(--color-border);
- color: #6b7280;
- box-shadow: none;
-}
-
-#root .voteBtn.voted {
- border-color: #94a3b8;
- color: #0f172a;
-}
-
-#root .voteCount {
- font-size: 12px;
- font-weight: 700;
- color: #0f172a;
- min-width: 18px;
-}
-
-#root .isHidden {
- display: none;
-}
-
-@media (max-width: 480px) {
- #root .discussionScroll {
- max-height: calc(100vh - 230px);
- }
-}
-
-.Home_sectionTitle__DKb2S {
- font-size: 18px;
- font-weight: 700;
- color: var(--color-ink);
-}
-
-.Home_sectionBody__JASIX {
- font-size: 14px;
- color: #3a3a3a;
- line-height: 18px;
-}
-
-.Home_testBadge__dtTdC {
- display: inline-flex;
- align-items: center;
- padding: 2px 6px;
- border-radius: 999px;
- background: #f8f8f8;
- color: #555;
- font-size: 10px;
- font-weight: 700;
- border: 1px solid #e5e5e5;
- opacity: 0.75;
-}
-
-.Home_textarea__k243o {
- width: 100%;
- border: 1px solid var(--color-border);
- border-radius: 12px;
- padding: 12px;
- font-size: 14px;
- font-family: inherit;
- resize: vertical;
- background: #fff;
- color: var(--color-ink);
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.03);
- transition: border-color 0.15s ease, box-shadow 0.15s ease;
-}
-
-.Home_textarea__k243o:focus {
- outline: none;
- border-color: #8ab6ff;
- box-shadow: 0 0 0 3px rgba(90, 134, 255, 0.18);
-}
-
-/* apply same styling to inputs used in expert form */
-input.Home_textarea__k243o {
- height: 38px;
-}
-
-.Home_buttonRow__Cnhie {
- display: flex;
- gap: 8px;
- flex-wrap: nowrap;
- justify-content: center;
-}
-
-.Home_primaryBtn__nO8b8,
-.Home_heroSecondaryBtn__2ba6l {
- flex: 0 0 auto;
- min-width: 120px;
- padding: 12px 18px;
- border-radius: 999px;
- border: 1px solid var(--color-border);
- background: #fff;
- cursor: pointer;
- font-size: 13px;
- font-weight: 700;
- transition: all 0.15s ease;
-}
-
-.Home_primaryBtn__nO8b8 {
- background-image: var(--gradient-brand);
- color: #fff;
- border: none;
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
-}
-
-.Home_primaryBtn__nO8b8:hover,
-.Home_heroSecondaryBtn__2ba6l:hover {
- box-shadow: var(--shadow-soft);
- transform: translateY(-1px);
-}
-
-#root .verifyButton:disabled,
-#root .verifyButton:disabled:hover,
-#root .verifyButton:disabled:active {
- opacity: 0.6;
- cursor: not-allowed;
- box-shadow: none;
- transform: none;
-}
-
-/* Status chip: single container, used only for "Verifying" state */
-.statusRow {
- display: flex;
- justify-content: flex-start;
-}
-
-.statusChip {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- padding: 8px 14px;
- border-radius: 999px;
- font-size: 13px;
- font-weight: 600;
- transition: opacity 0.2s ease, transform 0.15s ease;
-}
-
-.statusChip-text {
- color: inherit;
-}
-
-/* Verifying: gradient (matches Verify button), white text, spinner left */
-.statusChip.status--verifying {
- background: var(--gradient-brand);
- border: none;
- color: #fff;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
-}
-
-.statusChip.status--verifying .statusChip-icon {
- width: 14px;
- height: 14px;
- border: 2px solid rgba(255, 255, 255, 0.4);
- border-top-color: #fff;
- border-radius: 50%;
- animation: statusSpinner 0.7s linear infinite;
-}
-
-@keyframes statusSpinner {
- to { transform: rotate(360deg); }
-}
-
-.statusChip .statusChip-icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-}
-
-#root .reliabilityCard {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-top: 24px;
- padding: 16px;
- border: 1px solid #e5e7eb;
- border-radius: 12px;
- width: 100%;
- background: #fff;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
-}
-
-#root .reliabilityGrid {
- display: grid;
- grid-template-columns: 110px 1fr;
- gap: 14px;
- align-items: center;
-}
-
-#root .reliabilityGauge {
- position: relative;
- width: 104px;
- height: 104px;
- display: flex;
- align-items: center;
- justify-content: center;
- overflow: visible;
-}
-
-#root .reliabilityGaugeRing {
- width: 104px;
- height: 104px;
- border-radius: 50%;
- position: absolute;
- inset: 0;
- transform: rotate(-90deg);
- background-image:
- conic-gradient(
- transparent 0deg var(--score-deg, 0deg),
- var(--ring-track, #e5e7eb) var(--score-deg, 0deg) 360deg
- ),
- var(--credibility-gradient);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- /* Donut cutout (preferred) */
- -webkit-mask: radial-gradient(circle, transparent 55%, #000 56%);
- mask: radial-gradient(circle, transparent 55%, #000 56%);
-}
-
-#root .reliabilityGaugeRing::before {
- content: "";
- position: absolute;
- inset: 10px;
- border-radius: 50%;
- background: #fff;
-}
-
-#root .reliabilityGaugeInner {
- position: absolute;
- inset: 0;
- z-index: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 2px;
- text-align: center;
- justify-content: center;
-}
-
-#root .reliabilityGaugeLabel {
- font-size: 12px;
- color: #6b7280;
- font-weight: 600;
-}
-
-#root .reliabilityValue {
- font-size: 30px;
- font-weight: 800;
- background: var(--credibility-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
-}
-
-#root .reliabilitySummary {
- font-size: 12px;
- color: #3a3f4b;
- line-height: 16px;
-}
-
-#root .reliabilitySummary.summaryCollapsed {
- display: -webkit-box;
- -webkit-line-clamp: 4;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-#root .reliabilitySummaryWrap {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-#root .summaryToggle {
- background: none;
- border: none;
- padding: 0;
- font-size: 12px;
- font-weight: 600;
- color: #1683ff;
- cursor: pointer;
- align-self: flex-start;
- text-decoration: none;
-}
-
-#root .summaryToggle:hover {
- text-decoration: underline;
-}
-
-#root .reliabilityCopy {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-#root .reliabilityHeadline {
- font-size: 18px;
- font-weight: 800;
- color: #0f172a;
-}
-
-#root .reliabilitySubhead {
- font-size: 15px;
- font-weight: 700;
- color: #0f172a;
-}
-
-/* Reliability card (namespaced) */
-#root .reliabilityCard {
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: 12px;
- border: 1px solid var(--color-border);
- border-radius: 12px;
- width: 100%;
- background: #fff;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
-}
-
-#root .reliabilityGrid {
- display: grid;
- grid-template-columns: 110px 1fr;
- gap: 12px;
- align-items: center;
-}
-
-#root .reliabilityGauge {
- width: 100%;
- display: flex;
- justify-content: center;
-}
-
-#root .reliabilityGaugeRing {
- width: 96px;
- height: 96px;
- border-radius: 50%;
- background-image:
- conic-gradient(
- transparent 0deg var(--score-deg, 0deg),
- #e5e7eb var(--score-deg, 0deg) 360deg
- ),
- var(--credibility-gradient);
- background-repeat: no-repeat;
- background-size: 100% 100%;
- background-position: center;
- display: flex;
- align-items: center;
- justify-content: center;
- position: relative;
-}
-
-#root .reliabilityGaugeRing::after {
- content: "";
- position: absolute;
- width: 72px;
- height: 72px;
- border-radius: 50%;
- background: #fff;
-}
-
-#root .reliabilityGaugeRing > .reliabilityValue {
- position: relative;
- z-index: 1;
-}
-
-#root .reliabilityValue {
- font-size: 32px;
- font-weight: 800;
- background: var(--credibility-gradient);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
-}
-
-#root .reliabilityLabel {
- font-size: 13px;
- color: #555;
- font-weight: 700;
-}
-
-#root .reliabilitySummary {
- font-size: 12px;
- color: #3a3f4b;
- line-height: 16px;
-}
-
-#root .reliabilityCopy {
- display: flex;
- flex-direction: column;
- gap: 4px;
-}
-
-#root .reliabilityHeadline {
- font-size: 16px;
- font-weight: 800;
- color: #0f172a;
-}
-
-#root .reliabilitySubhead {
- font-size: 14px;
- font-weight: 700;
- color: #0f172a;
-}
-
-.Home_footer__yFiaX {
- margin-top: 14px;
- font-size: 12px;
- color: var(--color-muted);
- text-align: center;
- padding: 8px 0;
-}
-
-#root .sourcesCarousel {
- position: relative;
- width: 100%;
- max-width: 100%;
- box-sizing: border-box;
- padding: 12px 48px;
-}
-
-#root .sourceCardWrapper {
- width: 240px;
- max-width: 240px;
- box-sizing: border-box;
-}
-
-#root .sourceTopText {
- margin-top: 8px;
- font-size: 12px;
-}
-
-#root .sourceNumberBlock {
- background-color: #000;
- color: #fff;
- font-weight: 700;
- padding: 0 6px;
- border-radius: 6px;
-}
-
-#root .sourceCardLink {
- text-decoration: none;
- color: inherit;
- display: block;
-}
-
-#root .sourceCard {
- border: 1px solid #e5e5e5;
- border-radius: 10px;
- margin-top: 8px;
- padding: 14px;
- font-size: 13px;
- font-weight: 400;
- line-height: 16px;
- background: #fff;
- width: 240px;
- max-width: 240px;
- min-height: 100px;
- box-sizing: border-box;
- overflow: hidden;
-}
-
-#root .sourceCredibilityPill {
- display: inline-flex;
- align-items: center;
- padding: 0 7px;
- border-radius: 2px;
- font-weight: 700;
- font-size: 12px;
- background: var(--credibility-gradient);
- color: #fff;
- margin-bottom: 6px;
- opacity: 0.9;
-}
-
-#root .sourceHeading {
- font-size: 16px;
- font-weight: 700;
- line-height: 20px;
- color: #0f172a;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-#root .sourcesDescription {
- font-size: 13px;
- line-height: 16px;
- margin: 6px 0;
- color: #1f2937;
- display: -webkit-box;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-#root .sourceName {
- color: #727272;
- font-size: 12px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-#root .carouselNavButton {
- position: absolute;
- top: 50%;
- transform: translateY(-50%);
- width: 34px;
- height: 34px;
- border-radius: 999px;
- border: 1px solid #e2e8f0;
- background: #fff;
- color: #374151;
- cursor: pointer;
- z-index: 2;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- line-height: 1;
- transition: box-shadow 0.15s ease, border-color 0.15s ease, color 0.15s ease;
-}
-
-#root .carouselNavButtonLeft {
- left: 10px;
-}
-
-#root .carouselNavButtonRight {
- right: 10px;
-}
-
-#root .carouselNavButton:hover {
- box-shadow: 0 6px 16px rgba(15, 23, 42, 0.15);
- border-color: #cbd5f5;
-}
-
-#root .carouselNavButton:focus-visible {
- outline: none;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
-}
-
-#root .carouselNavButtonDisabled {
- opacity: 0.4;
- cursor: not-allowed;
- box-shadow: none;
-}
-
-#root .carouselNavButtonDisabled:hover {
- box-shadow: none;
- border-color: #e2e8f0;
-}
-
-#root .carouselChevron {
- width: 16px;
- height: 16px;
- display: block;
-}
-
-@media (max-width: 360px) {
- #root .sourcesCarousel {
- padding-left: 48px;
- padding-right: 48px;
- }
-
- #root .carouselNavButton {
- width: 28px;
- height: 28px;
- }
-}
-
diff --git a/web_extension/Veracity_Extension/scripts/postbuild.js b/web_extension/Veracity_Extension/scripts/postbuild.js
deleted file mode 100644
index 19c580a..0000000
--- a/web_extension/Veracity_Extension/scripts/postbuild.js
+++ /dev/null
@@ -1,1775 +0,0 @@
-/**
- * postbuild.js — Extension dist builder
- *
- * Run after `npm run export`. Produces web_extension/Veracity_Extension/dist/ from
- * Next.js output: rewrites _next → next for Chrome, injects CSP and panel loader
- * into HTML, copies public assets and inlines auth + panel UI into panel.js.
- * Panel lifecycle: DOMContentLoaded → loadConfig → syncAuthUI (auth gate) →
- * renderAppScreen (tabs: AI / Discussion / Expert) or landing; verify flow and
- * discussion hub talk to backend via background script messaging.
- */
-(function () {
-const fs = require("fs");
-const path = require("path");
-
-const root = process.cwd();
-const out = path.join(root, "out");
-const dist = path.join(root, "dist");
-
-if (fs.existsSync(dist)) {
- fs.rmSync(dist, { recursive: true, force: true });
-}
-
-// --- Path and CSP helpers (Chrome disallows _next in paths) ---
-const rewritePaths = (content) =>
- content
- .replace(/\/_next\//g, "/next/")
- .replace(/\.\/_next\//g, "./next/")
- .replace(/"\/_next\//g, '"/next/')
- .replace(/'\/_next\//g, "'/next/")
- .replace(/"\/next\//g, '"./next/')
- .replace(/'\/next\//g, "'./next/")
- .replace(/_next\//g, "next/");
-
-const upsertCspMeta = (content, cspString) => {
- const cspTag = ` `;
- if (content.includes("Content-Security-Policy")) {
- return content.replace(
- / ]*http-equiv=["']Content-Security-Policy["'][^>]*>/i,
- cspTag
- );
- }
- const viewport =
- ' ';
- if (content.includes(viewport)) {
- return content.replace(viewport, `${viewport}${cspTag}`);
- }
- if (content.includes("")) {
- return content.replace("", `${cspTag}`);
- }
- return `${cspTag}${content}`;
-};
-
-const simpleCsp =
- "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; connect-src 'self' https://www.veri-fact.ai https://api.veri-fact.ai https://veri-fact.ca.auth0.com;";
-
-const copyRecursive = (src, dest) => {
- if (!fs.existsSync(src)) return;
- const stat = fs.statSync(src);
- if (stat.isDirectory()) {
- fs.mkdirSync(dest, { recursive: true });
- fs.readdirSync(src).forEach((entry) => {
- copyRecursive(path.join(src, entry), path.join(dest, entry));
- });
- } else {
- fs.mkdirSync(path.dirname(dest), { recursive: true });
- fs.copyFileSync(src, dest);
- }
-};
-
-// --- Assemble dist: use existing out/ or synthesize from .next/server/pages ---
-if (!fs.existsSync(out)) {
- const pagesDir = path.join(root, ".next", "server", "pages");
- const staticDir = path.join(root, ".next", "static");
- if (!fs.existsSync(pagesDir)) {
- throw new Error("expected out/ after build");
- }
- fs.mkdirSync(out, { recursive: true });
- copyRecursive(path.join(pagesDir, "index.html"), path.join(out, "index.html"));
- copyRecursive(path.join(pagesDir, "404.html"), path.join(out, "404.html"));
- copyRecursive(staticDir, path.join(out, "_next", "static"));
- const publicDir = path.join(root, "public");
- copyRecursive(path.join(publicDir, "manifest.json"), path.join(out, "manifest.json"));
- copyRecursive(path.join(publicDir, "background.js"), path.join(out, "background.js"));
- copyRecursive(path.join(publicDir, "content.js"), path.join(out, "content.js"));
- copyRecursive(path.join(publicDir, "icons"), path.join(out, "icons"));
- copyRecursive(path.join(publicDir, "config.json"), path.join(out, "config.json"));
-}
-
-// Rename out -> dist
-fs.renameSync(out, dist);
-
-// Copy webapp hero stylesheet into dist for the landing hero
-const heroCssSrc = path.join(root, "styles", "webapp-hero.css");
-const heroCssDest = path.join(dist, "webapp-hero.css");
-if (fs.existsSync(heroCssSrc)) {
- fs.copyFileSync(heroCssSrc, heroCssDest);
-}
-
-// Copy panel stylesheet into dist (plain CSS for panel.js UI)
-const panelCssSrc = path.join(root, "public", "panel.css");
-const panelCssDest = path.join(dist, "panel.css");
-if (fs.existsSync(panelCssSrc)) {
- fs.copyFileSync(panelCssSrc, panelCssDest);
-}
-
-// Rename _next -> next inside dist
-const legacyNext = path.join(dist, "_next");
-const renamedNext = path.join(dist, "next");
-if (fs.existsSync(legacyNext)) {
- if (fs.existsSync(renamedNext)) {
- fs.rmSync(renamedNext, { recursive: true, force: true });
- }
- fs.renameSync(legacyNext, renamedNext);
-}
-
-const targets = [
- path.join(dist, "index.html"),
- path.join(dist, "404.html"),
- path.join(dist, "index.txt"),
- path.join(dist, "manifest.json"),
-];
-
-// --- Rewrite HTML: replace body with root + panel.js, inject styles and CSP ---
-targets.forEach((file) => {
- if (!fs.existsSync(file)) return;
- const data = fs.readFileSync(file, "utf8");
- let updated = rewritePaths(data);
- if (file.endsWith(".html")) {
- updated = updated.replace(/'
- );
- updated = updated.replace(
- "",
- ' '
- );
- updated = upsertCspMeta(
- updated,
- `default-src 'self'; ${simpleCsp} style-src 'self' 'unsafe-inline'; base-uri 'self';`
- );
- }
- fs.writeFileSync(file, updated);
-});
-
-// --- Manifest: CSP and host_permissions from public/manifest.json ---
-const manifestPath = path.join(dist, "manifest.json");
-if (fs.existsSync(manifestPath)) {
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
- const publicManifestPath = path.join(root, "public", "manifest.json");
- const publicManifest = fs.existsSync(publicManifestPath)
- ? JSON.parse(fs.readFileSync(publicManifestPath, "utf8"))
- : null;
- manifest.content_security_policy = manifest.content_security_policy || {};
- manifest.content_security_policy.extension_pages =
- publicManifest?.content_security_policy?.extension_pages || simpleCsp;
- if (publicManifest?.host_permissions) {
- manifest.host_permissions = publicManifest.host_permissions;
- }
- if (manifest.web_accessible_resources) {
- manifest.web_accessible_resources = manifest.web_accessible_resources.map((entry) => {
- if (entry.resources) {
- entry.resources = entry.resources.map((res) =>
- res.replace(/^_next\//, "next/").replace(/\/_next\//g, "/next/")
- );
- }
- return entry;
- });
- }
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
-}
-
-// --- Auth module inlined into panel.js (PKCE + JWKS); no separate script tag ---
-const authJsPath = path.join(root, "src", "lib", "auth.js");
-const authJs = fs.existsSync(authJsPath) ? fs.readFileSync(authJsPath, "utf8") : "";
-
-/**
- * Panel script template. Injected into dist/panel.js after auth.
- * Flow: load config → syncAuthUI (if authenticated render app with tabs, else landing) →
- * AI tab: verify claim via background VERIFY_CLAIM; Discussion: list/detail + GET_* / CREATE_* / VOTE_POST.
- * Discussion store: discussions[], postsByDiscussionId[discussionId]; normalized with created_at, hasVoted, userVote.
- */
-const panelJs = `document.addEventListener('DOMContentLoaded', () => {
- const root = document.getElementById('root');
- try {
- let API_URL = '';
- let AUTH0_CLIENT_ID = '';
- let activeTab = 'ai';
- let authError = null;
- let pendingDiscussionId = null;
- let currentSourceIndex = 0;
- let isVerifying = false;
- let lastClaimText = '';
- let lastVerifiedClaim = '';
- let lastFactCheck = null;
- let lastAnalysisResult = null;
- const normalizeError = (err) => {
- if (!err) return 'Something went wrong. Please try again.';
- if (typeof err === 'string') return err;
- if (err instanceof Error) return err.message || 'Something went wrong. Please try again.';
- if (typeof err === 'object') {
- try {
- return (
- err.error_description ||
- err.message ||
- JSON.stringify(err)
- );
- } catch {
- return 'Something went wrong. Please try again.';
- }
- }
- return 'Something went wrong. Please try again.';
- };
-
- const setInlineMessage = (msg) => {
- const inline = document.getElementById('authErrorDisplay') || root?.querySelector('#authStatusText');
- if (inline) inline.textContent = msg || '';
- };
-
- const setAuthStatus = (msg) => {
- const statusEl = root?.querySelector('#authStatusText') || document.getElementById('authStatusText');
- if (statusEl) statusEl.textContent = msg || '';
- };
-
- const tabs = {
- ai: { label: 'AI Fact Verification', body: 'Placeholder content.' },
- discussion: { label: 'Discussion Hub', body: 'Placeholder content.' },
- expert: { label: 'Contact an Expert', body: 'Placeholder content.' },
- };
-
- /** Show Verifying pill when active === true; remove from DOM when active === false. */
- const toggleVerifyingUI = (active) => {
- if (active) {
- const container = root?.querySelector('#verifyingStatusContainer');
- if (!container || container.querySelector('#statusChip')) return;
- const pill = document.createElement('span');
- pill.id = 'statusChip';
- pill.className = 'statusChip status--verifying';
- pill.setAttribute('aria-live', 'polite');
- pill.innerHTML = 'Verifying ';
- container.appendChild(pill);
- } else {
- const chip = root?.querySelector('#statusChip');
- if (chip) chip.remove();
- }
- };
-
- /** Single source of truth: set isVerifying and show/remove Verifying pill. */
- const setVerifying = (active) => {
- isVerifying = !!active;
- toggleVerifyingUI(active);
- updateVerifyButtonState();
- };
-
- const updateVerifyButtonState = () => {
- const claimInput = root?.querySelector('#claimInput');
- const verifyBtn = root?.querySelector('#verifyBtn');
- if (!verifyBtn) return;
- const trimmed = (claimInput?.value || '').trim();
- verifyBtn.disabled = isVerifying || trimmed.length === 0 || trimmed === lastVerifiedClaim;
- };
-
- const escapeHtml = (value) => {
- return String(value || '')
- .replace(/&/g, '&')
- .replace(/
/g, '>')
- .replace(/"/g, '"')
- .replace(/'/g, ''');
- };
-
- const truncateAtWord = (text, maxLen) => {
- const str = String(text || '');
- if (str.length <= maxLen) return { display: str, isLong: false };
- const slice = str.slice(0, maxLen);
- const lastSpace = slice.lastIndexOf(' ');
- const cut = lastSpace > 0 ? lastSpace : maxLen;
- return { display: str.slice(0, cut), isLong: true };
- };
-
- const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
- const formatAbsoluteTime = (timestamp) => {
- if (timestamp == null || !Number.isFinite(timestamp)) return '';
- const t = timestamp < 1e12 ? timestamp * 1000 : timestamp;
- const d = new Date(t);
- const month = MONTH_NAMES[d.getMonth()];
- const day = d.getDate();
- const year = d.getFullYear();
- const h = d.getHours();
- const m = d.getMinutes();
- const hh = String(h).padStart(2, '0');
- const mm = String(m).padStart(2, '0');
- return month + ' ' + day + ', ' + year + ' · ' + hh + ':' + mm;
- };
-
- let currentUserId = null;
- const parseJwtSub = (token) => {
- if (!token || typeof token !== 'string') return null;
- try {
- const parts = token.split('.');
- if (parts.length !== 3) return null;
- const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
- return payload.sub != null ? String(payload.sub) : null;
- } catch (_) {
- return null;
- }
- };
-
- const truncateText = (text, limit = 140) => {
- const value = String(text || '');
- if (value.length <= limit) return value;
- return value.slice(0, limit).trim() + '…';
- };
-
- const discussionStore = { discussions: [], postsByDiscussionId: {} };
-
- const parseTimestamp = (v) => {
- if (v == null) return null;
- if (typeof v === 'number' && Number.isFinite(v)) return v < 1e12 ? v * 1000 : v;
- if (typeof v === 'string') { const n = Date.parse(v); return Number.isFinite(n) ? n : null; }
- return null;
- };
-
- const mapApiDiscussion = (d) => {
- const id = String(d.id ?? d.discussion_id ?? '');
- const title = d.title ?? 'Discussion';
- const nonEmpty = (v) => v != null && String(v).trim() !== '';
- const trim = (v) => (v != null ? String(v).trim() : '');
- let description = '';
- if (nonEmpty(d.description)) description = trim(d.description);
- else if (nonEmpty(d.text)) description = trim(d.text);
- else if (nonEmpty(d.body)) description = trim(d.body);
- else if (nonEmpty(d.content)) description = trim(d.content);
- const user_id = d.user_id != null ? d.user_id : undefined;
- const author = (user_id != null && String(user_id).trim() !== '') ? ('u/' + String(user_id).trim()) : 'u/anonymous';
- const created_at = parseTimestamp(d.created_at) ?? parseTimestamp(d.createdAt) ?? null;
- const updated_at = parseTimestamp(d.updated_at) ?? parseTimestamp(d.updatedAt) ?? null;
- return {
- id,
- title,
- description,
- analysis_id: d.analysis_id ?? undefined,
- user_id,
- created_at,
- updated_at,
- author,
- createdAt: created_at != null ? created_at : undefined,
- voteScore: d.vote_score ?? d.voteScore ?? 0,
- postCount: d.post_count ?? d.postCount ?? 0,
- };
- };
-
- const mapApiPost = (p, discussionId) => {
- const voteScore = p.vote_score ?? p.voteScore ?? 0;
- const rawUp =
- Number.isFinite(p.up_votes) ? p.up_votes :
- Number.isFinite(p.upvotes) ? p.upvotes :
- undefined;
- const rawDown =
- Number.isFinite(p.down_votes) ? p.down_votes :
- Number.isFinite(p.downvotes) ? p.downvotes :
- undefined;
- const upvotes = Number.isFinite(rawUp) ? rawUp : Math.max(0, Math.floor(voteScore * 0.7));
- const downvotes = Number.isFinite(rawDown) ? rawDown : Math.max(0, Math.round(voteScore) - upvotes);
- const created_at = parseTimestamp(p.created_at) ?? parseTimestamp(p.createdAt) ?? null;
- const userVoteRaw = p.user_vote ?? p.current_user_vote;
- const userVote = (userVoteRaw === 'up' || userVoteRaw === 'down') ? userVoteRaw : null;
- const hasVoted = userVote != null;
- return {
- id: String(p.id ?? p.post_id ?? ''),
- discussionId: String(discussionId),
- title: p.title ?? '',
- body: p.text ?? p.body ?? '',
- created_at,
- createdAt: created_at != null ? created_at : (typeof p.created_at === 'number' ? (p.created_at < 1e12 ? p.created_at * 1000 : p.created_at) : (p.createdAt ?? Date.now())),
- voteScore,
- upvotes,
- downvotes,
- hasVoted,
- userVote,
- };
- };
-
-
- const discussionState = {
- view: 'list',
- selectedId: null,
- discussionCursor: 0,
- discussionsHasMore: true,
- discussionsLoading: false,
- discussionSort: 'new',
- postsCursorById: {},
- postsHasMoreById: {},
- postsLoadingById: {},
- postsSortById: {},
- };
-
- const fetchDiscussions = async ({ cursor = 0, limit = 6, sort = 'new' } = {}) => {
- if (cursor === 0 && discussionStore.discussions.length === 0 && API_URL) {
- try {
- const token = await ensureAccessToken();
- const res = await sendBackgroundMessage({ type: 'GET_DISCUSSIONS', apiUrl: API_URL, accessToken: token });
- if (res.success && Array.isArray(res.discussions)) {
- discussionStore.discussions = res.discussions.map(mapApiDiscussion);
- }
- } catch (_) {}
- }
- const sorted = [...discussionStore.discussions].sort((a, b) => {
- if (sort === 'top') return b.voteScore - a.voteScore;
- return b.createdAt - a.createdAt;
- });
- const start = Math.max(0, cursor);
- const items = sorted.slice(start, start + limit);
- const nextCursor = start + limit < sorted.length ? start + limit : null;
- return { items, nextCursor, hasMore: nextCursor !== null };
- };
-
- const fetchPosts = ({ discussionId, cursor = 0, limit = 6, sort = 'top' } = {}) => {
- const base = discussionStore.postsByDiscussionId[discussionId] || [];
- const sorted = [...base].sort((a, b) => {
- if (sort === 'new') return b.createdAt - a.createdAt;
- return b.voteScore - a.voteScore;
- });
- const start = Math.max(0, cursor);
- const items = sorted.slice(start, start + limit);
- const nextCursor = start + limit < sorted.length ? start + limit : null;
- return Promise.resolve({ items, nextCursor, hasMore: nextCursor !== null });
- };
-
- const buildFactCheckPostBody = ({ claim, score, headline, subhead, summary, sources }) => {
- const lines = [];
- if (claim) lines.push('Claim: ' + claim);
- if (Number.isFinite(score)) lines.push('Reliability: ' + score + '%');
- if (headline) lines.push('Headline: ' + headline);
- if (subhead) lines.push('Subhead: ' + subhead);
- if (summary) {
- lines.push('', 'Summary:', summary);
- }
- if (Array.isArray(sources) && sources.length > 0) {
- lines.push('', 'Sources:');
- sources.forEach((source, index) => {
- const title = source?.title || source?.name || ('Source ' + (index + 1));
- const url = source?.url || source?.link || '';
- const cred = getCredibilityPercent(source, index, 'real');
- let line = '- ' + title;
- if (url) line += ' (' + url + ')';
- if (cred !== null) line += ' — Credibility: ' + cred + '%';
- lines.push(line);
- });
- }
- lines.push('', 'Generated by Veracity AI fact verification.');
- return lines.join(String.fromCharCode(10));
- };
-
- const buildDiscussionItemHtml = (discussion) => {
- const titleText = discussion.title || 'Fact-check discussion';
- const descriptionText = discussion.description || '';
- const previewText = truncateText(descriptionText, 140);
- const timeText = formatAbsoluteTime(discussion.created_at);
- const metaHtml = timeText ? ('' + '' + escapeHtml(timeText) + ' ' + '
') : '';
- return (
- '' +
- '
' + escapeHtml(titleText) + '
' +
- '
' + escapeHtml(previewText) + '
' +
- metaHtml +
- '
'
- );
- };
-
- const buildPostItemHtml = (post) => {
- const bodyHtml = escapeHtml(post.body || '').replace(__VERACITY_BODY_NEWLINE_REGEX__, ' ');
- const upCount = Number.isFinite(post.upvotes) ? post.upvotes : Math.max(0, Math.floor(post.voteScore * 0.7));
- const downCount = Number.isFinite(post.downvotes) ? post.downvotes : Math.max(0, post.voteScore - upCount);
- const timeText = formatAbsoluteTime(post.created_at ?? post.createdAt);
- const metaHtml = timeText ? ('' + escapeHtml(timeText) + '
') : '';
- const voted = post.hasVoted === true;
- const upDisabled = voted ? ' disabled' : '';
- const downDisabled = voted ? ' disabled' : '';
- return (
- '' +
- '
' +
- '
' + bodyHtml + '
' +
- '
' +
- '
' +
- '▲ ' +
- '' + upCount + ' ' +
- '
' +
- '
' +
- '▼ ' +
- '' + downCount + ' ' +
- '
' +
- '
' +
- metaHtml +
- '
' +
- '
'
- );
- };
-
- const renderDiscussionListView = (panelEl) => {
- discussionState.view = 'list';
- discussionState.selectedId = null;
- panelEl.innerHTML = [
- '',
- ].join('');
-
- const listEl = panelEl.querySelector('#discussionList');
- const scrollEl = panelEl.querySelector('#discussionScroll');
- const loadingEl = panelEl.querySelector('#discussionLoading');
- const endEl = panelEl.querySelector('#discussionEnd');
- const appendDiscussions = (items) => {
- if (!listEl) return;
- const html = items.map(buildDiscussionItemHtml).join('');
- listEl.insertAdjacentHTML('beforeend', html);
- };
-
- const loadMore = async () => {
- if (discussionState.discussionsLoading || !discussionState.discussionsHasMore) return;
- discussionState.discussionsLoading = true;
- if (loadingEl) loadingEl.classList.remove('isHidden');
- const result = await fetchDiscussions({
- cursor: discussionState.discussionCursor,
- limit: 6,
- sort: discussionState.discussionSort,
- });
- appendDiscussions(result.items);
- discussionState.discussionCursor = result.nextCursor || discussionState.discussionCursor;
- discussionState.discussionsHasMore = result.hasMore;
- discussionState.discussionsLoading = false;
- if (loadingEl) loadingEl.classList.add('isHidden');
- if (!result.hasMore && endEl) endEl.classList.remove('isHidden');
- };
-
- const resetListState = () => {
- discussionState.discussionCursor = 0;
- discussionState.discussionsHasMore = true;
- discussionState.discussionsLoading = false;
- if (listEl) listEl.innerHTML = '';
- if (endEl) endEl.classList.add('isHidden');
- };
-
- resetListState();
- loadMore();
-
- scrollEl?.addEventListener('scroll', () => {
- if (!scrollEl) return;
- const nearBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 80;
- if (nearBottom) loadMore();
- });
-
- listEl?.addEventListener('click', (event) => {
- const target = event.target;
- const item = target?.closest?.('[data-discussion-id]');
- if (!item) return;
- const id = item.getAttribute('data-discussion-id');
- if (id) renderDiscussionDetailView(panelEl, id);
- });
-
- };
-
- const renderDiscussionDetailView = async (panelEl, discussionId) => {
- if (API_URL) {
- try {
- const token = await ensureAccessToken();
- const [discRes, postsRes] = await Promise.all([
- sendBackgroundMessage({ type: 'GET_DISCUSSION', apiUrl: API_URL, accessToken: token, discussionId }),
- sendBackgroundMessage({ type: 'GET_POSTS', apiUrl: API_URL, accessToken: token, discussionId }),
- ]);
- if (discRes.success && discRes.discussion) {
- const mapped = mapApiDiscussion(discRes.discussion);
- const sid = String(mapped.id);
- const idx = discussionStore.discussions.findIndex((d) => String(d.id) === sid);
- if (idx >= 0) {
- const existing = discussionStore.discussions[idx];
- const hasExistingDesc = existing.description != null && String(existing.description).trim() !== '';
- const hasIncomingDesc = mapped.description != null && String(mapped.description).trim() !== '';
- let finalDescription = mapped.description;
- if (hasExistingDesc && !hasIncomingDesc) finalDescription = existing.description ?? '';
- const finalUserId = mapped.user_id != null && mapped.user_id !== '' ? mapped.user_id : (existing.user_id ?? mapped.user_id);
- const finalCreatedAt = (mapped.created_at != null && mapped.created_at !== '') ? mapped.created_at : (existing.created_at ?? mapped.created_at);
- const finalUpdatedAt = (mapped.updated_at != null && mapped.updated_at !== '') ? mapped.updated_at : (existing.updated_at ?? mapped.updated_at);
- discussionStore.discussions[idx] = {
- ...mapped,
- description: finalDescription,
- user_id: finalUserId,
- created_at: finalCreatedAt,
- updated_at: finalUpdatedAt,
- author: (finalUserId != null && String(finalUserId).trim() !== '') ? ('u/' + String(finalUserId).trim()) : (mapped.author ?? 'u/anonymous'),
- createdAt: finalCreatedAt != null ? finalCreatedAt : existing.createdAt,
- };
- } else {
- discussionStore.discussions.unshift(mapped);
- }
- }
- if (postsRes.success && Array.isArray(postsRes.posts)) {
- const existingList = discussionStore.postsByDiscussionId[discussionId] || [];
- const existingById = {};
- existingList.forEach((p) => { existingById[String(p.id)] = p; });
- const merged = postsRes.posts.map((p) => {
- const mapped = mapApiPost(p, discussionId);
- const existing = existingById[mapped.id];
- if (existing) {
- const apiUp = Number.isFinite(mapped.upvotes) ? mapped.upvotes : 0;
- const apiDown = Number.isFinite(mapped.downvotes) ? mapped.downvotes : 0;
- const localUp = Number.isFinite(existing.upvotes) ? existing.upvotes : 0;
- const localDown = Number.isFinite(existing.downvotes) ? existing.downvotes : 0;
- const mergedUp = apiUp > 0 ? apiUp : localUp;
- const mergedDown = apiDown > 0 ? apiDown : localDown;
- return { ...mapped, upvotes: mergedUp, downvotes: mergedDown };
- }
- return mapped;
- });
- discussionStore.postsByDiscussionId[discussionId] = merged;
- }
- } catch (_) {}
- }
- const discussion = discussionStore.discussions.find((item) => String(item.id) === String(discussionId));
- if (!discussion) {
- renderDiscussionListView(panelEl);
- return;
- }
- const existingPosts = discussionStore.postsByDiscussionId[discussionId] || [];
- if (!discussion.description && existingPosts.length > 0) {
- const firstPost = existingPosts[0];
- const body = firstPost?.body || '';
- if (body.includes('Generated by Veracity AI fact verification.')) {
- discussion.description = body;
- }
- }
- discussionState.view = 'detail';
- discussionState.selectedId = discussionId;
-
- const currentSort = discussionState.postsSortById[discussionId] || 'top';
- const discussionBody = discussion.description ?? '';
- const descTruncated = discussionBody ? truncateAtWord(discussionBody, 300) : null;
- const descriptionBlockHtml = !discussionBody
- ? ''
- : descTruncated.isLong
- ? '' +
- '
' +
- '
Read more ' +
- '
'
- : '
';
- const detailHeaderHtml =
- '';
- panelEl.innerHTML = [
- '',
- '
← Back ',
- ' ' + detailHeaderHtml,
- '
',
- '
',
- ' Top ',
- ' New ',
- '
',
- '
New post ',
- '
',
- '
',
- '
',
- '
',
- ].join('');
-
- const backBtn = panelEl.querySelector('#discussionBackBtn');
- const postsList = panelEl.querySelector('#postsList');
- const postsScroll = panelEl.querySelector('#postsScroll');
- const loadingEl = panelEl.querySelector('#postsLoading');
- const endEl = panelEl.querySelector('#postsEnd');
- const sortButtons = Array.from(panelEl.querySelectorAll('.sortPill'));
- const newPostToggle = panelEl.querySelector('#newPostToggle');
- const newPostForm = panelEl.querySelector('#newPostForm');
- const newPostCancel = panelEl.querySelector('#newPostCancel');
- const postBody = panelEl.querySelector('#postBody');
- const postError = panelEl.querySelector('#postError');
-
- const descriptionTextEl = panelEl.querySelector('#discussionDetailDescriptionText');
- if (descriptionTextEl && discussionBody) {
- descriptionTextEl.style.whiteSpace = 'pre-wrap';
- if (descTruncated.isLong) {
- descriptionTextEl.textContent = descTruncated.display + '…';
- const toggleBtn = panelEl.querySelector('#discussionDescriptionToggle');
- if (toggleBtn) {
- const runToggle = () => {
- const expanded = descriptionTextEl.getAttribute('data-expanded') === '1';
- if (expanded) {
- descriptionTextEl.textContent = descTruncated.display + '…';
- descriptionTextEl.removeAttribute('data-expanded');
- toggleBtn.textContent = 'Read more';
- } else {
- descriptionTextEl.textContent = discussionBody;
- descriptionTextEl.setAttribute('data-expanded', '1');
- toggleBtn.textContent = 'Show less';
- }
- };
- toggleBtn.addEventListener('click', runToggle);
- toggleBtn.addEventListener('keydown', (e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- runToggle();
- }
- });
- }
- } else {
- descriptionTextEl.textContent = discussionBody;
- }
- }
-
- const resetPostsState = () => {
- discussionState.postsCursorById[discussionId] = 0;
- discussionState.postsHasMoreById[discussionId] = true;
- discussionState.postsLoadingById[discussionId] = false;
- if (postsList) postsList.innerHTML = '';
- if (endEl) endEl.classList.add('isHidden');
- };
-
- const applyVotedState = (container) => {
- if (!container) return;
- const posts = discussionStore.postsByDiscussionId[discussionId] || [];
- const cards = Array.from(container.querySelectorAll('.postCard'));
- cards.forEach((card) => {
- const postId = card.getAttribute('data-post-id');
- if (!postId) return;
- const post = posts.find((p) => String(p.id) === String(postId));
- if (!post || post.hasVoted !== true) return;
- const upBtn = card.querySelector('.voteBtn.upvote');
- const downBtn = card.querySelector('.voteBtn.downvote');
- if (upBtn) upBtn.disabled = true;
- if (downBtn) downBtn.disabled = true;
- if (post.userVote === 'up' && upBtn) upBtn.classList.add('voted');
- if (post.userVote === 'down' && downBtn) downBtn.classList.add('voted');
- });
- };
-
- const appendPosts = (items) => {
- if (!postsList) return;
- const html = items.map(buildPostItemHtml).join('');
- postsList.insertAdjacentHTML('beforeend', html);
- applyVotedState(postsList);
- };
-
- const loadMorePosts = async () => {
- if (discussionState.postsLoadingById[discussionId]) return;
- if (!discussionState.postsHasMoreById[discussionId]) return;
- discussionState.postsLoadingById[discussionId] = true;
- if (loadingEl) loadingEl.classList.remove('isHidden');
- const result = await fetchPosts({
- discussionId,
- cursor: discussionState.postsCursorById[discussionId] || 0,
- limit: 6,
- sort: discussionState.postsSortById[discussionId] || 'top',
- });
- appendPosts(result.items);
- discussionState.postsCursorById[discussionId] = result.nextCursor || discussionState.postsCursorById[discussionId] || 0;
- discussionState.postsHasMoreById[discussionId] = result.hasMore;
- discussionState.postsLoadingById[discussionId] = false;
- if (loadingEl) loadingEl.classList.add('isHidden');
- if (!result.hasMore && endEl) endEl.classList.remove('isHidden');
- };
-
- resetPostsState();
- loadMorePosts();
-
- postsScroll?.addEventListener('scroll', () => {
- if (!postsScroll) return;
- const nearBottom = postsScroll.scrollTop + postsScroll.clientHeight >= postsScroll.scrollHeight - 80;
- if (nearBottom) loadMorePosts();
- });
-
- postsList?.addEventListener('click', async (event) => {
- const target = event.target;
- const voteButton = target?.closest?.('.voteBtn');
- if (!voteButton) return;
- const postId = voteButton.getAttribute('data-id');
- const post = (discussionStore.postsByDiscussionId[discussionId] || []).find((item) => item.id === postId);
- if (!post) return;
- if (post.hasVoted === true) return;
- const voteType = voteButton.getAttribute('data-vote');
- if (voteType !== 'up' && voteType !== 'down') return;
- if (!Number.isFinite(post.upvotes)) post.upvotes = Math.max(0, Math.floor(post.voteScore * 0.7));
- if (!Number.isFinite(post.downvotes)) post.downvotes = Math.max(0, post.voteScore - post.upvotes);
- const prevUp = post.upvotes;
- const prevDown = post.downvotes;
- if (voteType === 'up') {
- post.upvotes += 1;
- const upEl = panelEl.querySelector('#up-' + post.id);
- if (upEl) upEl.textContent = String(post.upvotes);
- } else {
- post.downvotes += 1;
- const downEl = panelEl.querySelector('#down-' + post.id);
- if (downEl) downEl.textContent = String(post.downvotes);
- }
- const card = voteButton.closest('.postCard');
- if (card) {
- const upBtn = card.querySelector('.voteBtn.upvote');
- const downBtn = card.querySelector('.voteBtn.downvote');
- if (upBtn) upBtn.disabled = true;
- if (downBtn) downBtn.disabled = true;
- if (voteType === 'up' && upBtn) upBtn.classList.add('voted');
- if (voteType === 'down' && downBtn) downBtn.classList.add('voted');
- }
- if (API_URL) {
- try {
- const token = await ensureAccessToken();
- const res = await sendBackgroundMessage({
- type: 'VOTE_POST',
- apiUrl: API_URL,
- accessToken: token,
- post_id: String(postId),
- vote: voteType,
- });
- if (!res || !res.success) {
- post.upvotes = prevUp;
- post.downvotes = prevDown;
- const upEl = panelEl.querySelector('#up-' + post.id);
- const downEl = panelEl.querySelector('#down-' + post.id);
- if (upEl) upEl.textContent = String(prevUp);
- if (downEl) downEl.textContent = String(prevDown);
- if (card) {
- const upB = card.querySelector('.voteBtn.upvote');
- const downB = card.querySelector('.voteBtn.downvote');
- if (upB) { upB.disabled = false; upB.classList.remove('voted'); }
- if (downB) { downB.disabled = false; downB.classList.remove('voted'); }
- }
- } else {
- post.hasVoted = true;
- post.userVote = voteType;
- const data = res.data;
- if (data && (Number.isFinite(data.upvotes) || Number.isFinite(data.downvotes))) {
- if (Number.isFinite(data.upvotes)) post.upvotes = data.upvotes;
- if (Number.isFinite(data.downvotes)) post.downvotes = data.downvotes;
- const upEl = panelEl.querySelector('#up-' + post.id);
- const downEl = panelEl.querySelector('#down-' + post.id);
- if (upEl) upEl.textContent = String(post.upvotes);
- if (downEl) downEl.textContent = String(post.downvotes);
- }
- }
- } catch (_) {
- post.upvotes = prevUp;
- post.downvotes = prevDown;
- const upEl = panelEl.querySelector('#up-' + post.id);
- const downEl = panelEl.querySelector('#down-' + post.id);
- if (upEl) upEl.textContent = String(prevUp);
- if (downEl) downEl.textContent = String(prevDown);
- if (card) {
- const upB = card.querySelector('.voteBtn.upvote');
- const downB = card.querySelector('.voteBtn.downvote');
- if (upB) { upB.disabled = false; upB.classList.remove('voted'); }
- if (downB) { downB.disabled = false; downB.classList.remove('voted'); }
- }
- }
- }
- });
-
- sortButtons.forEach((btn) => {
- btn.addEventListener('click', () => {
- const sort = btn.getAttribute('data-sort');
- if (!sort || sort === discussionState.postsSortById[discussionId]) return;
- discussionState.postsSortById[discussionId] = sort;
- sortButtons.forEach((inner) => {
- inner.classList.toggle('sortPillActive', inner.getAttribute('data-sort') === sort);
- });
- resetPostsState();
- loadMorePosts();
- });
- });
-
- newPostToggle?.addEventListener('click', () => {
- newPostForm?.classList.toggle('isHidden');
- if (postError) postError.textContent = '';
- });
-
- newPostCancel?.addEventListener('click', () => {
- newPostForm?.classList.add('isHidden');
- if (postError) postError.textContent = '';
- });
-
- newPostForm?.addEventListener('submit', async (event) => {
- event.preventDefault();
- const body = (postBody?.value || '').trim();
- if (!body) {
- if (postError) postError.textContent = 'Please enter a post.';
- return;
- }
- if (postError) postError.textContent = '';
- if (!API_URL) {
- if (postError) postError.textContent = 'Something went wrong. Please try again.';
- return;
- }
- try {
- const token = await ensureAccessToken();
- const res = await sendBackgroundMessage({
- type: 'CREATE_POST',
- apiUrl: API_URL,
- accessToken: token,
- discussion_id: discussionId,
- text: body,
- });
- if (!res.success) {
- if (postError) postError.textContent = 'Something went wrong. Please try again.';
- return;
- }
- const postsRes = await sendBackgroundMessage({
- type: 'GET_POSTS',
- apiUrl: API_URL,
- accessToken: token,
- discussionId,
- });
- if (postsRes.success && Array.isArray(postsRes.posts)) {
- discussionStore.postsByDiscussionId[discussionId] = postsRes.posts.map((p) => mapApiPost(p, discussionId));
- }
- resetPostsState();
- loadMorePosts();
- if (postBody) postBody.value = '';
- newPostForm?.classList.add('isHidden');
- } catch (_) {
- if (postError) postError.textContent = 'Something went wrong. Please try again.';
- }
- });
-
- backBtn?.addEventListener('click', () => {
- renderDiscussionListView(panelEl);
- });
- };
-
- const getCredibilityPercent = (source, index, mode) => {
- if (mode === 'test') {
- const mockValues = [93, 88, 79, 91, 85];
- return mockValues[index] ?? 93;
- }
- const raw =
- source?.credibility ??
- source?.credibility_score ??
- source?.reliability ??
- source?.score;
- if (raw === null || raw === undefined) return null;
- const parsed = typeof raw === 'string' ? parseFloat(raw) : Number(raw);
- if (!Number.isFinite(parsed)) return null;
- const normalized = parsed >= 0 && parsed <= 1 ? parsed * 100 : parsed;
- return Math.round(Math.max(0, Math.min(100, normalized)));
- };
-
- const extractSources = (result) => {
- const candidates = [
- result?.sources,
- result?.citations,
- result?.references,
- result?.source_list,
- result?.data?.sources,
- result?.analysis?.sources,
- ];
- const match = candidates.find((item) => Array.isArray(item));
- return match || [];
- };
-
- const renderSources = (sources, mode, activeIndex) => {
- if (!Array.isArray(sources) || sources.length === 0) return '';
- const safeIndex = Math.max(0, Math.min(sources.length - 1, activeIndex || 0));
- const source = sources[safeIndex] || {};
- const credibility = getCredibilityPercent(source, safeIndex, mode);
- const title = source?.title || source?.name || ('Source ' + (safeIndex + 1));
- const snippet = source?.snippet || source?.summary || '';
- const url = source?.url || source?.link || '';
- const displayUrl = url && url.length > 35 ? url.substring(0, 35) + '...' : url;
- const credibilityHtml = credibility === null
- ? ''
- : 'Credibility: ' + credibility + '%
';
- const contentHtml =
- '' +
- credibilityHtml +
- '
' + escapeHtml(title) + '
' +
- (snippet ? '
' + escapeHtml(snippet) + '
' : '') +
- (displayUrl ? '
' + escapeHtml(displayUrl) + '
' : '') +
- '
';
- const wrappedCard = url
- ? '' + contentHtml + ' '
- : contentHtml;
- const showArrows = sources.length > 1;
- return (
- '' +
- (showArrows
- ? '
' +
- '' +
- ' ' +
- ' ' +
- ' '
- : '') +
- '
' +
- '
Source ' + (safeIndex + 1) + '
' +
- wrappedCard +
- '
' +
- (showArrows
- ? '
' +
- '' +
- ' ' +
- ' ' +
- ' '
- : '') +
- '
'
- );
- };
-
- const renderSourcesInto = (mount, sources, mode) => {
- if (!mount) return;
- if (!Array.isArray(sources) || sources.length === 0) {
- mount.innerHTML = '';
- return;
- }
- const update = () => {
- mount.innerHTML = renderSources(sources, mode, currentSourceIndex);
- const prevBtn = mount.querySelector('[data-direction="prev"]');
- const nextBtn = mount.querySelector('[data-direction="next"]');
- prevBtn?.addEventListener('click', () => {
- currentSourceIndex = (currentSourceIndex - 1 + sources.length) % sources.length;
- update();
- });
- nextBtn?.addEventListener('click', () => {
- currentSourceIndex = (currentSourceIndex + 1) % sources.length;
- update();
- });
- };
- update();
- };
-
- const setScore = (score, { summary, headline, subhead, result, claim_id, analysis_id } = {}) => {
- const mount = root?.querySelector('#resultMount');
- if (!mount) return;
-
- if (!Number.isFinite(score)) {
- lastFactCheck = null;
- mount.innerHTML = '';
- ensureCreateDiscussionButton();
- return;
- }
-
- const sources = extractSources(result);
- currentSourceIndex = 0;
- const clamped = Math.max(0, Math.min(100, parseFloat(score)));
- const deg = clamped * 3.6;
- lastFactCheck = {
- claim: lastClaimText,
- score: Number(score),
- headline,
- subhead,
- summary,
- sources,
- result,
- claim_id,
- analysis_id,
- };
- const bucket =
- clamped >= 85
- ? { h: 'The claim is highly reliable,', s: 'you can share with your network.' }
- : clamped >= 60
- ? { h: 'The claim is fairly reliable,', s: 'consider sharing with light caution.' }
- : clamped >= 40
- ? { h: 'The claim is uncertain,', s: 'seek additional verification.' }
- : { h: 'The claim is likely unreliable,', s: 'avoid sharing without verification.' };
-
- const cardHtml =
- '' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
Reliability
' +
- '
' + clamped + '%
' +
- '
' +
- '
' +
- '
' +
- '
' + (headline || bucket.h) + '
' +
- '
' + (subhead || bucket.s) + '
' +
- (summary
- ? '
' +
- '
' + summary + '
' +
- '
Read more ' +
- '
'
- : '') +
- '
' +
- '
' +
- '
';
-
- mount.innerHTML = cardHtml + '
';
- const summaryEl = mount.querySelector('#scoreSummary');
- const toggleBtn = mount.querySelector('#summaryToggle');
- if (summaryEl && toggleBtn) {
- toggleBtn.addEventListener('click', () => {
- const collapsed = summaryEl.classList.contains('summaryCollapsed');
- if (collapsed) {
- summaryEl.classList.remove('summaryCollapsed');
- toggleBtn.textContent = 'Read less';
- } else {
- summaryEl.classList.add('summaryCollapsed');
- toggleBtn.textContent = 'Read more';
- }
- });
- }
- const sourcesMount = mount.querySelector('#sourcesMount');
- if (sourcesMount && sources.length > 0) {
- renderSourcesInto(sourcesMount, sources, 'real');
- }
- ensureCreateDiscussionButton();
- };
- const ensureCreateDiscussionButton = () => {
- if (activeTab !== 'ai') {
- root?.querySelector('#createDiscussionBtn')?.remove();
- root?.querySelector('#createDiscussionContainer')?.remove();
- return;
- }
- const shouldShow = !!lastFactCheck && Number.isFinite(lastFactCheck.score) && !isVerifying;
- const aiPanel = root?.querySelector('#aiTabPanel');
- if (!aiPanel) return;
- const existingBtn = aiPanel.querySelector('#createDiscussionBtn');
- const existingContainer = aiPanel.querySelector('#createDiscussionContainer');
- if (!shouldShow) {
- existingBtn?.remove();
- existingContainer?.remove();
- return;
- }
- if (existingBtn || existingContainer) return;
- root?.querySelector('#createDiscussionContainer')?.remove();
- const anchor =
- aiPanel.querySelector('#sourcesMount') ||
- aiPanel.querySelector('#scoreBox') ||
- aiPanel;
- const container = document.createElement('div');
- container.id = 'createDiscussionContainer';
- container.className = 'createDiscussionCta';
- container.innerHTML =
- 'Want to discuss this result?
' +
- 'Create discussion ' +
- '
';
- if (anchor) {
- anchor.insertAdjacentElement('afterend', container);
- return;
- }
- aiPanel.insertAdjacentElement('beforeend', container);
- };
-
- const getDiscussionDraftDefaults = () => {
- const claim = lastFactCheck?.claim || '';
- const title = claim ? claim.slice(0, 120) : 'Fact-check discussion';
- const body = buildFactCheckPostBody({
- claim: lastFactCheck?.claim,
- score: lastFactCheck?.score,
- headline: lastFactCheck?.headline,
- subhead: lastFactCheck?.subhead,
- summary: lastFactCheck?.summary,
- sources: lastFactCheck?.sources || [],
- });
- return { title, body };
- };
-
- const openCreateDiscussionDraft = () => {
- const aiPanel = root?.querySelector('#aiTabPanel');
- if (!aiPanel || !lastFactCheck) return;
- const mount = aiPanel.querySelector('#createDiscussionFormMount');
- if (!mount) return;
- const defaults = getDiscussionDraftDefaults();
- mount.innerHTML =
- '' +
- 'Title ' +
- ' ' +
- 'Body ' +
- ' ' +
- '' +
- 'Create discussion ' +
- 'Cancel ' +
- '
' +
- '
' +
- ' ';
- const titleInput = mount.querySelector('#discussionTitle');
- const bodyInput = mount.querySelector('#discussionBody');
- if (titleInput) titleInput.value = defaults.title;
- if (bodyInput) bodyInput.value = defaults.body;
- const ctaBtn = root?.querySelector('#createDiscussionBtn');
- if (ctaBtn) ctaBtn.classList.add('isHidden');
- };
-
- const finalizeVerification = () => {
- setVerifying(false);
- lastVerifiedClaim = lastClaimText;
- ensureCreateDiscussionButton();
- };
-
-
- const detectLanguage = () => {
- const lang = navigator.language || '';
- if (lang.toLowerCase().startsWith('fr')) return 'french';
- return 'english';
- };
-
- const ensureAccessToken = async () => {
- const token = await (typeof VeracityAuth !== 'undefined' ? VeracityAuth.getAccessToken() : Promise.resolve(null));
- if (token) {
- if (currentUserId == null && API_URL) {
- try {
- const res = await sendBackgroundMessage({ type: 'GET_ME', apiUrl: API_URL, accessToken: token });
- if (res && res.success && (res.id != null || (res.user && res.user.id != null))) {
- currentUserId = res.id ?? res.user?.id ?? null;
- }
- } catch (_) {}
- }
- if (currentUserId == null) {
- const sub = parseJwtSub(token);
- if (sub) currentUserId = sub;
- }
- return token;
- }
- return await (typeof VeracityAuth !== 'undefined' ? VeracityAuth.login() : Promise.reject(new Error('Auth not loaded')));
- };
-
- const sendBackgroundMessage = (msg) => {
- return new Promise((resolve) => {
- chrome.runtime.sendMessage(msg, (response) => {
- if (chrome.runtime.lastError) resolve({ success: false, error: chrome.runtime.lastError.message });
- else resolve(response || { success: false });
- });
- });
- };
-
- /**
- * Single public verification entry point for all flows.
- *
- * Callers:
- * - AI tab Verify button (no argument → read from textarea)
- * - Context menu “Send to Veracity” (passes selected text)
- *
- * Responsibilities:
- * - Guard against concurrent runs via isVerifying
- * - Normalize + persist claim text and reset result UI
- * - Enforce auth and API configuration
- * - Delegate to background.js VERIFY_CLAIM and map the response into score card UI
- * - Always end via finalizeVerification() so button + status reset correctly
- */
- const startVerification = async (claimTextOverride) => {
- // Ignore if a verification is already in progress.
- if (isVerifying) return;
-
- // Begin verification lifecycle: show Verifying pill and disable Verify button.
- setVerifying(true);
-
- const mount = root?.querySelector('#resultMount');
- const claimInput = root?.querySelector('#claimInput');
- const authStatusText = root?.querySelector('#authStatusText');
-
- // Normalize claim text: prefer explicit argument, otherwise read from textarea.
- let claimText = (typeof claimTextOverride === 'string'
- ? claimTextOverride
- : (claimInput?.value || '')
- ).trim();
-
- // Keep textarea in sync so UI looks identical for all entry points.
- if (claimInput) claimInput.value = claimText;
-
- lastClaimText = claimText;
- lastAnalysisResult = null;
- if (mount) mount.innerHTML = '';
- currentSourceIndex = 0;
- lastFactCheck = null;
- setScore(null);
- ensureCreateDiscussionButton();
-
- // Core verification flow. All completion paths must call finalizeVerification().
- if (!claimText) {
- if (authStatusText) authStatusText.textContent = 'Please enter a claim.';
- finalizeVerification();
- return;
- }
-
- const authed = typeof VeracityAuth !== 'undefined' && await VeracityAuth.isAuthenticated();
- if (!authed) {
- if (authStatusText) authStatusText.textContent = 'Please sign in to verify claims.';
- finalizeVerification();
- return;
- }
-
- if (authStatusText) authStatusText.textContent = '';
-
- const apiConfigured = API_URL && API_URL.indexOf('YOUR_API') === -1;
- if (!apiConfigured) {
- requestAnimationFrame(() => {
- setScore(85, {
- summary: 'Placeholder result for development. Configure API_URL for live verification.',
- headline: 'The claim is fairly reliable,',
- subhead: 'consider sharing with light caution.',
- result: {
- sources: [
- { title: 'Source 1', snippet: 'Placeholder for development.', credibility_score: 0.88 },
- { title: 'Source 2', snippet: 'Placeholder for development.', credibility_score: 0.79 },
- ],
- },
- });
- finalizeVerification();
- });
- return;
- }
-
- let token;
- try {
- token = await ensureAccessToken();
- } catch (err) {
- const msg = 'Something went wrong. Please try again.';
- setInlineMessage(msg);
- if (authStatusText) authStatusText.textContent = msg;
- finalizeVerification();
- return;
- }
-
- if (!token) {
- const msg = 'Please sign in to verify claims.';
- if (authStatusText) authStatusText.textContent = msg;
- setInlineMessage(msg);
- finalizeVerification();
- return;
- }
-
- chrome.runtime.sendMessage(
- { type: 'VERIFY_CLAIM', claimText, accessToken: token, apiUrl: API_URL },
- (response) => {
- if (chrome.runtime.lastError) {
- const msg = 'Something went wrong. Please try again.';
- setInlineMessage(msg);
- if (authStatusText) authStatusText.textContent = msg;
- finalizeVerification();
- return;
- }
- if (!response || response.success !== true) {
- const msg = 'Something went wrong. Please try again.';
- setInlineMessage(msg);
- if (authStatusText) authStatusText.textContent = msg;
- finalizeVerification();
- return;
- }
- const score = Math.round((response.veracity_score || 0) * 100);
- const summary = response.analysis_text || '';
- const result = { sources: Array.isArray(response.sources) ? response.sources : [] };
- const claim_id = response.claim_id ?? null;
- const analysis_id = response.analysis_id ?? null;
- setScore(score, { summary, result, claim_id, analysis_id });
- lastAnalysisResult = { claim: claimText, score, summary, result, claim_id, analysis_id };
- finalizeVerification();
- }
- );
- };
-
- const startWebAuthFlow = async () => {
- const statusEl = document.getElementById('authErrorDisplay');
- if (!AUTH0_CLIENT_ID) {
- const msg = 'Missing AUTH0_CLIENT_ID in config.json';
- authError = msg;
- if (statusEl) statusEl.textContent = msg;
- setAuthStatus(msg);
- await syncAuthUI();
- throw new Error(msg);
- }
- if (statusEl) statusEl.textContent = 'Opening sign-in…';
- try {
- const token = await (typeof VeracityAuth !== 'undefined' ? VeracityAuth.login() : Promise.reject(new Error('Auth not loaded')));
- if (statusEl) statusEl.textContent = '';
- await syncAuthUI();
- return token;
- } catch (err) {
- const fromAuth = err && (err.isCredentialError === true || (typeof err.message === 'string' && /access_denied|invalid_grant|wrong.*password|invalid credentials|username or password/i.test(err.message)));
- if (fromAuth) {
- authError = null;
- if (statusEl) statusEl.textContent = '';
- setAuthStatus('');
- setInlineMessage('');
- } else {
- authError = 'Something went wrong. Please try again.';
- if (statusEl) statusEl.textContent = authError;
- setAuthStatus(authError);
- setInlineMessage(authError);
- }
- if (typeof VeracityAuth !== 'undefined' && VeracityAuth.clearStoredTokens) {
- await VeracityAuth.clearStoredTokens();
- }
- await syncAuthUI();
- throw err;
- }
- };
-
- async function syncAuthUI() {
- if (!root) return;
- try {
- if (typeof VeracityAuth !== 'undefined' && VeracityAuth.isLoginInProgress && VeracityAuth.isLoginInProgress()) {
- renderLandingHero();
- return;
- }
- const ok = typeof VeracityAuth !== 'undefined' && await VeracityAuth.isAuthenticated();
- if (ok) {
- authError = null;
- await renderAppScreen();
- } else {
- renderLandingHero();
- }
- } catch {
- renderLandingHero();
- }
- }
-
-
- const renderLandingHero = () => {
- if (!root) return;
- root.innerHTML = '';
- root.innerHTML = \`
-
-
-
-
-
Welcome to Veracity
-
Navigate information with clarity and confidence guided by a conversational assistant.
-
Sign up or log in to verify information and build trust with those you share it with.
-
-
Log in
-
We strictly limit data collection to only what is essential for functionality—nothing more.
-
Need a larger scale? Visit Veracity Web Application
-
-
-
- \`;
-
- const authErrorEl = root.querySelector('#authErrorDisplay');
- if (authErrorEl) authErrorEl.textContent = authError || '';
-
- const goToLoginBtn = root.querySelector('#goToLoginBtn');
- goToLoginBtn?.addEventListener('click', async () => {
- authError = null;
- try {
- await startWebAuthFlow();
- } catch {}
- });
- };
-
- const renderAppScreen = async () => {
- if (!root) return;
- root.innerHTML = '';
- root.innerHTML = \`
-
-
-
- AI Fact Verification
-
-
- Discussion Hub
-
-
- Contact an Expert
-
-
-
-
-
- \`;
-
- const tabButtons = Array.from(root.querySelectorAll('[data-tab-id]'));
- const panelEl = root.querySelector('#tabPanel');
-
- const renderTabContent = () => {
- if (!panelEl) return;
- if (activeTab === 'ai') {
- panelEl.innerHTML = \`
-
- \`;
- setVerifying(false);
- const verifyBtn = panelEl.querySelector('#verifyBtn');
- const claimInput = panelEl.querySelector('#claimInput');
- updateVerifyButtonState();
- claimInput?.addEventListener('input', updateVerifyButtonState);
- claimInput?.addEventListener('change', updateVerifyButtonState);
- verifyBtn?.addEventListener('click', async () => {
- await startVerification();
- });
- ensureCreateDiscussionButton();
- if (lastAnalysisResult) {
- lastClaimText = lastAnalysisResult.claim;
- lastVerifiedClaim = lastAnalysisResult.claim;
- const input = panelEl.querySelector('#claimInput');
- if (input) input.value = lastAnalysisResult.claim;
- setScore(lastAnalysisResult.score, { summary: lastAnalysisResult.summary, result: lastAnalysisResult.result });
- ensureCreateDiscussionButton();
- updateVerifyButtonState();
- }
- } else if (activeTab === 'discussion') {
- renderDiscussionListView(panelEl);
- } else if (activeTab === 'expert') {
- panelEl.innerHTML = \`
-
-
Need expert insight?
-
-
- \`;
- const expertForm = panelEl.querySelector('#expertForm');
- const nameInput = panelEl.querySelector('#expertName');
- const emailInput = panelEl.querySelector('#expertEmail');
- const messageInput = panelEl.querySelector('#expertMessage');
- const errorText = panelEl.querySelector('#expertErrorText');
- expertForm?.addEventListener('submit', async (event) => {
- event.preventDefault();
- const name = (nameInput?.value || '').trim();
- const email = (emailInput?.value || '').trim();
- const message = (messageInput?.value || '').trim();
- const missing = [];
- if (!name) missing.push('name');
- if (!email) missing.push('email');
- if (!message) missing.push('message');
- if (missing.length > 0) {
- if (errorText) errorText.textContent = 'Please enter your ' + missing.join(', ') + '.';
- return;
- }
- if (errorText) errorText.textContent = '';
- const lines = [
- 'Name: ' + (name || '—'),
- 'Email: ' + (email || '—'),
- '',
- 'Message:',
- message || '—',
- ];
- const subject = 'Veracity – Contact an Expert';
- const body = lines.join('\\n');
- const mailto = 'mailto:veracity@mila.quebec?subject=' +
- encodeURIComponent(subject) +
- '&body=' +
- encodeURIComponent(body);
- window.location.href = mailto;
- });
- }
- };
-
- const updateActiveButtons = () => {
- tabButtons.forEach((btn) => {
- const isActive = btn.dataset.tabId === activeTab;
- btn.setAttribute('aria-selected', String(isActive));
- btn.classList.toggle('Home_tabButtonActive__zVobV', isActive);
- });
- };
-
- tabButtons.forEach((btn) => {
- btn.addEventListener('click', () => {
- const nextTab = btn.dataset.tabId;
- setVerifying(false);
- if (activeTab === 'ai' && nextTab !== 'ai') {
- root?.querySelector('#createDiscussionBtn')?.remove();
- root?.querySelector('#createDiscussionContainer')?.remove();
- lastFactCheck = null;
- lastClaimText = '';
- lastAnalysisResult = null;
- currentSourceIndex = 0;
- setScore(null);
- ensureCreateDiscussionButton();
- }
- activeTab = nextTab;
- updateActiveButtons();
- renderTabContent();
- });
- });
-
- updateActiveButtons();
- renderTabContent();
- if (pendingDiscussionId && panelEl) {
- await renderDiscussionDetailView(panelEl, pendingDiscussionId);
- const postsScroll = panelEl.querySelector('#postsScroll');
- if (postsScroll) postsScroll.scrollTop = 0;
- pendingDiscussionId = null;
- }
-
- const logoutBtn = root.querySelector('#logoutBtn');
- logoutBtn?.addEventListener('click', async () => {
- authError = null;
- currentUserId = null;
- if (typeof VeracityAuth !== 'undefined') await VeracityAuth.logout();
- await syncAuthUI();
- });
- };
-
- /**
- * Handle SELECTION_TO_VERIFY from background.js.
- *
- * Payload comes from the context-menu click and includes raw selected text plus
- * page metadata. This helper:
- * - Ensures the user is authenticated
- * - Forces the AI tab to be active
- * - Prefills the claim textarea with the selected text
- * - Delegates to startVerification(text) so lifecycle matches a manual click
- */
- const handleSelectionToVerify = async (payload) => {
- const text = (payload?.text || '').trim();
- if (!text) return;
- const authed = typeof VeracityAuth !== 'undefined' && await VeracityAuth.isAuthenticated();
- if (!authed) {
- await syncAuthUI();
- return;
- }
- activeTab = 'ai';
- await syncAuthUI();
- const claimInput = root?.querySelector('#claimInput');
- if (claimInput) claimInput.value = text;
- await startVerification(text);
- };
-
- chrome.runtime.onMessage.addListener((msg) => {
- if (msg?.type === 'SELECTION_TO_VERIFY') {
- handleSelectionToVerify(msg);
- }
- });
-
- const loadConfig = async () => {
- const res = await fetch(chrome.runtime.getURL('config.json'));
- const cfg = await res.json();
- API_URL = cfg.API_URL || '';
- AUTH0_CLIENT_ID = cfg.AUTH0_CLIENT_ID || '';
- if (typeof VeracityAuth !== 'undefined') VeracityAuth.init({ clientId: AUTH0_CLIENT_ID });
- };
-
- const init = async () => {
- if (!root) return;
- root.innerHTML = '';
- try {
- await loadConfig();
- await syncAuthUI();
- } catch (err) {
- setInlineMessage(normalizeError(err));
- await syncAuthUI();
- }
-
- root.addEventListener('click', (event) => {
- const target = event.target;
- const btn = target?.closest?.('#createDiscussionBtn');
- if (btn) {
- openCreateDiscussionDraft();
- return;
- }
- const cancelBtn = target?.closest?.('#discussionCancel');
- if (cancelBtn) {
- const formMount = root?.querySelector('#createDiscussionFormMount');
- if (formMount) formMount.innerHTML = '';
- const ctaBtn = root?.querySelector('#createDiscussionBtn');
- if (ctaBtn) ctaBtn.classList.remove('isHidden');
- }
- });
-
- root.addEventListener('submit', async (event) => {
- const form = event.target;
- if (!form || form.id !== 'createDiscussionForm') return;
- event.preventDefault();
- if (!lastFactCheck) return;
- const titleInput = form.querySelector('#discussionTitle');
- const bodyInput = form.querySelector('#discussionBody');
- const errorEl = form.querySelector('#discussionError');
- const title = (titleInput?.value || '').trim();
- const body = (bodyInput?.value || '').trim();
- if (!title || !body) {
- if (errorEl) errorEl.textContent = 'Please enter a title and body.';
- return;
- }
- if (errorEl) errorEl.textContent = '';
- if (!API_URL) {
- if (errorEl) errorEl.textContent = 'Something went wrong. Please try again.';
- return;
- }
- try {
- const token = await ensureAccessToken();
- const res = await sendBackgroundMessage({
- type: 'CREATE_DISCUSSION',
- apiUrl: API_URL,
- accessToken: token,
- title,
- description: body,
- analysis_id: lastFactCheck.analysis_id ?? undefined,
- });
- if (!res.success || !res.discussion) {
- if (errorEl) errorEl.textContent = 'Something went wrong. Please try again.';
- return;
- }
- const discussion = res.discussion;
- const newId = String(discussion.id ?? discussion.discussion_id ?? '');
- if (newId) {
- const mapped = mapApiDiscussion(discussion);
- if (!mapped.description && body) mapped.description = body;
- const idx = discussionStore.discussions.findIndex((d) => String(d.id) === String(mapped.id));
- if (idx >= 0) discussionStore.discussions[idx] = mapped;
- else discussionStore.discussions.unshift(mapped);
- discussionStore.postsByDiscussionId[newId] = [];
- }
- const formMount = root?.querySelector('#createDiscussionFormMount');
- if (formMount) formMount.innerHTML = '';
- const ctaBtn = root?.querySelector('#createDiscussionBtn');
- if (ctaBtn) ctaBtn.classList.remove('isHidden');
- activeTab = 'discussion';
- pendingDiscussionId = newId;
- await syncAuthUI();
- } catch (_) {
- if (errorEl) errorEl.textContent = 'Something went wrong. Please try again.';
- }
- });
-
- window.addEventListener('focus', () => { syncAuthUI(); });
- document.addEventListener('visibilitychange', () => {
- if (document.visibilityState === 'visible') {
- syncAuthUI();
- }
- });
- };
-
- init();
- } catch (err) {
- if (root) {
- const message = (err && err.message) ? err.message : String(err || 'Unknown error');
- const safe = message.replace(/&/g, '&').replace(/