From 89fa3740f987771c860356a2b31bd832843b4df2 Mon Sep 17 00:00:00 2001 From: feie9456 Date: Sun, 26 Apr 2026 22:30:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20PWA=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 22 ++++++-- public/icons/apple-touch-icon.png | Bin 0 -> 680 bytes public/icons/icon-192.png | Bin 0 -> 735 bytes public/icons/icon-512.png | Bin 0 -> 2986 bytes public/icons/maskable-512.png | Bin 0 -> 2986 bytes public/manifest.webmanifest | 33 ++++++++++++ public/service-worker.js | 85 ++++++++++++++++++++++++++++++ public/vite.svg | 1 - src/main.tsx | 6 +++ 9 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 public/icons/apple-touch-icon.png create mode 100644 public/icons/icon-192.png create mode 100644 public/icons/icon-512.png create mode 100644 public/icons/maskable-512.png create mode 100644 public/manifest.webmanifest create mode 100644 public/service-worker.js delete mode 100644 public/vite.svg diff --git a/index.html b/index.html index 1f738fe..4af8334 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,26 @@ - + - - vite-app + + + + + + + + + + Simple LLM Chat UI
diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d9336678da3df070401a618892a4cfc0c6408498 GIT binary patch literal 680 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD4M^IaWiw)6V9NJ&aSW-r_4dv|-WCHM2FIuc z|C5|Kzgr)D?RbmrLyd>iyyA1pp`V0Uwm@LRG$<6iAOwX>Q84J>4TanpoKVQ93xf+( z7yQ5Td!PG$W~sOnjcpQn3mn;Y^GfNg5MGeTWX&w4u)=u3!A2`)DG4CMu~906u~s?5 zVF6o|!+j1Zo)wK^7tEX3n6ehInp|L#O5=6n-O>P*W99{MGZ=w9Iqs^<%q22kj{gy=)`CR8 zg=|tloe<)dyW$y#=xv*G*iCM1R%Fgye!))>W;@U=&J1w3Z2KB}hmqyh!zEK!G`2~? z49Hu^wyU>NHbcJEA^K&5WX9`@yRXg5Ke$H@>=kF;&t4Y-4)bhj*e7Xn#o)PvGjA~; zDx;aVNXX=dg1RG^@qmGu*V_shj8bV68vBylIA5@&bu{Sx>E__S(7FHULPo2?>FhcV z>UmL3EH8d8ILJ_ClP1yNm(a!lRCt)(it*NkM!y8JoCU{|%@P&}FW?meiZh2YZFbZu z+&-5v%O=D5vc?4kZDy&IREOp@DjCibvw2h&utJl<6IBjuk*o@gXjQl(mMusr=F%Ly Ys>5o37%lam15+S_r>mdKI;Vst05U}N5C8xG literal 0 HcmV?d00001 diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..80af286931328d53ca9c5b2770e16c2161733cba GIT binary patch literal 735 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zf+;U|Qzs;uunK>+Rj$6I&bvSRG>| z{`cncIJnMSr?N;UB92RDes$WjiK~7JvNTW;9@t)d?!|uwgDTy_317UD4eFK(O6*v$EI|JN#RHfE>=-(j3wRh9EEqVL3K$p~m>C#kfPQ9R&}-_IXZX(8BhOIY z#0pUkF@`~b0o4?s@D9O-_B#w8EKGm~1FgHq0W=1r4g?PTU@Q;>+7Q=eUdM3l9;aCi zgAJD=rWrsN0=4WBdbv)IL9UIN5v=z&$SNR+X<}&j_5&!^V8kGPkl_jA9iRZvD}R0p zGxT#-uq@(XV0z4cAc8@IxdCXQffU0j1_gtEHVhm+_WVGp^C02RNJ1=!*%@G%qnZ6d z7|ZPXYKETs9~cel7PBkx9yr0^(RY6b!-{|F6Av>f{Jj5+m#g89`_>+gh9~Nxz3dJ5 ze( zW{}voiGe4cL1J$gkcwnz(+5(nKg{CCBH_K|w%TmOrLNQ{o1%;#twIX)M znATJif-m|K1nG+)>XQ`FvR4CnGZb2UFxuU)puUuBK+HC5SHE*6$>z`gJS!XOmZ2BYFA`}?HFhL%)9Do9ipd zH@-X9JvHIA*PngwT=j?k*Zv?n(GM6=D0uRTEM|r%z@X?pTMro?f)hr8dLZ^%xCPL8 z+HLLrK7q1S@*YvP5P!t-i0qp4;TtDjw{P9ak5P2J?k7_A#|EP`z2E9-Lzfc1_Gt3i zV2@7+FYr8;3=GdhFQA@^^+Ou2sJNx5RF?vHxgm{87F#D!mNjrP&;VpZ13=Y!>wumr zb&&!XfT&yt0KmTT01lw6ks6ef)?+Zm8BARvSIR(*0jyTao#Z7YOW?Dd)jA({)baVk z7YHBoa^PmZkPj5|`AA)XajO9a1bjSn$Y#=3=A;5q3mI&zQ)94CbBPe?8Z_6b1=K&r z`&-F*pB<874C(dkLU9uaK<0g&8(jZX-|k(xbLV!Wq_CQaiPxyq*Xg*5OQA~4(O54k3O`Y~?=UCJh(C?9s|qtHx?pG8*fJ#So0{DjOQRXyVzGI6 za3set3Q6zjO1&dtGA4-^W>|+KXO2`glcepDs<8Vi<2XapTb!1#cp;XPnvEk!P`;l3 ztC@@?p>yTs!@-J9iWIvby|sLn0lU>(N7GMmx=O0Qor+yMyDlRH8lhr`f|09E6`vx>Q(e zpRsM6Rj?jpA=E3MI!e}WYOaH!A3K`sjn0>))~>lM&=s|34O4k^x)K-9dS-c0%HogC zB}AYWq)wM5Xj7?F;e)+$a$Fs-!+qJ>KvoiN+E(@58u))dj7=P49xeY5obaFnfNAb%JsPKgU{_$J}j39c8hAx)rl z2A9A_kSfFxH|h>Fy4T1suIXVhjDK+kcbb%bJ*ZaGjUYXlJeXBBkprlFazTUMS5SdH zH9^H`Y0J1>9F*#(qEa^kyFlEHsybhY!WwCs4m6GxJaC%ULk{buWuMDDa{}H--D`D}&HK`lN;V z;j)5$zEr0v_SIqoeigdX%I5Jn!3$n9sN*$*dAw%GytvfD8?&vvaVfzYu_SLCx`4Eo zkFJ8DIKr$NPabE*taFK3VSOH~9?dR|@O!x%$-6>QStEfRknml-*ua1*8>+|mQvOg# zen&Rn;f8h$?b-YR3{8}O+%4{NLz^2O!_Y|iR`<2n4J{Y|Xm(#s7{G#8n5i4wUcLJ= zZm7e65G8v{aO6$puEO5g7_O%<1ZR)#*N(mZKq$S3wC_pAIOc3U_hxK3`rrM;^nDk_k^$t{os9?4w(FN?>TjSXNtsHed^=eU~ac z<6!$ySU!Cy9Tsq$`Kmw!*3O%)E2Q{T?;5;g3j{mXEY=+NJ+?vwZMSld@Y>b`vDd;k gi9AGFH4*z|+b5@*%K6)gFW{Jlp39`aP7RO!2Tz*%00000 literal 0 HcmV?d00001 diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c42079d53c0416a46febfcfe66c5c2e98aad4c GIT binary patch literal 2986 zcmbVNU1(fI6h1SVG&jknxoc|Mq{7}L4}H@>g{CCBH_K|w%TmOrLNQ{o1%;#twIX)M znATJif-m|K1nG+)>XQ`FvR4CnGZb2UFxuU)puUuBK+HC5SHE*6$>z`gJS!XOmZ2BYFA`}?HFhL%)9Do9ipd zH@-X9JvHIA*PngwT=j?k*Zv?n(GM6=D0uRTEM|r%z@X?pTMro?f)hr8dLZ^%xCPL8 z+HLLrK7q1S@*YvP5P!t-i0qp4;TtDjw{P9ak5P2J?k7_A#|EP`z2E9-Lzfc1_Gt3i zV2@7+FYr8;3=GdhFQA@^^+Ou2sJNx5RF?vHxgm{87F#D!mNjrP&;VpZ13=Y!>wumr zb&&!XfT&yt0KmTT01lw6ks6ef)?+Zm8BARvSIR(*0jyTao#Z7YOW?Dd)jA({)baVk z7YHBoa^PmZkPj5|`AA)XajO9a1bjSn$Y#=3=A;5q3mI&zQ)94CbBPe?8Z_6b1=K&r z`&-F*pB<874C(dkLU9uaK<0g&8(jZX-|k(xbLV!Wq_CQaiPxyq*Xg*5OQA~4(O54k3O`Y~?=UCJh(C?9s|qtHx?pG8*fJ#So0{DjOQRXyVzGI6 za3set3Q6zjO1&dtGA4-^W>|+KXO2`glcepDs<8Vi<2XapTb!1#cp;XPnvEk!P`;l3 ztC@@?p>yTs!@-J9iWIvby|sLn0lU>(N7GMmx=O0Qor+yMyDlRH8lhr`f|09E6`vx>Q(e zpRsM6Rj?jpA=E3MI!e}WYOaH!A3K`sjn0>))~>lM&=s|34O4k^x)K-9dS-c0%HogC zB}AYWq)wM5Xj7?F;e)+$a$Fs-!+qJ>KvoiN+E(@58u))dj7=P49xeY5obaFnfNAb%JsPKgU{_$J}j39c8hAx)rl z2A9A_kSfFxH|h>Fy4T1suIXVhjDK+kcbb%bJ*ZaGjUYXlJeXBBkprlFazTUMS5SdH zH9^H`Y0J1>9F*#(qEa^kyFlEHsybhY!WwCs4m6GxJaC%ULk{buWuMDDa{}H--D`D}&HK`lN;V z;j)5$zEr0v_SIqoeigdX%I5Jn!3$n9sN*$*dAw%GytvfD8?&vvaVfzYu_SLCx`4Eo zkFJ8DIKr$NPabE*taFK3VSOH~9?dR|@O!x%$-6>QStEfRknml-*ua1*8>+|mQvOg# zen&Rn;f8h$?b-YR3{8}O+%4{NLz^2O!_Y|iR`<2n4J{Y|Xm(#s7{G#8n5i4wUcLJ= zZm7e65G8v{aO6$puEO5g7_O%<1ZR)#*N(mZKq$S3wC_pAIOc3U_hxK3`rrM;^nDk_k^$t{os9?4w(FN?>TjSXNtsHed^=eU~ac z<6!$ySU!Cy9Tsq$`Kmw!*3O%)E2Q{T?;5;g3j{mXEY=+NJ+?vwZMSld@Y>b`vDd;k gi9AGFH4*z|+b5@*%K6)gFW{Jlp39`aP7RO!2Tz*%00000 literal 0 HcmV?d00001 diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..f6157ff --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,33 @@ +{ + "name": "Simple LLM Chat UI", + "short_name": "LLM Chat", + "description": "A local-first chat panel for OpenAI-compatible LLM services.", + "id": "/", + "lang": "zh-CN", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#ffffff", + "theme_color": "#111111", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/public/service-worker.js b/public/service-worker.js new file mode 100644 index 0000000..14ed945 --- /dev/null +++ b/public/service-worker.js @@ -0,0 +1,85 @@ +const CACHE_VERSION = "simple-llm-chat-ui-v2" +const APP_SHELL_URLS = [ + "/", + "/manifest.webmanifest", + "/icons/icon-192.png", + "/icons/icon-512.png", + "/icons/maskable-512.png", + "/icons/apple-touch-icon.png", +] +const CACHEABLE_DESTINATIONS = new Set(["font", "image", "script", "style"]) + +self.addEventListener("install", (event) => { + event.waitUntil( + caches + .open(CACHE_VERSION) + .then((cache) => cache.addAll(APP_SHELL_URLS)) + .then(() => self.skipWaiting()) + ) +}) + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_VERSION) + .map((key) => caches.delete(key)) + ) + ) + .then(() => self.clients.claim()) + ) +}) + +self.addEventListener("fetch", (event) => { + const { request } = event + const url = new URL(request.url) + + if (request.method !== "GET" || url.origin !== self.location.origin) { + return + } + + if (request.mode === "navigate") { + event.respondWith(networkFirst(request)) + return + } + + if (CACHEABLE_DESTINATIONS.has(request.destination)) { + event.respondWith(cacheFirst(request)) + } +}) + +async function networkFirst(request) { + const cache = await caches.open(CACHE_VERSION) + + try { + const response = await fetch(request) + + if (response.ok) { + await cache.put(request, response.clone()) + } + + return response + } catch { + return (await cache.match(request)) ?? (await cache.match("/")) + } +} + +async function cacheFirst(request) { + const cache = await caches.open(CACHE_VERSION) + const cached = await cache.match(request) + + if (cached) { + return cached + } + + const response = await fetch(request) + + if (response.ok) { + await cache.put(request, response.clone()) + } + + return response +} diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 2c36773..970bc72 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -12,3 +12,9 @@ createRoot(document.getElementById("root")!).render( ) + +if ("serviceWorker" in navigator && import.meta.env.PROD) { + window.addEventListener("load", () => { + void navigator.serviceWorker.register("/service-worker.js") + }) +}