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 0000000..d933667 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..80af286 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..e3c4207 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png new file mode 100644 index 0000000..e3c4207 Binary files /dev/null and b/public/icons/maskable-512.png differ 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") + }) +}