diff --git a/app/public/manifest.json b/app/public/manifest.json
index 7806318..d9b26b7 100644
--- a/app/public/manifest.json
+++ b/app/public/manifest.json
@@ -1,8 +1,21 @@
{
+ "$schema": "https://json.schemastore.org/web-manifest-combined.json",
"short_name": "Chat with GPT",
"name": "Chat with GPT",
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
- "background_color": "#ffffff"
+ "background_color": "#ffffff",
+ "icons": [
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ]
}
diff --git a/app/src/components/page.tsx b/app/src/components/page.tsx
index a8844e5..c42a938 100644
--- a/app/src/components/page.tsx
+++ b/app/src/components/page.tsx
@@ -4,6 +4,7 @@ import { useChatSpotlightProps } from '../spotlight';
import { LoginModal, CreateAccountModal } from './auth-modals';
import Header, { HeaderProps, SubHeader } from './header';
import MessageInput from './input';
+import { InstallUpdateNotification } from './pwa-notifications';
import SettingsDrawer from './settings';
import Sidebar from './sidebar';
import AudioControls from './tts-controls';
@@ -90,6 +91,7 @@ export function Page(props: {
+
;
diff --git a/app/src/components/pwa-notifications.tsx b/app/src/components/pwa-notifications.tsx
new file mode 100644
index 0000000..efa8f5b
--- /dev/null
+++ b/app/src/components/pwa-notifications.tsx
@@ -0,0 +1,32 @@
+import { Button, Notification } from "@mantine/core";
+import { useCallback } from "react";
+import { useAppDispatch, useAppSelector } from "../store";
+import { resetUpdate, selectUpdateAvailable } from "../store/pwa";
+
+export function InstallUpdateNotification() {
+ const updateAvailable = useAppSelector(selectUpdateAvailable);
+ const dispatch = useAppDispatch();
+
+ const onClose = useCallback(() => dispatch(resetUpdate()), [dispatch]);
+
+ const onUpdate = useCallback(async () => {
+ dispatch(resetUpdate());
+
+ const registration = await navigator.serviceWorker.getRegistration();
+ if (registration && registration.waiting) {
+ registration.waiting.postMessage({ type: "SKIP_WAITING" });
+ }
+
+ window.location.reload();
+ }, [dispatch]);
+
+ return updateAvailable ? (
+
+ Click{" "}
+ {" "}
+ to get the latest version.
+
+ ) : null;
+}
diff --git a/app/src/index.tsx b/app/src/index.tsx
index 9c1f60b..3761e0c 100644
--- a/app/src/index.tsx
+++ b/app/src/index.tsx
@@ -8,6 +8,8 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { PersistGate } from 'redux-persist/integration/react';
import { AppContextProvider } from './core/context';
import store, { persistor } from './store';
+import * as serviceWorkerRegistration from "./serviceworker-registration";
+import { setUpdateAvailable } from "./store/pwa";
import ChatPage from './components/pages/chat';
import LandingPage from './components/pages/landing';
@@ -86,4 +88,8 @@ async function bootstrapApplication() {
);
}
-bootstrapApplication();
\ No newline at end of file
+bootstrapApplication();
+
+serviceWorkerRegistration.register({
+ onUpdate: () => store.dispatch(setUpdateAvailable()),
+});
diff --git a/app/src/service-worker.ts b/app/src/service-worker.ts
new file mode 100644
index 0000000..1d02d72
--- /dev/null
+++ b/app/src/service-worker.ts
@@ -0,0 +1,43 @@
+///
+/* eslint-disable no-restricted-globals */
+
+// This service worker can be customized!
+// See https://developer.chrome.com/docs/workbox/modules/
+// for the list of available Workbox modules, or add any other
+// code you'd like.
+
+import { clientsClaim } from "workbox-core";
+import { precacheAndRoute } from "workbox-precaching";
+import { registerRoute } from "workbox-routing";
+import { StaleWhileRevalidate } from "workbox-strategies";
+
+declare const self: ServiceWorkerGlobalScope;
+
+clientsClaim();
+
+// Precache all of the assets generated by your build process.
+precacheAndRoute(self.__WB_MANIFEST);
+
+// An example runtime caching route for requests that aren't handled by the
+// precache, in this case same-origin .png requests like those from in public/
+registerRoute(
+ // Add in any other file extensions or routing criteria as needed.
+ ({ url }) =>
+ url.origin === self.location.origin &&
+ (url.pathname.endsWith(".png") ||
+ url.pathname.endsWith(".ico") ||
+ url.pathname.endsWith(".json") ||
+ url.pathname.endsWith(".css")),
+ // Customize this strategy as needed, e.g., by changing to CacheFirst.
+ new StaleWhileRevalidate({
+ cacheName: "static",
+ })
+);
+
+// This allows the web app to trigger skipWaiting via
+// registration.waiting.postMessage({type: 'SKIP_WAITING'})
+self.addEventListener("message", (event) => {
+ if (event.data && event.data.type === "SKIP_WAITING") {
+ self.skipWaiting();
+ }
+});
diff --git a/app/src/serviceworker-registration.ts b/app/src/serviceworker-registration.ts
new file mode 100644
index 0000000..dc5caf4
--- /dev/null
+++ b/app/src/serviceworker-registration.ts
@@ -0,0 +1,146 @@
+// This optional code is used to register a service worker.
+// register() is not called by default.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on subsequent visits to a page, after all the
+// existing tabs open on the page have been closed, since previously cached
+// resources are updated in the background.
+
+// To learn more about the benefits of this model and instructions on how to
+// opt-in, read https://cra.link/PWA
+
+const isLocalhost = Boolean(
+ window.location.hostname === "localhost" ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === "[::1]" ||
+ // 127.0.0.0/8 are considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ )
+);
+
+type Config = {
+ onSuccess?: (registration: ServiceWorkerRegistration) => void;
+ onUpdate?: (registration: ServiceWorkerRegistration) => void;
+};
+
+export function register(config?: Config) {
+ if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebook/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener("load", () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Let's check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl, config);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ "This web app is being served cache-first by a service " +
+ "worker. To learn more, visit https://cra.link/PWA"
+ );
+ });
+ } else {
+ // Is not localhost. Just register service worker
+ registerValidSW(swUrl, config);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl: string, config?: Config) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then((registration) => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ if (installingWorker == null) {
+ return;
+ }
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === "installed") {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the updated precached content has been fetched,
+ // but the previous service worker will still serve the older
+ // content until all client tabs are closed.
+ console.log(
+ "New content is available and will be used when all " +
+ "tabs for this page are closed. See https://cra.link/PWA."
+ );
+
+ // Execute callback
+ if (config && config.onUpdate) {
+ config.onUpdate(registration);
+ }
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log("Content is cached for offline use.");
+
+ // Execute callback
+ if (config && config.onSuccess) {
+ config.onSuccess(registration);
+ }
+ }
+ }
+ };
+ };
+ })
+ .catch((error) => {
+ console.error("Error during service worker registration:", error);
+ });
+}
+
+function checkValidServiceWorker(swUrl: string, config?: Config) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl, {
+ headers: { "Service-Worker": "script" },
+ })
+ .then((response) => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ const contentType = response.headers.get("content-type");
+ if (
+ response.status === 404 ||
+ (contentType != null && contentType.indexOf("javascript") === -1)
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then((registration) => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl, config);
+ }
+ })
+ .catch(() => {
+ console.log(
+ "No internet connection found. App is running in offline mode."
+ );
+ });
+}
+
+export function unregister() {
+ if ("serviceWorker" in navigator) {
+ navigator.serviceWorker.ready
+ .then((registration) => {
+ registration.unregister();
+ })
+ .catch((error) => {
+ console.error(error.message);
+ });
+ }
+}
diff --git a/app/src/store/index.ts b/app/src/store/index.ts
index 1870901..4aa6fed 100644
--- a/app/src/store/index.ts
+++ b/app/src/store/index.ts
@@ -6,6 +6,7 @@ import messageReducer from './message';
import uiReducer from './ui';
import settingsUIReducer from './settings-ui';
import sidebarReducer from './sidebar';
+import pwaReducer from './pwa';
const persistConfig = {
key: 'root',
@@ -30,6 +31,7 @@ const store = configureStore({
ui: uiReducer,
settingsUI: settingsUIReducer,
sidebar: persistReducer(persistSidebarConfig, sidebarReducer),
+ pwa: pwaReducer,
},
})
diff --git a/app/src/store/pwa.ts b/app/src/store/pwa.ts
new file mode 100644
index 0000000..0290312
--- /dev/null
+++ b/app/src/store/pwa.ts
@@ -0,0 +1,30 @@
+import { createSlice } from "@reduxjs/toolkit";
+import type { RootState } from ".";
+
+interface UpdateState {
+ updateAvailable: boolean;
+}
+
+const initialState: UpdateState = {
+ updateAvailable: false,
+};
+
+export const updateSlice = createSlice({
+ name: "pwa",
+ initialState,
+ reducers: {
+ setUpdateAvailable: (state) => {
+ state.updateAvailable = true;
+ },
+ resetUpdate: (state) => {
+ state.updateAvailable = false;
+ },
+ },
+});
+
+export const { setUpdateAvailable, resetUpdate } = updateSlice.actions;
+
+export const selectUpdateAvailable = (state: RootState) =>
+ state.pwa.updateAvailable;
+
+export default updateSlice.reducer;