From 89609612eb8db619719b4eb71480299de7a8318a Mon Sep 17 00:00:00 2001 From: tpkeeper Date: Fri, 31 Oct 2025 22:24:10 +0800 Subject: [PATCH 1/3] optimize(mcp/client): correct receiver name --- mcp/client.go | 75 +++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/mcp/client.go b/mcp/client.go index 12973753..eefddbe2 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -32,61 +32,60 @@ type Client struct { func New() *Client { // 默认配置 - var defaultClient = Client{ + return &Client{ Provider: ProviderDeepSeek, BaseURL: "https://api.deepseek.com/v1", Model: "deepseek-chat", Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 } - return &defaultClient } // SetDeepSeekAPIKey 设置DeepSeek API密钥 -func (cfg *Client) SetDeepSeekAPIKey(apiKey string) { - cfg.Provider = ProviderDeepSeek - cfg.APIKey = apiKey - cfg.BaseURL = "https://api.deepseek.com/v1" - cfg.Model = "deepseek-chat" +func (client *Client) SetDeepSeekAPIKey(apiKey string) { + client.Provider = ProviderDeepSeek + client.APIKey = apiKey + client.BaseURL = "https://api.deepseek.com/v1" + client.Model = "deepseek-chat" } // SetQwenAPIKey 设置阿里云Qwen API密钥 -func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) { - cfg.Provider = ProviderQwen - cfg.APIKey = apiKey - cfg.SecretKey = secretKey - cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max +func (client *Client) SetQwenAPIKey(apiKey, secretKey string) { + client.Provider = ProviderQwen + client.APIKey = apiKey + client.SecretKey = secretKey + client.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + client.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } // SetCustomAPI 设置自定义OpenAI兼容API -func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) { - cfg.Provider = ProviderCustom - cfg.APIKey = apiKey +func (client *Client) SetCustomAPI(apiURL, apiKey, modelName string) { + client.Provider = ProviderCustom + client.APIKey = apiKey // 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions) if strings.HasSuffix(apiURL, "#") { - cfg.BaseURL = strings.TrimSuffix(apiURL, "#") - cfg.UseFullURL = true + client.BaseURL = strings.TrimSuffix(apiURL, "#") + client.UseFullURL = true } else { - cfg.BaseURL = apiURL - cfg.UseFullURL = false + client.BaseURL = apiURL + client.UseFullURL = false } - cfg.Model = modelName - cfg.Timeout = 120 * time.Second + client.Model = modelName + client.Timeout = 120 * time.Second } // SetClient 设置完整的AI配置(高级用户) -func (cfg *Client) SetClient(Client Client) { +func (client *Client) SetClient(Client Client) { if Client.Timeout == 0 { Client.Timeout = 30 * time.Second } - cfg = &Client + client = &Client } // CallWithMessages 使用 system + user prompt 调用AI API(推荐) -func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { - if cfg.APIKey == "" { +func (client *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { + if client.APIKey == "" { return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") } @@ -99,7 +98,7 @@ func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, er fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) } - result, err := cfg.callOnce(systemPrompt, userPrompt) + result, err := client.callOnce(systemPrompt, userPrompt) if err == nil { if attempt > 1 { fmt.Printf("✓ AI API重试成功\n") @@ -125,7 +124,7 @@ func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, er } // callOnce 单次调用AI API(内部使用) -func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { +func (client *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建 messages 数组 messages := []map[string]string{} @@ -145,7 +144,7 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建请求体 requestBody := map[string]interface{}{ - "model": cfg.Model, + "model": client.Model, "messages": messages, "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "max_tokens": 2000, @@ -161,12 +160,12 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 创建HTTP请求 var url string - if cfg.UseFullURL { + if client.UseFullURL { // 使用完整URL,不添加/chat/completions - url = cfg.BaseURL + url = client.BaseURL } else { // 默认行为:添加/chat/completions - url = fmt.Sprintf("%s/chat/completions", cfg.BaseURL) + url = fmt.Sprintf("%s/chat/completions", client.BaseURL) } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { @@ -176,20 +175,20 @@ func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { req.Header.Set("Content-Type", "application/json") // 根据不同的Provider设置认证方式 - switch cfg.Provider { + switch client.Provider { case ProviderDeepSeek: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey)) case ProviderQwen: // 阿里云Qwen使用API-Key认证 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey)) // 注意:如果使用的不是兼容模式,可能需要不同的认证方式 default: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.APIKey)) } // 发送请求 - client := &http.Client{Timeout: cfg.Timeout} - resp, err := client.Do(req) + httpClient := &http.Client{Timeout: client.Timeout} + resp, err := httpClient.Do(req) if err != nil { return "", fmt.Errorf("发送请求失败: %w", err) } From 899add597aba23cde18e22a16a174e26479b046a Mon Sep 17 00:00:00 2001 From: Ember <197652334@qq.com> Date: Fri, 31 Oct 2025 23:26:37 +0800 Subject: [PATCH 2/3] feat: enhance UI with Lucide icons across various components - add logo - Replaced emoji placeholders with Lucide icons in AITradersPage, CompetitionPage, EquityChart, Header, LoginPage, and RegisterPage for improved visual consistency. - Updated button styles to include icons for actions like adding models and exchanges. - Enhanced error and empty state displays with relevant icons to provide better user feedback. --- web/package-lock.json | 10 + web/package.json | 17 +- web/public/images/logo.png | Bin 0 -> 40097 bytes web/src/App.tsx | 63 +++-- web/src/components/AILearning.tsx | 47 ++-- web/src/components/AITradersPage.tsx | 95 ++++---- web/src/components/ComparisonChart.tsx | 5 +- web/src/components/CompetitionPage.tsx | 21 +- web/src/components/EquityChart.tsx | 325 +++++++++++++++++-------- web/src/components/Header.tsx | 5 +- web/src/components/LoginPage.tsx | 9 +- web/src/components/RegisterPage.tsx | 13 +- 12 files changed, 393 insertions(+), 217 deletions(-) create mode 100644 web/public/images/logo.png diff --git a/web/package-lock.json b/web/package-lock.json index c7990faa..32e3c01b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "clsx": "^2.1.1", "date-fns": "^4.1.0", + "lucide-react": "^0.552.0", "react": "^18.3.1", "react-dom": "^18.3.1", "recharts": "^2.15.2", @@ -2156,6 +2157,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.552.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.552.0.tgz", + "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/web/package.json b/web/package.json index a126d761..dfe57495 100644 --- a/web/package.json +++ b/web/package.json @@ -8,22 +8,23 @@ "preview": "vite preview" }, "dependencies": { + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "lucide-react": "^0.552.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "zustand": "^5.0.2", - "swr": "^2.2.5", "recharts": "^2.15.2", - "date-fns": "^4.1.0", - "clsx": "^2.1.1" + "swr": "^2.2.5", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "tailwindcss": "^3.4.17", + "autoprefixer": "^10.4.20", "postcss": "^8.4.49", - "autoprefixer": "^10.4.20" + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "vite": "^6.0.7" } } diff --git a/web/public/images/logo.png b/web/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..28ec8c71af506754d59e74cacc84eceda8b051f4 GIT binary patch literal 40097 zcmY&=1yEc~)aBsrE@;;H}uSOVyR1^@g6WxX)_^99URRZ0}_Yl86j^A~Y* zEg1_11pw{mdsqNigf##XB=ULTe_j9pL;)B8;`0>@^jQJ;|Gf*APyq4&-h%`QSRBUy z0AYZPxX3q8u+w!DbYi{sf`ljnigjo>gphG_PZlm=xMilul0XWy<+Qj8_*m! zKl+okfI(Qj&>GSteZgs3bKqLc(hVl_2%%O%IyDy59Nsatb(M9jbY+f_OdYuATVfyY z?yowYYouQt`r28t_S1+W z4{5BsUdq(y^aS}Qbe$y%1Az{c?Z{+E=*{=&uz}yt9F1{>|GNRGUUjYQl@5XY)ef0< z(VOL@Q^QHSyD}<-_-R5?#z^#SlE2aGO0T|}!eO|YGEDeXpGonFaI6RcDFiYYl2PXqA*0StVJAojn;Z#XAyWbe zANtchy+4rH@x8Q4OSS)Uf;6Mgn;Ocyus8B;yRYbF161B?b>1W{7VVQK)msf0!}9+F z)57S%e2MqzD=JhMJcEkHG$k1e<<$XN1c3^-^KorpjN%|Tv|Y-Dw5fdoq+!HpDT$#Xzy)YAqT zt!tuW#{Lv!rV0A{V$4AI`meJ8t~x7U;yB{1kdk<~_;67_4`JPq;Iv3gkQRtBfxUAG zsgv%^qotKjdN*YW(wYf!O5Ow^HW(CHF;Ha1{uNIC7g-rlWc#r3L74TzKh5ih&xz%W zfV+|dRpVcB1Z&l<)|x^WtxKcz48iG*x83H66InrqI#$ zyZ>J;U;#diKpGSYdd*MBX>^mq44)-r6ow8fSd9h$z+O_@8gDxNmZ~AJp*|}#F3>*m z9XoZh=)pt6^kMY|3c*$a9oY<2OleG?C`Qw*3vfXgzkY^QC(#IE83cbHR9V;N_|%}v zs+Rt=qd~qT%n7pd^qHmx-%RKzkRx&$1id zX?1d`>M)E(wo-Xw8u=)?#n}&-zY7U#uz`bl1}TtB{#5Yu2P-N(NI}DAp#P`cKEncP zOwO*VxbCs@{7M7k8g7#!2UVIIgt-|S9agQ!)+rf08U%;()5^9CEa(O& zs3q+k)Q#9by(R%@ctV44{E7wDLbfgzE@(7tAXeTd0+JwGvLMum|CQdlm+pcwP(b`F zH4JdLGLTwLP^NyJx5MT9*8`vG7D9!4tU<}22lYUek0ZDxNDu?m1EdK~pnh`t4(fq_ z8`)jjAi+>j55SV={MYOHaC$G?)oSO&-_%prLy!G}4IRwxfRUd*GkmR4=K6%WrApy+R~nj58#ncIOFnJ3V}DIj zL=q&I@;SXQsyr5+fZ+CnYKFxtYy|`+o%++ubn4fs@1XdZfcm3<@B6OprXaD#@p*;+ z-`#(g53Nx(IVSm}7j!A7O>Dm$?XB~v)MaQCMp`JRnXuhWjhTP&Rs7(F|8+wKQ(Rrd z>sER|YkhjTtNnFXn^$zdG;fW!E9@wL7ALV zYZXk4lyo0i9Mlq9-l7wYDyUm0-brmioQx@8Wl`!?ygbRMH3yU6x%Z%bs(R52IB$?( zFeGaKHe@7(1OxVWlJMQ5qLBH zrP$F`!0{2ccKWWWMobXt8!D2zDiB$u3!^1PKFy7X98p?s)j=bKL!*sp1rW@dMGfPE zB#e~=!0g9vq~(7>gob8u6Awog#uG|7e&2FxnbL7Kn}ZES#4O;fy_Sbx1S(5|LGtW- zP$sIyXFi8$&7?u*oE!q?q-RR#Z0Q;5rlNY;CLl;rP6XsC;X0 z7^k=Qm~~J32~g6;me5x35qnroOW6KVB5+9Nf@i{5oI)TD%RUTU(H;@FBFc5zl_zPKR(^p>gOr z^6M-*y3cAxpu4i#)~)zjqKdK@LwR-b4OUg0H3d~|CMSOgMSU0QG4(QE8FCf&2G0v| zwpm`JPK7vTCE%*9^X;m<^NUn#u`e;Os#4>dJFop^S$TNtes{Esqh)iM^$tngLr_gD zpRI0f$pf*J?Sqbr1}1mcqaQ@ok?^%E43)Him1$XqfSZNx{J>IAr%c?@cLB(*%8Q)8 z{!{9IQVyqOhsUx@!m!VwkWWBq$E8bzm{8E>CvQd4MU{mJRiO9`Lnvd^VO+mlUN%`6x4 z?>Idj8iDI#?Va)@nCQ^5YJbXpaU_d8d(C}!LU5xm8sE6Lz=UV*xiX4hxBh4$R%eRXsUe<_-|lv; zQZ!(!Ir{p$2x3H1kPY`<-i#eH9Q(CT95iN>Qc89oqR;Pf$$84r<>GpEt+kFhn=t`Z z*pMUEKV(qxZg!3I=F-dF*-0zBrriiAs6+;Z6A(#esvB>8%Vt>|j?+mqw9U3!l?{O_ z4oy5OdboA$mdYhzM|#O4W8h~UxV|B<&(?+2Db}W*D#cj)=MDO3SRpo-z8)`c>G{5V zn=d6_ki4B!Q$yJC#nEaVc32%#yEN$*Ub;yg9^1>ghbmRn5l#3FtyNR{y?q2|uYLcx z0^d|}zWb;zFZ=#Xts(PAEdR~ok%6Y;Ov$;88j-Fa+(h7Icpxa~isj-EP2dg2AVFo= z;rn!ZJiETGACh7E$BoFyIMEr-bEmnn(f#3Ee*;5=kWjl~&j|OKkB{%7?PkKGYLT%^!GfP)H>J^>dve9T^9b4@{^~kQn@cb4fa?$(5 z#w#-U+q46w~pfOJqjr%@-jyb15jR}1lhX2R2YtSTln;MHB- zE5))#kxLOpQp@JzpdYA?RPW@E= zdurzG?alC|!BFf_01YDeYPYKMUA2DdkW>^y&ava^p!Rj7mf!PI_@d`fe%!a$)nY4( z6p+~Y;M4g)6>v?phorh~mSrrv6EXtv6Psc4`tPRY)+*~P*K@79q|c;<>Q_j%K8OnN*Bd6%gXt!=lP zJ~U9vgufYkn0{F%LX6056#ZB53wcf9l5;-e>BJuc`AO1jUbe{T?(zt%u%y9P@;rywHbo?Pzb)k zH@I;jw^Cyn?@3UiXG-Zok8GLwGWN3HeSd?}sdDOviR3ZkRv<$J{JI)5<;q+h_Q5={ z5Kgv3GP9BEgL%D6%7q=7{1dz;(ugi>*94IM2Wtw6Rd)DH(94z3KH=wz_>Wv2b6dq9#Z=a7T3s&T2;tLc5HnBFwKzr% zl2QWrwb5(oOM{2FDD%t{QNo$>d=Im0?KWaLi7J&2GAr~FLMv@5Xb8xy%E)tG=f8^!5hUhj{aC-gRP za9_247fXMRIEofXAxzF4jq6zt+_NqIdfKABgLKC*;ZqK7G@m4*;uRT)tM(lMp)16+ zOyh@?eIB77k2ByK8}FZny;=id$#|C#B^2R>27$djfeb|;WN!yDUe?R(k1VdOIR`3b zhz<5|GCJ5@Sr$loWzK+UD<`K*U&jlXY+-l8GZ}*`AnYnI@N&SCXDDs4(=}Ja79*V} zx#gV^VD6TLPK86NBaA(%9ZI$ie!@W`euT_`g^`Bh?jd+C_>M`67FrsG6*z5xg5OGF zb~fokA7V%)lp5Que`5!zeufTKF0Crv9xG9{e#7M?inH+F@Ef$2;J{BR7u#^#P&{nl z4d7YxzPE!Vl({MWqnEWzBMNk(&6v07&D4H6z#jPuu3^Tp4AJFg59H!rvNyi#BjcIU zZl)jVw%I8|w(6zoZ-K>z#y&a8<@4am6dQm#Y02St{*~_Xji(~Sj$&5fAup{JkjTBb zZu;i7a$NetH(B(CqhgD3h#M&JVaiRhVjvJp6;zNDv@za}qDF=gs2&Yz8bzoSyR1?A zL2W;aZXFya6#7YnoVR1jk}!5fvY4j? zvDsd{V+CoLnXC8IS%ys%PU!~^fizTs(G`iNF(jRxq|dNbXV|Drr+C{1gtZlf!!`w^ z4QvD5tVUY8OqnZH%S~Ya7)8*@R50VY7ykC~OisejkTEbCQ(jnk8s2DE1}pgyjkLD4 z&GU;3cdijg`n~U@EZ4UeOLJ#GmGHE+40Jv`S3!%`+nL)+4u1>9O}DyIDh{vhC08|R zH5E0_(QI$xRk!P+#Nf#3?G!VwkPr(9?`>KNg~kfIu#N-kB_Jarg|$^~UTNV){}OXr zf#iS_c9z0@)?KaK#*L5^Nv7TkN#d)#Ia<66zZ!#tvP5y)ybPSxkb*|1HTVl-%T#lBen^TWKiYk9OCnmbf=XG^E0~~kX2~;keF*lj&zn*J2`;ZYM0=s!>{C^pz;+&N3C7EO89lWC4OOwyzy)|8{T{<#x3N`$VMbW% z{R~qtt|*{4TtSQJUtpDKs(-td$R-J`4*MZCmb&u=UR0{Sx@rFJ%h5#f;B-4z@j`p~ zzaR_ive&uQ&AZdL9FKtB>Z9+#-jNVr5ALPIBGD$=7F1oU);MQ( z!SZu7k>*DdwkHH9y0zcE9~RF$(9!mW7ucK;a02!IT`AM$%x(HK$tu19LoznRXAxh1PQKL6`Sl){sdrL{XEC|X(i&aP+;xdK=h$4&)9G}0KSkI z;ZpIHFAsMh#V`UE7fjbkxpuLhCeL{uQv2S8Irtl(axAmoA8s%Hf?7`?8!`(;vgTsK znxpVZYp|vo=@L`S00Fx_w$(A&sF!*Wx1Qc_UX7r(uWc!K%6lyLJBkgzZ3m*d15(XAvQ;)1`&#a!SPp6U(J2aG zc8ijn5N0=KJVuj@8{FSQu)5uaGKk(F4c13{K-ncCg__)lMBDlTxA|pPLZkv z$)!n6^ijX=eOwb8`n~d^L0^g@TBS8Uc9BrocQ9{bKfwXvgKKoMJ&5fBuE%|E?PB#W zio30vJUbSj9(x_vylCa_kNyEj_;`NY)!`Y zik30VL%9}A=0EEqN%yz6`<*K`eY`*XOx3$rkJ5?SutL)sUuV4KB)q`Dxi>b>VNI4U z@PXO4|E>`KXpyFeZ?rD&oM-Z_lnn!G8WlZ^tyb|{M@L!AIm*W+O2C6h0M4EX=2%M5 z_oQ~444z+J;39fAjN>Jyk=6>Quuy=Lfam;=!6Gvrg)~+&*Wq=zh(?FwSUOXi1=-b; z_B+0QSIe*JWIKXkc)ion@f*wQ1Z{T$AAas0xkgn#KZVRt5Ja~(y@13QziKJrTb9MA7I?i0Zzr9$|PZW ze=hwrK8yn5AKjjEBWaCba9+l@ntCNNQT0fmGbje+!I~P+g;rLdn(=Q%zSut|X`ZoG zK+m6(1YEy|XtolZEv#=Rv=cr)-b`E$ODS!BEWY7vI`oSfJhJFuTOlp~6*<9hR?C>s$}nz0@B%7Z2%xXR8&!8V?&ERM z9!x`d;g@s`U)n!0!W@E&FQVnrygq^nIFVg*7wBQkRMm&eOY4Ias~3Sos*Fkn$OGEj z#qt)>#^Bo_Czj9nc~e#?;(#JdabHNmy0+k*(?(M&lK~Df@BkbIo!o_$kd0g=$4-7~ z=w63-s7Y;}e<)JE8F>k}E*JeTwhzR0A@aA^?Pn$`rBJ!J`KP{Iz|bfXRAnw+%86SbpxEN4kFeeIS(=awQvq-%Bts&{^MYs9Ya@uu`q z{HvdS@OkWXtC(3S@5ZB%_}lwv@QqnW<>@((H!$I0omChMFJW{eYJy;qU&T{K@mmJ> zomsYIPQyNR@u1hvez@f!U4L;P0;?skB5uh&xLIRnx%p$o@|kaykJ!Jty|s1UU>j{N zq2ITfnfQy{&2Ld7$CurQ`#ir4a+SD$r~wVDKz!!cH^lG9r!QvEfVfV2d;)_ zh4eM&8a}(JH4(MLNiz@SVd8z!3wjE8SwxC&xQ=3!fQ9`{bGNaoOdnoHSRIZ*Q6dr7 zJuw>>_EvjeNQ}qi^&KZx%T^4@<)&QjyZu0@^rJ;qHz{atr@CLQjjLUWKe7!5k5M{_ zCc^g9C}+7l0zSHR9rubokOjA+L)B}?zwXx!;^cz~a^j4C?pAsnCfnaTUYPryW(p9u z+(yUC1E2)2zl*v@tF)DQll@+n$GcJ(zT@-QXnR<(EIQW^#}tc5uf!4_NLyz|Yw{-G zPMo0r4M)3p!VXwSZ8p3=HGtF57=t`XvqO%gLLiF{nk9v|&SY~F3{nO!)E)*Tj%Wnr z7;l7aldVQbW_iJ>JG)@Gl7z@LgUFlGt_hpe!qz~^UZsw!BFBD>Rtg>#mCd%Tn>2?M%N~G-L&Cg{yY*iMkMsYIg zujbdOP_mWTxL2DOb^b(KNSlFs#H^qFk~xf^$D&e{fIj5;p92$zGYj7Y!%#I^fQche~YK?DmrPXG-(Qe_vb81=1!lBIg# zs-~=2=y5N3Ot(o>m-XoSpA|ewtDauC-&Lhe4KMK}#4r-h;lC;O|UVqU` z<{xv$OClk-1hvf2c10pAr)DQhBJG+xgX)LL4;y}0r`B0!w$)g^j~QO8=M!uz!7Oho z`CGU2#se^?R)cXlUy=)>ZW!%<53t9ts@sZL-3_!*byKJd+m@rDnTkED z9&{dR&KCxWeJzLhGthW2)+t>>(jC5CG)m+su-(9pKXTp6R$7Z1pN#tj3T;D`r7vOFLibmvS zEv}}if=y5tPDs>y+67n`bk~5%Q4r-(l@gTq0} zriA4{*hWIe3!A^n;mY0;AVxbLR%BMsP7Q{S3wRtT=0gczoopgPDT1j*%JT&Zj5#aU zu>H{OT(qYOd4sTc2AQVi2^M)P+gw8q7`Ozqt>} zv8W~81Q8j+nHkA!vn>_0D40dzp2b*8r<^dei%A#zU1b;9`1XqPIGP@N9T z>dYA|rs?=z$#H2Q*oT`3wdN{6CLDk`3H-x)%763mjx)dG2P2y{0?dw3Y{Z~7rl|QE zcyapHXj)k8!1K27CKNu5v1W&_na3#NFja$sw(fgals5Q))dw`*x}sRzzt6dow(@m_ zp?L_B7hq1GCa9bEx3$}e;(i?&r99}^EiK~Rjw*^;iD80Gng6~!V%xkqy6cWE3rSas zem^ho)Q`Exb4hcbU68D*pVw|p|Fs#|+g-BkVU&hX>z`I#3HLTrFj9T!bX za&Q_Djd*k!>*yQ8o1!KS=M|TpvEg<2CF3_25zx33UU~zU?Mcl6k5E3hCU!F5z22)s zNm{fiIsmOIx>og1awH0etsm0n%Rf6eUfbGB-h^D!B9r(c>h2A*x@C(RlFeVZU#!k$ znC#GwE?tMPbygxs=lNz=@oJq$EI%Vn1JzJzL(OUPTKn-_8}O7KFx)H4s@@bct}tcq zv|ZD*@qAk*6zf$-t1%&--VoIJI@;OR)C5zNK}#*v3)UG3NXZc`$2Tg>?>e8DKu#a~ z$~Km?(V}7b??W$E_^099yy4qinc&^)ldBji03qsfw=in9*H`te@8cQcyrZ{N}zOGF)PQctt!SO^hM4FZ)-Qd*jV+YT>=)0ahVUkBPJa9{)Nj z3qW(0v-Knwe(5UXwjaxc2Sg?gyOJI!5gbRUZ4h`{uQ_S8r#K$>5V+Fiwb%>o?0AB^ z8%RHlg}poY{a)4iT-EvU(kT^7dZH``l)ymORIvtbS^6JX`fua-8(VaKBX}Uo?pL&i zq8WXu5@bt`Ypul2%TviP)Lz(J?6~JJifmlKNge(?*powyAhSOkq8UeGMTHG7NlgIT z<)0iMYjX}d#q~~*2n2i;CBb9Wapk>p8wyybFx+8NCJ|beL-#x1Y;WJ_&ZAv)G96B^ z`~8}^MFWVKQhl&dc{!u-bqDs+3GGO9*FCSIF*+H1d)j0H^K$no(6Y$7&oDNf=24-` z!^jUB`bm|X^!qpFKo+EB+<6k1Eu~UZ4eEE%Z4Zy9evO@hZlcjo;LaTKf;qWj}tfhTXL#W?Uxv*PKlKjLdMqsHxNMd-wO8mt6JNGzj)q zgfcRjSCbr=5KEf)RjZHj{o@WTR1hPvn~LU|!|b!_?>Gj>l|4$|#W;-T4Aarqt}X0i z<$5-YLHwF;MCslZSr^*E`$NU#rR~BQQe0KLOiz?)JP-!*%FT~zuO3FIrfsf=6HMqJ%c_U1tt>LL#5*G#awFF6YCG0kGAf#L)@1$wXe7MubU;6^F}$=E5{# zHF8Lx=6Y}DO`i7JXQMOB6?KFR(i6JE5V~5P^k0y)?`X3YJVZ*?Fv5_^0v6Lubl3Xd zRpmXh=gy`~rLQB?*5^`_p4h=c)NPII5M81?I%_01maw=+XJzfFj6s*AWn3pEa{TlN zVcBd&)vx@`qoE%*6aJTj5ix+^GbZz>1LkH_8^M_lU6D=n)W1ayUGVHC&SsZv4ek?G z==oXVS)gbJtZHSuX_6v-93OnkV=RIE6C8Qfm69M47l_k0X=lvA7u+4?bo(z*q52OE zpQ1;TUtM8pn`%RUiTMoq1~EEZ6h}A$k*2_v4oSArSf)uqCS(8U^~4yuUNIeT%6WmcE+iW6UgHqlenT$qk!;eGsFbA0a+qGLQrYdTWfwKJ;VlL+)N_Nf4 z17dwoB{g?d?#O)WXCvtsx}d*-`@|`>4{OAH8@EK~q{#mZbA`@$*5!~R_HGR0U_&eI)#$c5Sw^FcGT%6< zWt=Ug5SuNLA@{-AqfUfoSk2As)8DKuOHPr~rK__%1g)D)M9sM5F1sJZizfFRvj($V zVyNF%O6AxuA8MCz8`ulF`wCc}ysYN!zK7h5w%|xfKwcMcbVaQ$#tYqE|V0K%lOu)#De8HDhWl;n)N~vMTbw- z&XmhKmOLT0kd>c2-nE%#mUUdw@CF{QK!B_1L5|x*mzR|b7>F6?OPbxo z9&dRBvB_f;x*!svG=2Ftz4`IB`4J-Y2=RJufc01W3pyTZEN?uKfJq2#l`v1(oRHWB zSLu)R`B=PZ2I3*Tt`;Dl{g+H{S2o2>M}1c)wWI?!XK(*%tPN*icw12aq0omH;b8{9 z{4#aUeHg*Q>uTp4!K0`GzcOKk%-@SF>n~-J3phF+8*=6YbDw*mcKd{}aY6!q(3Hjl zaZ^-EWf*VXHk?JDZPGAnHkKAj^5bfzLBdp_Y5$+ww%d>0mIGgdWC@-K8EL!-GwyxK z-hap3SqIK_W-9)atDCnse*?eI!gN$#fzg%YDKW>hBJB8|ANrpo7(RkiCUr}xg>7}^ zc{09DT)a+zwk{J2!Q-fe{_0|-2RS5k-KBgkr1v+uUWWIjaC4qJE?zo_-?xdOd!+Ao zkK^yNC(+zqiiMuZHmU4gjnP#z zth)b#v%x^>Jw{3pZtk>f{Q(A?plsUxd0KpZAc^PUf0UkdDVZg}+nJVpOZPlMBmlEiV`TXGgr)7T| zR<46CXiWjcm8RH~>BVOrJqVY72xLx(E*43>I|Pe%$x@Ew8A#;4&vPEOgK`l8=Mn|~ z+zB3V#~Xwjz)Z#W&(Gy8I`EZai92zbXrS~}?jQTgn!&%&Q_maGb!y^>7bI?!HV#QD z(Xm{-Ps2`|lT>aYwK7rK@vLgv75_z18gt?#vr1zbUv=E_fFo1W0o9R{#;oE1igmhg zgbJQY(HJ6_obG`vtK2{n5=!J4+T&6Lu*;w{*dCf2ZrLwk@E#z?o20Vzepuv$piJKN!2J9Lh|Gf3| z`{7BWU^&4?t#rmOF1q?Tg8;NN_ax`+md5t^Z_=wc^5k|X1Lr8<{P)+PyL37tss5Pn zxD^Qjhp8A7^UIx972|Ohf6QEt`yKRA7WQx?bq>pkIgsDk$J@WD{Fl)Ox()SL$;3ZjN-jFsHRjX4eY6+zuM>H=*mQCsf#knc^Fdep&dBro)xeq+1vld^j_Vb!{ ztHxX5Z}fs$Oyf~$WwY!iZI|e&UPm}zzb1@-?JO9OUbY@w3MnW%W>em(R0kHf28H$S zB$WZR|9w=!}6*7N!1NN@Jq+L^)QG2-@698#aBtEH6dddv<3ev-4$ z@M%P|`O4P_{MPu2Eh~IO)npjs(7oef!$;^bL}=)s$cmM3CQz>{&0b-*N`Da13hEU9 z8!4o&-w$d;k_3p5I#V`NvY&**OX`rKqTWLZP|yjYJ%DnUtP|>-uKILg06kOG=y1U| zEp?CcHIWKsHf*dS)AF2USn9&#Ld)}1O#`r+a$zy}zyFXJ4G)9aB08fE&vD_HeY?V@ z6Azcj&s8e4rSr$tvUKT~zLqNtU9QN9XUycLWRnNZMqF;C!!s33js`yo!M_?(`6g(jLi7iM ze?J`tydMf9nq_~kPdmopNrSg7y!P5q!<3(F{s$~hHhf`@JaiB#>lT^?&K8tqIMh*1 z1*24DX8MwS#?3$aQSJV5~{ z+7RuWEbZ=!#GN^A7Mt~yo(%3U&MSf@$OqOO}!h;IA} zIvi-&*=T|lrdIvxiabfe+q0LBsb@=OlP~?kbWDtfNF#u+WdMo`fy;5Vd{NI33=E^> zx_zl)u#bg>7B_bMAMM@3-6oX3j-R8`^^Ud9OFeOjSB4pokA1>~R*s6mPOL7P_G1R0 zFBNlqrj9t-r-W1B?GG_$?Sg5;EKd?OocKh}GSp1y{coiLQZz0Bav5yoim@!RRtRj+ zNC4E@v(LY0@7>`>CWY@Tx9Oh4kgO~%`Og*k-mi}=W2SpqxJbe^*KQX=uf0Oz3NI9a zm`#C&ERY1pZg0wUdZ33De(_6;3!Mno_X#Rf-9m;v+=%po^XW+4+D1Ax`dcgVny+$g zcwjzIs@;1w+RTy_sm(CG z%`o|0Joq!Uk!m?&sR}9u0^HJQG$wd(S*C<5GnnTAT9&O$(T(td4ds2Pi;nTeajiMxiH!DV7)v_xYSl6k;k0#|e$ z+s*f-p_K#0;^y)XJ~9+1ovaUHjjS#wr0qOZ1f zKgskBLd&4d(4=AX~FDb_2e^KiUd zs$JA@D?=beOBhjlC(1~OO6H3;9iZ#^H&I{(Xnc!vW2jCi6XV# zaa~6Cl2CED;esGCqd|%g{t)0XqZNm~XE-_&j1cxAOGBgaXO zg&W(6k4;%2vwqod_R|)CCMgVD4^gK(1ZmtdM#%{tTABz*olgU)*%_18u;Q;Hp8^QO z;$jSSolJ@Qq|lCIjnB6QCi(P-h@ntm?{`ro!NAfwd4~sS(G{Bx)=#q_$5M<6`9071 zo-rfJ8<{z$q6d1C3chtIEk`{MgmbWm7I}a>!;J#B8r0%%>Awnm$v5us3eEvH$|WhN za$i2lmI7lXcZ=^sj4r033bQooScW$>u~TA(W3`d_x%t6e5mjPxQTeq;##emw)eSC` zdIuJVm4iCPQXu;IWXMI@p*1`aE7MrDo8w4b{_ zb(-JABZE*;NH`fz3cUtE&kEQ-c!qZ6XH`LOC7nSKz)sD6P=A&v1EWus;fUJdr$vjp zn&pprC>~pyFE&s9PWg)$k1VKCo%|$KJmT;EF*SzfKMBbt<0tV>HFigh?w&vn|6mGe zs%9Ocn-QwM`MHQ6a`)uNL0bdzdbW9rN86s~v(+y09mB5m_-w&N^mXoXSf1|sGrpwx zZyMzX3+lGWQ7(#4(*tJ0NCigRGu5-F9@G3;k{Fw|G}f81DWEkRZ>ujIE{bWD(z}Ud$y;?ul!`| z$oa)uq8&l)w3Bqw)!xiArPO> zBQQKpNQDXUuhl?C)YQ?u z9`EfIV1^H91#X~QW{0Zgvr>I4i<1a=zj`g)nH&} zQ~vqc!G_*A7Ne>e>H)2UH1}xK_xADt#|mD5pl+bSV+Eyn)z94F^7#3!{tbIsyfYfi zJO?Fu{k@x6v2lh~+t&Q0)n}-1^`viFx-_hp__A#bCCPG=wy863Z8b9K$__ad-6B!h zx=dn5tSsjaMFPA3i?z#I{0#cHzU!!)Jcw#Ndnxm8$VHC@W1RKlOb;#j3dyXor2CX^ z$e5Swv)$9qJj|@UgU=rFIsc6AzJSFr;w7XQxv5hSm@pey{5o?_3N{Kk>z)l*-+W?@ zgXWFxMgmKV)#^G_-6koCVIG7m?kb>La7N7fu8 zg2Xas5Kr{}at|zPY=oHM%F8^dvBMxT^Et@P{$GSs+D1+}Ku9aJZ+HwyrmNh0M;-PRww?XceH(cA_7al|T;+vta@IAwxJ?BXLb$ssksaJqOgqh|DFNHS z@Ei*WQzKEzntzH!s}4%8*bXrp{NMjDp2IHHFf~MEFT8h__bhOE5yS= zM9aL2x>yrpNZ4km=YCga4(iN>&JaA(n)QTLJ_3^9yD0Ot7|I3DMmS|ZulAW6lXsIc zVGz9G!!j3``Ly(skynb6zXoCw;E2Vjyx?80ZVpY#%DMEZvd?N4!2uv9t(3_p)@3Z*(f11^BM5J@IGVJn3{oi zz!nZH5QMy0I;)-%S}(v8{P4z*NvL@z=kY&tQ0UQq|7DVOS}njD^FknlBJ$c(A=sM2 zF;haViYV_F`F>RJH5yc_@3aRcG@D8~WolHbr75Sn#AM}792Yf%k|83cWkM+Q5+;^z zsv~G(`iUt6X1Yp_T>~t&bNJ=TCJz(0!(aAoeaS?o#(`$3z!z*ms6%<#nY0$ecE+4` zlsY`ZlLh{|F$1E2jsX+w*?h1j$?gw8GYH2~^E$y+ZI1z0m}UZQcZ}lEK`}^#beOd6 zmj-#rj8lasYfhp^3vS0*0Ib9NjFG}-p60RU`9?EIJS#KP^hD28#UKONq5@!n1B>R+ zTnsp7c6n?efCxgtHeFlql)#d4HQjWk0yEgP?%Ez(TroL8Uzqz13f$sspAu^(l!Hhq z>g8|-a6vI~IpGr?(-+)uk)y06SJ6r=SWDVjwS-WLB9Nb*2BOD|3s)7OfK^ira9b7F zFU*i+aR3N@6sqdXEHzn9I(ma^bCiC#^{yvu>=XkB>;}3Ca?wjFtZvLJUQjSMcEbeM zGz`E?!xj+-FV1e_AOy#$rP-m-iiD-6KQh07YdUgA-YsTX+(`)vL*Pzk5|H}VYe!M> zspeEy>R$(c=_Y3`4X|b^43eHGpQmrrHP3&SY*%v3B05Fe!5W;+%_(7%nP%R2wjX&{yT-0DPdFDz};DWP>G;Tf2_#m!LFo}RjquGxAAliE3199abHD*-2w$|I*8?I-( zGbC^x-!}tCP4J;GGr<{$HfDOE?i@GHO@n2j-+VXSvmRY!h8Ra%Kp#Oppq< zHkK5gEvb8z;O>^HI@t0~EQSWzQx0(4=}CVb&QeJja|8ecLk?{jSB;v&prjZ@L)`6r z_}RVgluwEj%kku#;?CldU&_IZ7lSG(8Cho=S;4r3vs@AFkj7X;P`KIkCRN~$fD|Dr zH1`y!FHrZ=t*PDcKLN1n8_ZL|WYF$EdBZ79uW%^Ha5(ZTPm~-gX9Ky;`QS8Zo`%#s z4(HLCu5Z2U?(KKGJ-#KjvC$<`3T;9Y(K_eMb8|8&3GNk_aK+<`k{0ogq~HD8)H17p zGD>Q*f(aBAnaG&+A(P4_VbqucI!TC=-ppB{_;1MGv0XQ_#!EQX{MS%I$7VE%ZR@JZ zfQ&(y1Gx~19t#_}116ZS*%=S5X-U~7tL_5SYq3yLHv^l@hwrI~87|TdOmeyIP1ivy z9@Hj&)va#yp8#0(?d8oH9(U%0m>#yah6_B9O$*DaPOG|=7&J^@J=^nR)Xvtn$wM>W zq&AO!8mC#>#;dHa9S-*JE_c?qu(>cEh_*%#(N(-TSP-RF!o8l?5v#*RB+1EQlPnz_@aQO6qK#5e0Uv5t%KI+w82G>e%;F!w zArz4M8gFo`fgBy0T{_QE-~v?v!gMmOkCek_D(+aeHR2_vdr4T&76ouhn!#ysa4exo zHk`{AVu090I$B*W>ty1RQDpXDyb3Co!x>%0Id2ZgH{9| z^t!!&{+EAd#WF?~Q4S9uKJ4$aiOON}`tLJbYbt-U{IJ>LxhBnK>v`qV{aVR%xlywy zttMs5_d09Ow|6!+?;?+U@Zxj-?z6kY{k{IdR&U+S15q%dIdNXe(Zt39`tb*L)L6ks zD3~*wFqP`*5-KIdb>$pMPNk`iYnii5!B+AjyDutZy(KugSi9G)ct@s7N8;e1>Ly>J zv-$)mV8zgnWdtsz5EW!?Iq)SXC^!4d@go?b#LZ&`ht4UdD=M##u8w)kT)jh)NKnkf zSvZY7r4MuPTY)rK#^mswd_!R+O?Ba*%~8JvhsOAC!Dx>eB0#_^{QvTg|K%rt@+Uw1 z@IxX$sf>E)r{85J+WM*D1cJ;1h>7MWo=%WGvHnB_&xX%TRG3Sj#V1*t;Hu$pe``JY zfBxqGe)a7)?`~~qn;lI_kcGo2StXzg)&lD1grq!|6%?sjCN_sI%Bz~YDl^GK6*vVB zz#|6=ZUqpkj~!P=ZGuyYG}nR{Sr)vw1T267I>p8T4p@34=kC!!Z<0v74L~DS}#k15F%wT9m_DB1g&%L3SiK#c+WW*^)vi*R>8VK|$qC3a$W7 zd@>N^bPQ{{Vim^KoOs;_FEGI90(d;6MQu$5VG`bNSy~vdj)-lJu-wh7uT9jSrzc*o z+xX~1}e>|en`SoJ}g=!H%}m^==oIL z%%eZAvGjvrD>kLV+jKBwC0(b}($XC5RD+KNsi29{Xx?EFn##Av+^cs9N$ z8-scTj1%YQ&eM!tUCNzNxmOhTwU}-vs$SwX0zS&9l`Eox5U4z3 zFFkglN#oJ5ihEnzgF&mEY{YN_hZt@=)sDdvkE%617!H#529{nMREo<1!%Wxcju4Md zN<8b{sCe1xRME~JFHYlXm{h7pjH{-`B!~!_7_pOq&g74Gr(BeUs4Bu3cVy&9S;u-h z>B=2Q%CVQ_5;N-oz)n^e5N6A?8Q`ZDft+ux*Eeu$5`^JQX@2y42p;r)tQ=z^pg^2SvXyFKQl&F6G;}MRD`;_$rgmt zc85(eTFGwI|8~@7mC!TVW^t<%_rNpxhS<_?v>&mZMAeI<&N{Ka@_^~%vg&1#gv$4b zQhb3{5ms3n&=tSmdsQy^WGF-=P-Nl=J2!M6d@9=`- za~^1CIG{yR1-%!;(=5ec;-uv=WGu1~>;Sb;KNp8(wyU&wth9};9f~-zSWsgPbBZfp z7$NVdBx?z=zO)?{wot)`hRg4(K(xc4&z>dtD6r4QExzy*w&!?=7aD0 z@3QO>Yl?{H((>f4rgFlFQydvoG$`0hC`xySPrg2S^0!&_@hCo!?H9L;;h>idS%iF0 zjJjBuSYjXlOPh_M;os;xaFne?k;5Guu|u>&5OO%)kcG0fgk7nVO`X$n@wmj=QxKYS z@X3G`UdelfV4dt3n(l(SAIs9UzXgW?ShwINFEB(YpL?zD9t_T;Ic~W!c}-Q!Q;t)K z3K=UGnR3cju;2V~N7S8ABw>?iV>QqaMU%>5C&;`~Ui%cp5XWVOPanTkwR^Dp?P~ZW z8;;nA5G^J?b(nN=dMpSh&DeNmm^>)&>^ykmbDgbM${0U=3muoUpXTb`b#eSrjCO1tpp|t1tcsY}G&(4gL6UVD{$M-JG+A~7aeq13xW;F~FZJ#W zEB6+{3J#CJk29>?(LtZ)lWHXYA3qOZA4aEELLn%@84}@=Bdb#qm`Ek{({#h@^oFMZ zSU3Cz&l}!c*PFo)x+7JQubcPxPLZI5-j>OPm~YR5PtKmI0T5O{1r2@j*A)j-%Z)2mA)| zCu!EzirB~_ZgmMzt8kk+#vQmoKyN_nBzic=b>_-ooX}er2wMx(Nkkqfz{njX7Ui9| z`5XstA!LGLMN0lq(Ix2OEOdjhSTI8?btjZl&D)X!VBMBGefn`q56~2}7+lbP32r+E zK+F6n4z!FKSE(b_5-xC^rl>c@g+Yf`q&g#ja`<67rkD0~B&U?ymqA3B4dG+Q#uz1R zovvbt)13W?nFqS<-I+eB<9t|l%XnWO!YB>~)n<}o@kOZ%S~}%mfto&_y(rmrgLu6J_vaXc5C!Tz^tH=S4QHBG}t!jo}k{c8xY%=J?;$WIfRzX2&u&R2J zl9D^>wFOzLtfTzv?y3tzla%Br6rkg-qYNq8?lg{ouux)iy*MCqjc>;x0M_le&!-=y z)PaNsIi+wahEh58K$G~LXTs(F=_OyDX+MF&g$rT$VC0};a?mv2PMT^tvd&mQJ!vXs zMl*>U*TO+Qv7;u0F;EZS8}W&nKCj1y={*81WiLAC%5uP@H)ZGxDrPT@--vuCIP`v0TBX= z9C#_?0qc!51;m>r#2Hm3n+~$D2(@;ArS1(5<{r^`;Id@X)D)Ov(y4uoW!S(>UUf0` z3Uv&UD&!?5x}AZ8BUCY*aFm^GrE^288a3!t#+!aNjdRu# zzxzd-S8zC?F|n9)p+0OC#b(KTmq7tl(mL0X3nj%{ES%E}(G}VyloG3j2^Mo9j4-yN zV}rvkh{4Wd8Jh#5kOAGQ2o< zYjNDfm}{cXzzvCz0_I-q_VWT?li@(iyx5^koY9VBAZotuJt2d2&$aQ zDULMH@%{{kWk(y#NdpB0%1AXafKW1LsW`_Ln#TnWJa;bGiWwfr!jA--%|{ zpp;-B^{~HapQY?NgChxZiuNeDc%uPOO2{R9SL3t-%m4&og2L{>4YXOzQ-AhRup>rw z6a)~#E<+0hW}%UYxWEn_p(IRk1{Ac2yoYpc)##E*%0UqvQVoj{7?fvDZz*`MDCpGK zgl?|313L^U*FB(4sq*8ugmh9StZIUInOC^&T;HsbLaI(v;ncAt-mN9MTa^M}-KyJt z@?cT1S`){0ff81>6(xQm)EWR{l7#uyPBK2I)4{&MmI62g9a272TXqKs1h_d*i&Ry4 zYRGr&G?$AY7Aq0udBRS@59KtUiYUK0;3b`iDX3kW+=wWRap*|(wZ^c+8$;%h0*Mre z1mUQX%SxLVfE$SGeLN&FB=8idjri~)=c8D&ROR5EZzd`>m2A@|C3MmC!-UC*i%NKe z2aWMaFpQa9l;gBhB|Qu-F{2St30QJR19Cx_K+}HVdDFD{3y4|N6tdtq;lDpW!gL^>;y10dQxQR3;OE=1REib!Ox+Y(e ziyCsEfPUfXVs@tKoioS;1HhW}osZ8HJGiJ?i9P{e=EJ0P-XTpT^&fhxzp`#fye$Me z2ul(iVN58zH!{n2TBCJXCGRCsin+3Hu!b1uSuI3;Tc_ z&V&QP6Kpmz*kr&gx!U{fn!=`GuVE?IMtl{Fqi~0BJFbmNHmZ)=*%}CDuK{I)K~C6Y z@C*%cIzD1t_VRLVq%D|xRZRH4dWTz383ffU2i0~*{dHJ5KCMe{`kw$;H~kJTI6lFO zYa+xjP2v;}xi&Hchfs7_JTT^-%#}VPEjc4eB`slYs*Q0Y9Q7+X|YRF=D}> z)4bRl@=*)iJEMG1vHP-nyT?8=Y;Q_K`XurbjF3SlpkQBf2x*oDV1ZCtfJdYSt}eEJ z2e<$kfuWq?u6 zQ$Wq2lH9?a(`ttl&Z#5_g(0SX%$?A8X9{T+%0K8kgcYaL5v5&2u%V zfD69=P6TU(Q702fdlM>vfPrX6rX=X)54~aQS+|ZI$^=&84XjPFHgwtJc`49nm)7>~MIiz=ka{;h! z@XbBT(BvkP>Pa8mKd0eDBFN(eLMRpi<&Tri^#0qAfs$U^L513=rL%w3&?zu_>Bq|RAc|pA zwpqa}XO+%jO#KUDY=TIJfBO2DA`s**{!3Wk(bvL;PBcvqy z0P#@Y>PXV|!{|XLTE8Pr9V3e>n{Mfg+2*t{0<=ZXKZ>9JTlLXD4@d6n6{hk)fN%|U zr4E>S^gJ4V>_EeP-hw%zIf%yH&iGr85kf>) z>_-XV@-NcpNfzxh3&Xr}faXw7Eb$*uC6fq&*ksT_mxDfWCrrtC8JO~Rlg_dJDuTp{ z*1hP}FGlPCK1v=ruy$xC<^gUuEXz0f5#~gZ(m3R62TE*BSp>9=$ku2K@<|zDKLd zDTzJ&NMt4|Pm(+KoUhXByEE>iguLOlPZM%_Y@>_2$mr;7?f~(43#jlDYyL;<0zpV} zP_f1h;9x(5D(UKr@~H6LsI`G*G%O!JMX|!m=g+Ls5*|S!c7z(@wS@J^v>lG^*C4vK#R& zKR)n&B4D}kOX31V-0gc&|4H=hzpk|kzP!w*-@q0 zCSTm9Ki#x99h3U0Fu$AELZBTjYdCXx<7HI#Fv$(wjqa!q_VD~%cL{NuVXV9g*F ziW(9CtvItVR6q(rv-2`8AA^ZT#U*F`9i2&cy7%c4YY5AHNEOb3BxtU0ZU8E57;WPM z#2B0+4sh16&jJ7DB=_r2Z5-JG7>Pv23*HyoR5ga?C{yZv|4?dMhYcl-Th$Z};+l3g3j5VQ83fX06!6HYng9^OGWc~=6jFOEg+0y$y zA}|;zvQSMoxz+nW$dlKQG;&GGp`z{JE+N-x3?)t=#wXmSW7qDNxd})|#TSa@CTy2u_5_Sw(-PxLi=TIIFmt^2O2H?E<3J7z9i0o1X(0ZP_tsaOSt=}NOtf;ZLYP6 z17O-z6}yBMoQ59;!WZZVm~pK3srqr;CrFjg!xqFyU{qphN>FqvNL2`@SaqQc?A`#o z@L)`dF`){t301w`J}k+o!9UmhkRO>v1~h;l$?_UnNT-oN0IbtUX@NR?+|V$KZ&=U_ z*up{~&+qmER2Hv#JYY>h1%w%XQO6oC0$YL|mrOZjrwD_b zjypuKt0=d$nW-ax>o%&!OpB*f*dVZEil&7w<-yArEQ;1v!o?oDIN+ZqGM+Vx-Ky_4 zgRi@s_s95WQ^2f!kNI944|s#wB7Brc(TXTp+QDDV1q?gZfi1lqfP3MPEGuTSR4j=f zg&-g&vxs5PQb~DCCr`$Wuja_aSf5x88b|;k9VdYRSjS1{4A=M?4bQI40Utbg@Y6s2 z(_i}1m%ywEQ^iv@&GDx`bM%4s#d9;Kq~po@%mhWs?Snd4h2&p*?KLu>7?{ivh!_{NdfGt50M_D6o>Lni7&Mzw0JDCfi z!u`(|6akBYTyA;Mc0PMmJ3avpn0V2sKX~TQ2)h_#EyqR!H&}#(^<^dy{kazoHF#-0 z=pczwoZ$sl77V#XhI%dGY%He10IQpsnhgdkR>tZ*+0DH2d4Mw6etbTU&P2H<>oa2l^Epo%`wb-k9P2^JQ{l#0ZY+Z}=0&OuHsgMca@ zv6w~{iQ2G&hi}`k3k@Fdks*zvW7OsxnOcG~8!aFQ+$6{h`%KK%*4>y}4YSV6-%b># zY(3Bl*7(C;2enkuB5gU!UHq0Dee!T=2q7&`0s*j=Crd}k+*PK`>wjh+Nv$$=39EUati(&9}Tk(o>l<>;nq$$LU#P3;9C)Ic)BvpGk3 zy3KLMf*O!T@^K%b0e=KRntwVrpd~~Cvm`P1zsmIiA-yFeUlGYAJmoU3E$X)t{BGK^ z*@8={GBcT!H0qE}FM$A9rfGSoAE<6N`m$d5IRS0F=2$t##BKk`=~K=T5oFK6RHM%H$8=a;kX8(091vIy7i1K$<{cq6WJRCrxfJ@?u{zY6%*K}!t_fC6Q3Si&h0pG0>8mtDRX zo^>_~zQz;1jEWstaY(4HT%s$-_u;8Q6@|+9`&eHe|5;T2<90Xg@4|p}(*&^k@kn29 zsV~DRq0P>urJ9Ag$y%!{vZ5q}pv%6V*l(=i<98<~J{-(o$uM91a&P`*Vd0?HCrgD; zZkC5egH-MvooqJKc1_?6YnU(!tz?Fse5`N2TGE1~wh+Z)@;6P`rC3@bS-FNWh$V%o z$EyHhwg_-lKiFJZi;Wgo&eKzXe$*Ka6~3Wu5FJ6$X6$t@I%^~`+c*u`<0Np+0IR9_ z^L~qt8}0>Ob5DGIXiljvkcErv%9{ztbc$xOCnv7;Kp+2j&6?h(x4L zSVj)7%q|j==`x$Wvu{FO4$5I&8O)4z}XXUh=S5`&7l&U5>X?@Di$=EP-nbSZ2#y zwTD5!$m5@CGCrMrD{gF!F=i={f!+AJMQ=^t{u7Y3@`BZWJwA~-$X}!xfeu3!;k5t& zkfP{k3Ev8I+tU7|GIeRN1ou z(+QF?uT(+^1zQDnlJ3K(^XanvxdgysUr<&r0IvX6TJ1$J+9PmYdq1QD_BhU?&F!MK z!FOm4vQOY8Tt1Zqt_@%T1PloMw_*9rsYZDjl{{3DX}%-B3|=m!&)Rf3?AtB48TPAI zo6Ua@n2se;H^xXQd%R<7x9<`38AStjJV=;E;pj?Hehe8LQMtDBl*t2*07irhkmq?& z3>HU4pw=EJEG71l;`DThyj>eK}WF*?5zx!qoQp03chi}%T0X_mYpQn=@?3m;c0fnIn*6Mig^;GLOMYL*9Ndo z7{?X9Y}jgmdxhCtI%}qi6J(Q&Z%*)faMWea6~$e?JP0H>O(p7*7;!!q!{Qodi0Ck? z-g0`)brwsb{>lid`1Nx7&Q-_=`{+%cGFDUb&JOfZ)f0~*;~evjOH3+}G2pNs^sD1y zt9Wf9$mGpt+)1$$ZKn!4P^Ue9NY_9uBFZSyhpQ`$$u@(~TSQl9LJXYNP&IualpH64 zg#n94s-|bZptFcK?}h!4%&IlFY+AQX=dD3AKbOS(3-gFWmP;fHn>w*CYR>G0NJJI!M7AD~watWv5@0op%o)ni z(TOAuXWVyL$3mDP=1hWqW6&_s!{gLOlsq#-w!lhQUPEjHd*DGtx}NKuA+W`BM=(2g zM?mL9z+VVr{_sb8!0X$EccJ;LZhNE0Fw`~CsY^*Xt_2pJW zbSkV7JOEH6P0uU|Dn#?dBa=#AN@7?!Oo>M{aexT8iW%}QkiwZvnlZX+41x>VVvq<^ z!m_kSDZOD$!G^ZDa@6V&eaC}w6^`eZKme@s%kT&V3k~N8YJe2jR+u)JSDI2x8D*f4 zU#542%KVdQBEIg-OJ;d}^wCF;A3wIePp!vAfztO9GC=~DD1iW2OBOzG;DO`)_uv1m z-}k1F%I;{t|h!l|Zza95ib-D;y6r)2-;2St`e0|Mg%0p&$AoIt8{CMad=u zAs-}gi4q8awPfKNpnwaQSLDC;wXd-U-Z6d5!;X2rs%c0v;s$@yS1Gqbrh#l7L1lx^ zV^|88f&{Lt1Oi|!`PSGp5TfQV0xy`9*_4pNCZHI^tQtP>Re^ojao{HYH97?%InD#b zC5t1F3ldnW1Oi|!Rh(m-;(Z(#0XzW>9BtF#nLQb7&Ac?TunBq76|B2vx#1=-3TPU5 zQ3sUtZxx%BLq160ic25>){<`!xBwg&T4BW)RDllDP(sHP&a-rm2c~&UPq(uDxnPNn zZs>~)7M8d8=Fth|K>}A(0+$R}(+cLpyqfZAR`YqZy;ig`3k2bIgidMn_rh|UMv<@ za$VRCdArCawk{(_P}kx)r-7G0kAGo z+LzOpL4X}Ehl8{AdSz#Y8jhUq*H5HpyAfBZRRFB3bT?MO%k@^OD~MSEr&jQko}T0- zhc%p@(NOcu5^yTEKnTd0JDSyyUqstO!R@I+3;-YL(ryPMc4(=8Ew#0awiBf$CED&P zVXtHuCO{--BL(e@Q18jdb?u6yx+-O>7m?K&U)eg-5XY3od!@}7j9bSH2RLJshMGgR z+2V`#?1#hh&x0sts~I?C%PV&5qi-6*KUc+J8MVr|$Cq>XA_d_Owts`jg>*g%Tryz! z-+v!Y;!A8?&2pdjm!yjBsX9>zS z0yE5j8}7N_X}aHpXW*JA05dRdF}MK3Y&bMn(@ggi`%smc5jyexYe`B)L@Fz@c4SpV z#?r2>aJgLW%YSM8xqNBKork--V5=cj)o<^t?T!M}ft*B&67fX1lLusRxUbNssR;FP z{m3v#h%lV$d)9qdxJP+JqJPgOQ7!!w&NuhvWy-z=Xa=9?tFN~&+6DG8aJYa%U} zk~=(}*nzgf**SL;8_&t@X13c_NUcP_73eO}xOg|YTSwN9%4 zou`+~NX0B#A`<=CVcKGENE~2)uG9(^n$0`Po@O?@B6&?oj~Art(Yhr+3!XGqTv~5liu}F(WD$yVaqoK}l#@PLYAqG=zH7UQ;wbT>kfe{rWfmK5c*3 z*00yg1#6FH^<4NcV;J+Ib1_r-wJf5ARmJS=-xm4rtMnSniY0egnbg);RYTNlm20`u zn=NJWWwmva^R5)eFeJrPe6S(^U@HV?MnI~Nsi!tryE~bSXUgO<`Tjrt%PKuj3(B%C z_7ZvMFeAe{%@(t)DBrN!Pg$4i*;kimfB)6r`)A3;Pikg@*XAJY%H}yY5L)5b-Ht#I z*6p~wckW=t3^hO&4`m;&^>1i~O5T~EUCKByjSJJXXHo!?a`GbQq4kgJ+)j5Sg-QjmSWP1m6iEioL(?NyYWNIknDPuygnwBdC+mJ)69LNx}(c(Mz4CZW6FMR|AAqqiptyq))B!rsNO71%{H55CTD1`+p9`#?(L%&A2l3r$ufClE0@q)r}r)}Oo4xz1w zR{l;g#-5R}sW8r0B~VEHgS=PycWM5%uG+dp|IugseQR7@a1N5g6)3mX+Dhtz?=sCc zk?Y!+5k8XN9v%@>*$_^@cEB~+tWIZiSjFD8S2f)P&6va;ci4rrkI}@m&Gd#DltD13 zD6Cf3t2$e?O{qJJ4OnBE;d@JGOox%XrFPSzd=}gzVYsR;_>qFAhk;a&4u)!me7Y=%TM{X{3GXEFCi1oaxq9i zZS40oBvgwS1vh%=A`EtfCm8CHV9$FqhIaLQmMjX5ycquB$YN^yrseZuF_cQS@_@74 zJ5@8wdQP6eP!F``Gu989kG*A_#VPTCWx_RVS}b0BPv5oIm2J*ND(hXKA~tGMRb;`N zc&S(z0|XJaoz38=x+!Fh71fHx$QbG}!C0B%mv80>DCw~An1gJcFrBP4%s}YK+tQu+ z>8G5|XxC4X8QUZRL0FT}yx)l};&66wvUpI1Q=`bO`iD9PBBL$O^aOi1zF;Wk#&UTm z;AYow`THi&F(V0a)=nL+q8hy!qlrgEGYqYoq%PYU3k8b>uTxpAc`Mgu*fhv1FBs!k z7NdbZ7Fy~Zzf_GO12s^kD8bw8mqFEXg0iQSn~T$Mkl$}(dDNN}I$S*5P{x=>e+2;v z2-4&YFE$}b!Z3{XS>9DFYNXWErTEPlb9R0Pyz?2a5m@PmMUs+2nDl~5oDo$SrFJ4x zL?yesc0ajKw^R_;{_nkW0o1^h#4ldFU`lmz9y$t}B9tl1Yp8$Vsd2}kxlcd+^!)sM zC^kO3k2G5AjNz48?13l6F6e6StSO3FrX>iS#+aLne|5FS>Ju+PSQN&UT%^LAI&H2onsRz7#s>T1SS3 zGx-S_8H}Z19=M2~QarGj&(VBr5Y`}USJ!jaSgu&4UTXqsF)f@zTqLE2!ieFL!pn$K z)Tt-~f?=e_TFZoI8LL?k0%B!N-7tvUAS{=5Kx}+y)|}Xo*LrZ)Om6nVC&1#JCA4Io ztrB-UV`$p1+Evk9Efzf&axNDdY;V9<<@ma zJnj~-`4u~a{6~8M=rLJ#WJQsjZG`zv@h6;`>hGZt){}vLU@PV${^E--e({T6{PLH- zgdnzfaF6}vX{NTR8_ZeEE6(~Vs)zwqA>>r~DZwc34N+i7h?#uUV-nc45SST8{>n`p zgQjD4=#K#l3URg}9>B(l*iv$wwqopoG&MbRj2}d{O+G1&2GyR)L2rN{BBl zs9Es{B0sSeb(EEA8KjcfCe|RcAIf4wn7qvkSwUECT5=k;Py%tQG+$s|No=YWZ%PEJ zR;J=f#Vp7V7FIoW7_xClQBKhpx{a4kreV)PCY6qKMR%QDY&)-~dyRXOZKOvl_ zdJh7nDlTLjM50e9V7H;@KBBRq8b(lm0#%n?p$vZWyv%1+&T{ocUNLmai3yN59=?&1 zQ#+KvV`<$=*K)aYfW5;B#(s(n#V%vSrMP$ap~fXf00b<9qpMA3d@uud*hUpriw?$O zTPLg=X~nC`jJ4*w?Iw3%_DA zYQEW8AMX^avcTM`#LvQr+r(Z5p{FF%#^$Ih{%j+=17bt|tpX=?7=*Rw+YJ$*B(5G# z3Yaq_AhJETWYxJs(%cjQ!Je!@!GM$kUW`j1uPGndl#oNGya0!6;sGB)OQogVVk!B7 zFfwqK+lE%#S)4>9%;Etf)E4g%S0Pj_%_l8ZBcr>H92+5`vNBpwWVoy09UbQ}rD_7N z^k1FB6tDs$LIwFXqp*hYCd{DEbg04ic=|MEdRF7d03r)2+F0xLrw_em4=k4-9RgQG#;NMI#FXS}>4igGn;SMzq2wn{MC z!qkFuuv{PnG?tx}WLyaww)mFF6jz|YjMUTwsx7+(XH`j@d_J%83mjc=OxDcKXU~ep z;;_s%p}Y&KF{m?;Y&{rKduZ#VxID3T?g;Z!AFbL)lE7Ohj0dXdSA zE^NhGwx%E!1v^^FF@8cM5!BeO`77||%%lCuH!Vgur z8cT=C#ZA=(GF22-+?Jswv&(Gthjsm%I=du}B5N+WdordpIF(7qCpeanm1;Di=qzI~Gx(4e z8q!`Z`A}6s8~hWiaf1JJPLyPqu~$)TmXHx@2Ns1&iW@3~`(gOh{q?I(XCH*M=X>oV zFRv6{fE{28p0(k2O`ab&UkVi6t~7-7^cBeL!k1;m!j zvhXHp|2p~W|L}VKZ_2CxRyN-efseafT&FtB)33ei!KaRZCXhyg(2+9XB%LL#kB1OX zWq?Kf#k{)Kl^SEJ2Lflj^;Bhf1p`zd=CJ{1hk9H#(m_~{@(l2(X@tNdZ)+5~-~HB> zD0yqXU%q?F>52Ry;WbRvgO%sJNqIJ^We?X(mG5~+!n?g7hFC)x7#H zX}(LYdBLyQWBLnGTnXCwv zSS9(ZB>S7|Rne}h1v6P0J4r&xgSX7A#d?NeXX8PAw`h(vv8N3-RZw*ro6~N_fVX*_ zG`~&Wa`apvXrrf|;I~iMiGDYqWzUoBWmCLtGhUTYM$UYF03d|jK-Oi-P}*Tn?K}CH8d3P#8feFeK4uK*dXLq zC1{1wld{nh*U#GA%z47QMM#4$xl2MnnG@=}zxr3d`>TKVYAKf!FUsUQx6M_{+wGOo zj1xI~-X_=o>wNxw+Pq~@+SUu)xh8UjCv1EuM#s)->LYcoHWqTWTVW&>$F{$Tl^AyhQ_)Uq1mG8p0Es2DgP=a0|kM8|B(Q3ml*Xt4-l zM#Mrz+yJpxX3*uSqBd(hTp9X2ZEo<^vtGS!pf7MUo)T-B`ybvxC*}D^ zWQRcS6c>5%*{uGPq&zb(uIb2|HUxb6&;D5<;kM2FaB0*SB@1Fj+9=Y-uiELEV ztEbcqMRP}3gSY1@O|CR8jJQIy!&MN6!Yy65RZDLL)D<&HNnuq#_63b$F=2j~Dte<> zGw!EK`5c-_Uv#~FzGrU*3Be*_fTplrYx{5+B1rkxho}+u$mO-zRrlB_xB|(skm_Pq z6X46o%>{KeC2#sTg&PtJvc#yFRcEsM6o)nT<9BTclaV>cAwyukiDc}|*FyF(k&Ysq zdHn1m1hp3rz|qkbVH?Dh$=!+F1|f|qyhm3~qDT~vy_3{yONRbnL^L?A|{H47r=Y=IN4OUHFR{Wz!M0+XB6D}s$ zi|8)wJuD#t>k*P3(UFcRB$PIFR{Wy9b;0vxHhFNY3Q0Qt>xRs9NO_`M_1){?;yFg0 z=v-(!;wl!~fItw|07WE52*58!F{7+5*-@7M_#1(=6|;rnu!AFY=?k! z5Y@7`fl@%zD0a|~;yLY{746NA8{PC8Tv1JzO$)h(e@1Pv1RvAjvoSY1{vK(yxw7C4 zTvaqo8*|0i^QzOHLvk7z~OIJ7Y+M!*;_a7*m#`A)YZYQX@{v+`M?qarwd zB>)M%fE;jg#C3xIbPyW|1YyNvu!X>;w%?`vgA#EG#*W5J)HUc92U0-vZ767ipK&CI z>%WR&<)|G07=KX>b!s3cCdBR_bW9eU%SI*#Gu{TagtkK?7Ly6%=#^eaC0m`k{SYqV z8_NtQx?TX#0-xhhEs+k2A%KBIsbU%&WYpl4bVE{Gl{y)VgLIMl!2jYg(tfVLfNH65 zgo;MV6xLNL2`4K|gs%C^K(DR8+JA-5X3j|*9SULH+|c1Rz6<}GORVH=H}#e(hO4&9 zb2x0{%ZBpf%+pMFDkb|zUL8k9k>4^9%t$dEH*oAODptq|%ZWw~C?g<-b@E@M&{AFI zSm1g>78LsgOUdh8%x7(OzPi3%=v^J!D;8rFFt~$R6`++oeOA@a-~51TpQl&ZVsTzx zr?WW`Qx#oxKvYc-r@Onm1?g__0O|1P21!LiP!Ko{kM5KZkdQp!XrxopLqhVSq+8?; z1c~o)f4zOXJHMIV%Now(vZy8nL5~~7-5|?C>7PMCx=({$c@=!d8z#}Ck6fvbLqxLucR!b2xb~c^< z>eF;A2i&xMIr)eAqRxu;cQ7R`cYQ}Jg5eiqYf*F=Y}%n=Thl2Ddno3>3XAR!IKOVJ zu~bKK zW>rP{wvF)o?YK~XJJFp4I70Xh=h)KirZQvEDoI%OmYn@Yp^%ET(mm6uY)&Ws7T9)l zUUpwkl19>UK$7Mr$p0)r;(c@fA5Uhw@D%CYqm8sK|49~-D@nA&oUr|EzUUpQkmu** z`7gNV{L%Dv)#sqOo@eH;8n^Ad>icv1s?+=PPv+@Mou;e=tui3`{Gvfi22-J}?XOwf z`UyYJ@{vx(Omsq<_b&1E;v5ZLw|k|Rz~CSH?{X@LWu+FTGTW$aFuDJ$wU(8aEWVVvy`6YKLR(W9ppA*Lf-QKRIG zmRC1<<6F487!AUYjQ3R9+TD5xvfKDruy#h$oUYt@c+<$eGJ~;bVm+dzR2IY z=6Y`^d<|I%D>PrbcU|pfv`j^g*nS|{72bwd#uXKi5;cE}jml`Vsm(c|DjMv-9fmcH z=q}?0XZcTeaKO{MZ&lmR-MN{5nr2pMY}*WPWkO@%glo=v3xXbEOJe~S`Rl*Ql0yEK zqkEP@QhR`7^*kE>b`Vy}yXXmnV0~Eqo>hx1iV0|wJil7UE!}V7RxjBmoB51~wZDQ! zWuvV4(wF#~Y>cV8BM#vLcASuvZkHi({mqn*9^dE{VuA&^@yKzIBub6T11S%*Xd>0 ze(jD2@<*LP?JDr%UX}?Ql}@9Vhu0*!75y9%!*iOA>G%`4!O(fTbVkWxr53aAdD=S> z4r70XD*w7LVhxttOJWL6D5^H@6NN@>jw-x0h-uu*>53A&VA64ya<_&bDU)adsi6` ztZu_sgr|!eJT}AoZNJpi^mKK4=lUSc5-yFcd@dCuLg4LIl!VH?_OpM+U8kdeidA24 z+;g=nmC&ruPm-zP-0_nwJ^wt*vA#Ho@S-5WBn$Cln)bPK)FVPa_eh)%r^sqtVZ845 zgoZIKWRZv?V}$hzYLdaLLCjix%eay#w4@Q;mEG!gKK2a z$DqZb&xwhww``~2lvHLRBH4{Q6AH6fg+Czn0WnIlwX4~QQgext#{S3tJ4W-*ZJY$F z)4+tOah%){oMM>z0--V*8Pbx2dPVyYl@W;|g_DJXClMVa{1UOG=bn8ABog|AjQSss zxgZ7bvsYlfHJt&NBEtuY&!VyLQK*Er#pIB>QO|;>+GnrDr$_AmB*SEiU3|2@%Wlth zUdXx{oReJMTMfPA_dE%+9p|bENgRyEAeAGjd5t=r!#4g!>Ajb%8wyO(!WdLQsc5-iE;603fcgPY37}M{G6d5;(`lJ|Xi)v5w@de-8tI?HRH2M_JH=d7^nS=d znsAP7mJb%A&>sPsSnkEl<2>_Gabgul4Lw18qZOxtseuX`e=TlVtW3v_s4iNebcuba zoeb(DxUGiaD3@Orxnt(tH6}9l@>o`!hzz zJh|_m-y>z->GN*-?b z{qb{o!Airt5_*gk<%5Z;y!cj^(Bf#|2M+36w_B@`m`8-vG`Mfayw9oO^eBdLokb@y zuptfxf_jb|x#IK)P?sx3A3h^_PfakC#U|0_dk!rVaKOvgW0(%9;7)8}l6N`p;??R1 zhs_~e&dS3`bTOBiU*u2BrEaD_h1piIb zKPvvmLFp9_4mAp_P0#-lgt<(ll^d!c+zgo@JwCN8drf2&9)g3(ZPvnE0Oex{u089* zOd`-Qy}ja-YcHjkQ5u(+*)w({LGvFYERUfN$Xs8M)!3ZsVdC%QiPe^;Y7 zHLf|5_c;WeJc1km%U6cCWU2^=jLRm|bJFJCL%YVPglaZ#?tLm|Jsy7QcF*w^)9e~k z)_+FB^t9)n>f3dof-#a-8&;l}`MZkhq~#U*@$Gr#`ErCPlXQWK9rBi(+hWK3u2RXF zNnFHy&WiPuEfjspj(M@xT4 zI1s#15uRtQ&3-qi*8m+ z-V+U$f{9l2O7y-joj*s}V!smmk)AK$*oSQ(J_sk5nNOtj-9`RFxze|fwX@$kwr#x7 z%w>AXCmbl6Vr=*buOuEj$C&A!4wr;eKT7iV7vjGt;4#7Qo=vqhS}qw4)q}(y)+A+D zV7&S%c;zJ}%b$gD$3#OIoiV}ZOvkU-x%-R7H{Hw6p1myBtky~Sv4MEn(T=^=zI0GK z-B4e|2)M`<>2JFd#=J0(7U)UQ+JzldYsu)VSLFxT(^!XN$j`}AQ$0^;++)|aAXUTf z8u-h)i$VwVU~5%=G{fg7@xFV4k>s0YS2VUg%*W;?dZ;VC{EmW2ql!E(nfRRY5?{;W z;`16-%q?jTFQod$M-F_KB_upPuX&-I>2u}xwKJsph1lzrsHhfXaEG4jQ?Rg;&K8*@ z3uWT*xZNw-uw;!KQSwEp;CCZYw=M&z$OE5lO!AQz$P8Qo;%bjGWr>;R-`!pe;EdW* zS`w9+&N0oL6}_zJDP-jduXRaZSR$KNT9c{~;5>-uUE30C)=ittP1x>nB|MzRKqVTe zBF;l^FYmyYoj$2Fyi1Yc6bUwj@^%R^!&?dWYw?E zu80Mi-r%VXSGVAdDjxgBaG_U}D!LYoFRID}>16_%gq&{5I|lAq7Okx`w`B_&zq=+~ zyX*9jB_UMk#u}Z>ehMTNm|K*=UVdigs-3?B7MpEMVQm(|X4SMLAl}j<5gp{NKh-jr zWvjl6_K;T+M76E%JmDTa)&gFEFI@x!`zLaJOjI2ETKr5{j#nrc;4$4QV^m5>mK^;~ z0|qJdc(kIm9I8*W^5ogar1pznq#0vK^0L<6r4Slp4RYaRGv#Fe!+k{C{wQ9LbZXG- z$i;*)b>wct;)4`b65);zwifNlJH+9WVI#8-LdK*w_T}dd6OZC=Fgzoa#O52E#6_nc z*$O6ScHw^3EO=g>Ud0wP3(q2VCGJ1)9`fw^gZtlx;G|!pd)$8c$>+LylCwTjZ);=D zOoY|#EH8fMT743U@V?N}2=SXh1TAF7>+Wb5uFwY;zFCCKmC;)k<;>O)m#OCe(d#X1 zMGuyiO#AKb(v_56T`z^?OvY_Zut-^9MjMxO_lZlqvkBqxnWwm=*Cz$9lo|BlS#3c)wG*!QzBVino}G<9eS12+11-a(B)Y9=`x(h6RElmXG=&`0_dlWhxQkB(gJ54+T$av6+QLIZ#IX4GKR5Lt-f<5 z0n7V@Pxe>Cru{osjOuonnl>tIc5|D`!kzP6?S8sd?>re$6Co&LygZ`XgT?nggX>2p zc%ID}PknE2MZJSXl?Om%kJ?{BNKT8k3j8P=!Yx3;fyNJ@g-b`UE??m$B69nCd*Im3A47QJAAEg%SO>XiEOc}cmrt@71$t^Xppj|9}T`9~c z!l_|hHlPX_7~5zOu0vO z2aD%CLEZh}Zzu`mQ|6^^_TPJapI(G?aF(|x+m>AEioh^LSsxvUC9mI%x4Q}-iIe`~ zCQSu_pBJ0ICoeQ(y8$4u<|I6gTa;$ zjR?G(m8VQGfr#uV7SYAZIiJNMzOpSyMW^@Kk)1U7UgIn^*CTy-sFwB3#3u;>gjwpO z@3EMjRx!~wPSO)d($x#1yItA}Rz9277# z2BO%VgMuK$28ZmecAK=!qQXsEMVHoLvdF#0A;kUaa?j6(ir|PCy5UPo>}8*#sr-Pp z2Tai_D-2Gl0bPMQ4tOXhBN`CdTfz;<+@sLK23jFR0i7-)U1q5XaIlgew&ay)u#i5q z>IO7lqE9n61iC9JU_~=WH~&PR(a>3!Ctd%pFW;2&^f55{%en4;uv|%=FdB+F8CDwD zyly@C3hm#)D%sqEMWUhX;qH$q2yj-}0r<<|$F(GYWby<61s_i-ml!BqYH<)8-qy!I z0+dsvr`ZWgV|F;*gCDfs}I7@M{0+iq<0&UdS3Md}T_y%-NoQin^Fv22* zmCP31T(@dCw5cR@vh(AY4V%z?_wO4EwBE{aGVf$%^LzKn<=JyYQF%$sC<0+{9GiHD zho$v^Nx9ZZp-dqs4Nz}zoTLT8hH`5HG{n5Ni(w=9!~~$12M(9?Kzk`4v>KEZeM*jF zKnhT3+OavcrnVkKDVHk;=l&-5RP_l?l-y@)epiKlGG)rtf&O4FS&4Q^f=^fvLVP}! zBtv_t)dAVD|BdWva`+Gd7UcKaCaK_I8R7r}`Te*{n4!IbK$Ha#y_LUaAW)(Nv?>@$ zOz_6RDi81N8**e_3Q;cqO%XDp!Zv=J_`90?>iJQNbR@*WqQe4Y@W6!t0EQpz`~(ho zX92`P;68@A!K^AFI4()YTEljq(cV7GA{1HM_h(MI3#NGhC z^Y6LJ%! zwA_Q$`gzr#bt8}^0w7SEPG6%QQ@k?)t^+=|o-c&z0l5WTZJ{_!r6eFeOvOw(00Ptj z0@OFF3SU~y)c?z7v%c$!V9jJD#q#($+e)N}?qwYh(QFMAl6Z*a3d$JH4dbKNc zmg)QPvC1RWaFPn+a{^#$fLDk7fdIwQ`T$Tj^3ydZ6B5P%3y5hIav{Po zV1Ytwv=e54+2lKir@ zAjsfE^{oKMk6RxXEPdY)nj|6iBJP!YPjF_Dc)^A)^Pb2fAcAEdga;a#FyWakW|S0NXdXe&1snejac(6l0LV06xz^ L22X3BI7IvpG}KwK literal 0 HcmV?d00001 diff --git a/web/src/App.tsx b/web/src/App.tsx index ddc6d028..462f15bf 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -11,6 +11,7 @@ import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import { t, type Language } from './i18n/translations'; import { useSystemConfig } from './hooks/useSystemConfig'; +import { Bot, RefreshCw, TrendingUp, BarChart3, Brain, Download, Upload, Check, X, AlertCircle, Zap, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react'; import type { SystemStatus, AccountInfo, @@ -175,9 +176,8 @@ function App() { return (
-
- ⚡ +
+ NoFx Logo

加载中...

@@ -201,9 +201,7 @@ function App() {
{/* Left - Logo and Title */}
-
- ⚡ -
+ NoFx Logo

{t('appTitle', language)} @@ -264,7 +262,8 @@ function App() { {/* Admin Mode Indicator */} {systemConfig?.admin_mode && (
- ⚡ 管理员模式 + + 管理员模式
)} @@ -429,9 +428,9 @@ function TraderDetailsPage({

- - 🤖 - +
+ +
{selectedTrader.trader_name}

@@ -470,8 +469,9 @@ function TraderDetailsPage({ {/* Debug Info */} {account && (
-
- 🔄 Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} | +
+ + Last Update: {lastUpdate} | Total Equity: {account?.total_equity?.toFixed(2) || '0.00'} | Available: {account?.available_balance?.toFixed(2) || '0.00'} | P&L: {account?.total_pnl?.toFixed(2) || '0.00'}{' '} ({account?.total_pnl_pct?.toFixed(2) || '0.00'}%)
@@ -517,7 +517,8 @@ function TraderDetailsPage({

- 📈 {t('currentPositions', language)} + + {t('currentPositions', language)}

{positions && positions.length > 0 && (
@@ -581,7 +582,9 @@ function TraderDetailsPage({
) : (
-
📊
+
+ +
{t('noPositions', language)}
{t('noActivePositions', language)}
@@ -594,11 +597,11 @@ function TraderDetailsPage({
{/* 标题 */}
-
- 🧠 +

{t('recentDecisions', language)}

@@ -618,7 +621,9 @@ function TraderDetailsPage({ )) ) : (
-
🧠
+
+ +
{t('noDecisionsYet', language)}
{t('aiDecisionsWillAppear', language)}
@@ -657,10 +662,11 @@ function StatCard({ {change !== undefined && (
- {positive ? '▲' : '▼'} {positive ? '+' : ''} + {positive ? : } + {positive ? '+' : ''} {change.toFixed(2)}%
@@ -704,7 +710,8 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua className="flex items-center gap-2 text-sm transition-colors" style={{ color: '#60a5fa' }} > - 📥 {t('inputPrompt', language)} + + {t('inputPrompt', language)} {showInputPrompt ? t('collapse', language) : t('expand', language)} {showInputPrompt && ( @@ -723,7 +730,8 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua className="flex items-center gap-2 text-sm transition-colors" style={{ color: '#F0B90B' }} > - 📤 {t('aiThinking', language)} + + {t('aiThinking', language)} {showCoT ? t('collapse', language) : t('expand', language)} {showCoT && ( @@ -753,9 +761,11 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua {action.price > 0 && ( @{action.price.toFixed(4)} )} - - {action.success ? '✓' : '✗'} - + {action.success ? ( + + ) : ( + + )} {action.error && {action.error}}
))} @@ -789,8 +799,9 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua {/* Error Message */} {decision.error_message && ( -
- ❌ {decision.error_message} +
+ + {decision.error_message}
)}
diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index 20da61d2..f44baf27 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -2,6 +2,7 @@ import useSWR from 'swr'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; import { api } from '../lib/api'; +import { Brain, BarChart3, TrendingUp, TrendingDown, Sparkles, Coins, Trophy, ScrollText, Lightbulb } from 'lucide-react'; interface TradeOutcome { symbol: string; @@ -72,7 +73,9 @@ export default function AILearning({ traderId }: AILearningProps) { if (!performance) { return (
-
📊 {t('loading', language)}
+
+ {t('loading', language)} +
); } @@ -81,7 +84,7 @@ export default function AILearning({ traderId }: AILearningProps) { return (
- 🧠 +

{t('aiLearning', language)}

@@ -109,12 +112,12 @@ export default function AILearning({ traderId }: AILearningProps) { filter: 'blur(60px)' }} />
-
- 🧠 +

{performance.total_trades}

-
📊 Trades
+
+ Trades +
@@ -199,7 +204,9 @@ export default function AILearning({ traderId }: AILearningProps) {
+{(performance.avg_win || 0).toFixed(2)}
-
📈 USDT Average
+
+ USDT Average +
@@ -220,7 +227,9 @@ export default function AILearning({ traderId }: AILearningProps) {
{(performance.avg_loss || 0).toFixed(2)}
-
📉 USDT Average
+
+ USDT Average +
@@ -239,11 +248,11 @@ export default function AILearning({ traderId }: AILearningProps) { }} />
-
- 🧬 +
夏普比率
@@ -307,11 +316,11 @@ export default function AILearning({ traderId }: AILearningProps) { }} />
-
- 💰 +
@@ -373,7 +382,7 @@ export default function AILearning({ traderId }: AILearningProps) { boxShadow: '0 4px 16px rgba(16, 185, 129, 0.1)' }}>
- 🏆 + {t('bestPerformer', language)}
@@ -395,7 +404,7 @@ export default function AILearning({ traderId }: AILearningProps) { boxShadow: '0 4px 16px rgba(248, 113, 113, 0.1)' }}>
- 📉 + {t('worstPerformer', language)}
@@ -428,7 +437,7 @@ export default function AILearning({ traderId }: AILearningProps) { backdropFilter: 'blur(10px)' }}>

- 📊 {t('symbolPerformance', language)} + {t('symbolPerformance', language)}

@@ -488,7 +497,7 @@ export default function AILearning({ traderId }: AILearningProps) { backdropFilter: 'blur(10px)' }}>
- 📜 +

{t('tradeHistory', language)}

@@ -631,7 +640,9 @@ export default function AILearning({ traderId }: AILearningProps) { }) ) : (

-
📜
+
+ +
{t('noCompletedTrades', language)}
)} @@ -646,11 +657,11 @@ export default function AILearning({ traderId }: AILearningProps) { boxShadow: '0 4px 16px rgba(240, 185, 11, 0.1)' }}>
-
- 💡 +

{t('howAILearns', language)}

diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index d76d45f7..25763608 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -6,6 +6,7 @@ import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; import { getExchangeIcon } from './ExchangeIcons'; import { getModelIcon } from './ModelIcons'; +import { Bot, Brain, Landmark, BarChart3, Trash2, Plus, Users, AlertTriangle } from 'lucide-react'; // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { @@ -360,11 +361,11 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {/* Header */}
-
- 🤖 +

@@ -385,38 +386,41 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
- + - +

@@ -425,8 +429,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
{/* AI Models */}
-

- 🧠 {t('aiModels', language)} +

+ + {t('aiModels', language)}

{configuredModels.map(model => { @@ -465,7 +470,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { })} {configuredModels.length === 0 && (
-
🧠
+
暂无已配置的AI模型
)} @@ -474,8 +479,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {/* Exchanges */}
-

- 🏦 {t('exchanges', language)} +

+ + {t('exchanges', language)}

{configuredExchanges.map(exchange => { @@ -506,7 +512,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { })} {configuredExchanges.length === 0 && (
-
🏦
+
暂无已配置的交易所
)} @@ -518,23 +524,24 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {

- 👥 {t('currentTraders', language)} + + {t('currentTraders', language)}

{traders && traders.length > 0 ? (
{traders.map(trader => ( -
-
- 🤖 +
@@ -566,29 +573,30 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
- + - +
@@ -597,7 +605,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
) : (
-
🤖
+
{t('noTraders', language)}
{t('createFirstTrader', language)}
{(configuredModels.length === 0 || configuredExchanges.length === 0) && ( @@ -855,11 +863,12 @@ function CreateTraderModal({ style={{ accentColor: '#F6465D' }} />
-
+
+ 覆盖基础交易策略
- ⚠️ 警告:勾选后将完全使用您的自定义策略,不再使用系统默认的风控和交易逻辑。 + 警告:勾选后将完全使用您的自定义策略,不再使用系统默认的风控和交易逻辑。 这可能导致交易风险增加。仅在您完全理解交易逻辑时使用此选项。
@@ -950,7 +959,7 @@ function ModelConfigModal({ style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} title="删除配置" > - 🗑️ + )}
@@ -1122,7 +1131,7 @@ function ExchangeConfigModal({ style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }} title="删除配置" > - 🗑️ + )}
diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index b333c04f..b0dec3f0 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -14,6 +14,7 @@ import useSWR from 'swr'; import { api } from '../lib/api'; import type { CompetitionTraderData } from '../types'; import { getTraderColor } from '../utils/traderColors'; +import { BarChart3 } from 'lucide-react'; interface ComparisonChartProps { traders: CompetitionTraderData[]; @@ -133,7 +134,9 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { if (combinedData.length === 0) { return (
-
📊
+
+ +
暂无历史数据
运行几个周期后将显示对比曲线
diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx index 2a638b83..59e9be88 100644 --- a/web/src/components/CompetitionPage.tsx +++ b/web/src/components/CompetitionPage.tsx @@ -3,6 +3,7 @@ import { api } from '../lib/api'; import type { CompetitionData } from '../types'; import { ComparisonChart } from './ComparisonChart'; import { getTraderColor } from '../utils/traderColors'; +import { Trophy, Medal, Circle, CircleDot } from 'lucide-react'; export function CompetitionPage() { const { data: competition } = useSWR( @@ -51,11 +52,11 @@ export function CompetitionPage() { {/* Competition Header - 精简版 */}
-
- 🏆 +

@@ -121,8 +122,12 @@ export function CompetitionPage() {
{/* Rank & Name */}
-
- {index === 0 ? '🥇' : index === 1 ? '🥈' : '🥉'} +
+
{trader.trader_name}
@@ -171,13 +176,17 @@ export function CompetitionPage() { {/* Status */}
- {trader.is_running ? '●' : '○'} + {trader.is_running ? ( + + ) : ( + + )}
diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index b8b846cf..bfa89617 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -13,6 +13,7 @@ import useSWR from 'swr'; import { api } from '../lib/api'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { AlertTriangle, BarChart3, DollarSign, Percent, TrendingUp as ArrowUp, TrendingDown as ArrowDown } from 'lucide-react' interface EquityPoint { timestamp: string; @@ -52,16 +53,26 @@ export function EquityChart({ traderId }: EquityChartProps) { if (error) { return ( -
-
-
⚠️
+
+
+
-
{t('loadingError', language)}
-
{error.message}
+
+ {t('loadingError', language)} +
+
+ {error.message} +
- ); + ) } // 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致) @@ -69,15 +80,21 @@ export function EquityChart({ traderId }: EquityChartProps) { if (!validHistory || validHistory.length === 0) { return ( -
-

{t('accountEquityCurve', language)}

-
-
📊
-
{t('noHistoricalData', language)}
-
{t('dataWillAppear', language)}
+
+

+ {t('accountEquityCurve', language)} +

+
+
+ +
+
+ {t('noHistoricalData', language)} +
+
{t('dataWillAppear', language)}
- ); + ) } // 限制显示最近的数据点(性能优化) @@ -161,142 +178,238 @@ export function EquityChart({ traderId }: EquityChartProps) { }; return ( -
+
{/* Header */} -
-
-

{t('accountEquityCurve', language)}

-
- +
+
+

+ {t('accountEquityCurve', language)} +

+
+ {account?.total_equity.toFixed(2) || '0.00'} - USDT - -
+ USDT + + +
+ - {isProfit ? '▲' : '▼'} {isProfit ? '+' : ''} + {isProfit ? : } + {isProfit ? '+' : ''} {currentValue.raw_pnl_pct}% - - ({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT) + + ({isProfit ? '+' : ''} + {currentValue.raw_pnl.toFixed(2)} USDT)
{/* Display Mode Toggle */} -
+
{/* Chart */} -
- - - - - - - - - - - - displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%` - } - /> - } /> - - 50 ? false : { fill: '#F0B90B', r: 3 }} - activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }} - connectNulls={true} - /> - - +
+ + + + + + + + + + + + displayMode === 'dollar' ? `$${value.toFixed(0)}` : `${value}%` + } + /> + } /> + + 50 ? false : { fill: '#F0B90B', r: 3 }} + activeDot={{ + r: 6, + fill: '#FCD535', + stroke: '#F0B90B', + strokeWidth: 2, + }} + connectNulls={true} + /> + +
{/* Footer Stats */} -
-
-
{t('initialBalance', language)}
-
+
+
+
+ {t('initialBalance', language)} +
+
{initialBalance.toFixed(2)} USDT
-
-
{t('currentEquity', language)}
-
+
+
+ {t('currentEquity', language)} +
+
{currentValue.raw_equity.toFixed(2)} USDT
-
-
{t('historicalCycles', language)}
-
{validHistory.length} {t('cycles', language)}
+
+
+ {t('historicalCycles', language)} +
+
+ {validHistory.length} {t('cycles', language)} +
-
-
{t('displayRange', language)}
-
+
+
+ {t('displayRange', language)} +
+
{validHistory.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` - : t('allData', language) - } + : t('allData', language)}
- ); + ) } diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index 3fb1c693..c0c25065 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,5 +1,6 @@ import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { Zap } from 'lucide-react'; interface HeaderProps { simple?: boolean; // For login/register pages @@ -14,9 +15,9 @@ export function Header({ simple = false }: HeaderProps) {
{/* Left - Logo and Title */}
-
- ⚡ +

diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index bf4dd9e3..97fe8390 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -3,6 +3,7 @@ import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; import { Header } from './Header'; +import { Zap, Smartphone } from 'lucide-react'; export function LoginPage() { const { language } = useLanguage(); @@ -57,9 +58,9 @@ export function LoginPage() {
{/* Logo */}
-
- ⚡ +

{t('loginTitle', language)} @@ -121,7 +122,9 @@ export function LoginPage() { ) : (
-
📱
+
+ +

{t('scanQRCodeInstructions', language)}
{t('enterOTPCode', language)} diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 7d3d2284..033be1a0 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { Zap, Smartphone, Lock } from 'lucide-react'; export function RegisterPage() { const { language } = useLanguage(); @@ -75,9 +76,9 @@ export function RegisterPage() {

{/* Logo */}
-
- ⚡ +

{t('appTitle', language)} @@ -158,7 +159,9 @@ export function RegisterPage() { {step === 'setup-otp' && (
-
📱
+
+ +

{t('setupTwoFactor', language)}

@@ -236,7 +239,9 @@ export function RegisterPage() { {step === 'verify-otp' && (
-
🔐
+
+ +

{t('enterOTPCode', language)}
{t('completeRegistrationSubtitle', language)} From e5208522f2d0d7a2142c292d56a4c560b1f79965 Mon Sep 17 00:00:00 2001 From: Ember <197652334@qq.com> Date: Fri, 31 Oct 2025 23:54:10 +0800 Subject: [PATCH 3/3] UI feat: replace Lucide icons with logo images in multiple components --- web/src/App.tsx | 4 +--- web/src/components/Header.tsx | 6 ++---- web/src/components/LoginPage.tsx | 7 +++---- web/src/components/RegisterPage.tsx | 7 +++---- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 462f15bf..7c84ba9d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -176,9 +176,7 @@ function App() { return (

-
- NoFx Logo -
+ NoFx Logo

加载中...

diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index c0c25065..af141d85 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,6 +1,5 @@ import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; -import { Zap } from 'lucide-react'; interface HeaderProps { simple?: boolean; // For login/register pages @@ -15,9 +14,8 @@ export function Header({ simple = false }: HeaderProps) {
{/* Left - Logo and Title */}
-
- +
+ NoFx Logo

diff --git a/web/src/components/LoginPage.tsx b/web/src/components/LoginPage.tsx index 97fe8390..8923ebe2 100644 --- a/web/src/components/LoginPage.tsx +++ b/web/src/components/LoginPage.tsx @@ -3,7 +3,7 @@ import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; import { Header } from './Header'; -import { Zap, Smartphone } from 'lucide-react'; +import { Smartphone } from 'lucide-react'; export function LoginPage() { const { language } = useLanguage(); @@ -58,9 +58,8 @@ export function LoginPage() {
{/* Logo */}
-
- +
+ NoFx Logo

{t('loginTitle', language)} diff --git a/web/src/components/RegisterPage.tsx b/web/src/components/RegisterPage.tsx index 033be1a0..68139e42 100644 --- a/web/src/components/RegisterPage.tsx +++ b/web/src/components/RegisterPage.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; -import { Zap, Smartphone, Lock } from 'lucide-react'; +import { Smartphone, Lock } from 'lucide-react'; export function RegisterPage() { const { language } = useLanguage(); @@ -76,9 +76,8 @@ export function RegisterPage() {
{/* Logo */}
-
- +
+ NoFx Logo

{t('appTitle', language)}