From 6da382cfedf754d29769765bad7772b7cb742dd2 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Wed, 29 Oct 2025 19:11:45 -0400 Subject: [PATCH 1/9] feat: add web app manifest and mobile PWA meta tags --- frontend/index.html | 7 ++++++- frontend/public/manifest.json | 6 ++++++ server/utils/boot/MetaGenerator.js | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 frontend/public/manifest.json diff --git a/frontend/index.html b/frontend/index.html index 22cc5b0f627..6eb5b43c8a1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -28,6 +28,11 @@ + + + + + @@ -35,4 +40,4 @@ - \ No newline at end of file + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 00000000000..f99301edad4 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,6 @@ +{ + "name":"AnythingLLM", + "short_name":"AnythingLLM", + "display":"standalone", + "orientation":"portrait" +} diff --git a/server/utils/boot/MetaGenerator.js b/server/utils/boot/MetaGenerator.js index 61f7e0d7a80..eff7f587f27 100644 --- a/server/utils/boot/MetaGenerator.js +++ b/server/utils/boot/MetaGenerator.js @@ -126,6 +126,20 @@ class MetaGenerator { { tag: "link", props: { rel: "icon", href: "/favicon.png" } }, { tag: "link", props: { rel: "apple-touch-icon", href: "/favicon.png" } }, + + // PWA tags + { + tag: "meta", + props: { name: "mobile-web-app-capable", content: "yes" }, + }, + { + tag: "meta", + props: { + name: "apple-mobile-web-app-status-bar-style", + content: "black-translucent", + }, + }, + { tag: "link", props: { rel: "manifest", href: "/manifest.json" } }, ]; } From e4c3b8a1c7c0b87b1465395c17add3c54105d295 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Wed, 29 Oct 2025 23:33:57 -0400 Subject: [PATCH 2/9] feat: serve dynamic manifest.json with custom branding for pwa --- frontend/public/manifest.json | 6 --- server/index.js | 62 ++++++++++++++++++++++ server/utils/boot/MetaGenerator.js | 85 +++++++++++++++++++++++++----- 3 files changed, 134 insertions(+), 19 deletions(-) delete mode 100644 frontend/public/manifest.json diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json deleted file mode 100644 index f99301edad4..00000000000 --- a/frontend/public/manifest.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name":"AnythingLLM", - "short_name":"AnythingLLM", - "display":"standalone", - "orientation":"portrait" -} diff --git a/server/index.js b/server/index.js index e578c3f6405..2781ff928f3 100644 --- a/server/index.js +++ b/server/index.js @@ -110,6 +110,68 @@ if (process.env.NODE_ENV !== "development") { response.type("text/plain"); response.send("User-agent: *\nDisallow: /").end(); }); + + // Dynamic manifest.json endpoint for PWA with custom branding support + app.get("/manifest.json", async function (_, response) { + try { + const { SystemSettings } = require("./models/systemSettings"); + const customTitle = await SystemSettings.getValueOrFallback( + { label: "meta_page_title" }, + null + ); + const faviconURL = await SystemSettings.getValueOrFallback( + { label: "meta_page_favicon" }, + null + ); + + const manifestName = customTitle || "AnythingLLM"; + + // Validate icon URL + let iconUrl = "/favicon.png"; + if (faviconURL) { + try { + new URL(faviconURL); + iconUrl = faviconURL; + } catch { + iconUrl = "/favicon.png"; + } + } + + const manifest = { + name: manifestName, + short_name: manifestName, + display: "standalone", + orientation: "portrait", + icons: [ + { + src: iconUrl, + sizes: "any", + purpose: "any maskable", + }, + ], + }; + + response.type("application/json"); + response.send(JSON.stringify(manifest, null, 2)).end(); + } catch (_error) { + // Fallback to default manifest + response.type("application/json"); + response + .send( + JSON.stringify( + { + name: "AnythingLLM", + short_name: "AnythingLLM", + display: "standalone", + orientation: "portrait", + }, + null, + 2 + ) + ) + .end(); + } + }); } else { // Debug route for development connections to vectorDBs apiRouter.post("/v/:command", async (request, response) => { diff --git a/server/utils/boot/MetaGenerator.js b/server/utils/boot/MetaGenerator.js index eff7f587f27..924f52b8c7a 100644 --- a/server/utils/boot/MetaGenerator.js +++ b/server/utils/boot/MetaGenerator.js @@ -195,19 +195,78 @@ class MetaGenerator { if (customTitle === null && faviconURL === null) { this.#customConfig = this.#defaultMeta(); } else { - this.#customConfig = [ - { - tag: "link", - props: { rel: "icon", href: this.#validUrl(faviconURL) }, - }, - { - tag: "title", - props: null, - content: - customTitle ?? - "AnythingLLM | Your personal LLM trained on anything", - }, - ]; + // When custom settings exist, include all default meta tags but override specific ones + this.#customConfig = this.#defaultMeta().map((tag) => { + // Override favicon link + if (tag.tag === "link" && tag.props?.rel === "icon") { + return { + tag: "link", + props: { rel: "icon", href: this.#validUrl(faviconURL) }, + }; + } + // Override page title + if (tag.tag === "title") { + return { + tag: "title", + props: null, + content: + customTitle ?? + "AnythingLLM | Your personal LLM trained on anything", + }; + } + // Override meta title + if (tag.tag === "meta" && tag.props?.name === "title") { + return { + tag: "meta", + props: { + name: "title", + content: + customTitle ?? + "AnythingLLM | Your personal LLM trained on anything", + }, + }; + } + // Override og:title + if (tag.tag === "meta" && tag.props?.property === "og:title") { + return { + tag: "meta", + props: { + property: "og:title", + content: + customTitle ?? + "AnythingLLM | Your personal LLM trained on anything", + }, + }; + } + // Override twitter:title + if (tag.tag === "meta" && tag.props?.property === "twitter:title") { + return { + tag: "meta", + props: { + property: "twitter:title", + content: + customTitle ?? + "AnythingLLM | Your personal LLM trained on anything", + }, + }; + } + // Override apple-touch-icon if custom favicon is set + if ( + tag.tag === "link" && + tag.props?.rel === "apple-touch-icon" && + faviconURL + ) { + return { + tag: "link", + props: { + rel: "apple-touch-icon", + href: this.#validUrl(faviconURL), + }, + }; + } + // Return original tag for everything else (including PWA tags) + return tag; + }); } return this.#customConfig; From d59e1500794e3aeada07303518c3716211d01495 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Wed, 29 Oct 2025 23:58:18 -0400 Subject: [PATCH 3/9] feat: add ios status bar theming for pwa --- frontend/src/index.css | 6 ++++++ server/utils/boot/MetaGenerator.js | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/frontend/src/index.css b/frontend/src/index.css index 4f739aba9cc..46a2ca08d44 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -255,6 +255,12 @@ body { background-color: white; } +@media (prefers-color-scheme: dark) { + body { + background-color: #0e0f0f; + } +} + a { color: inherit; text-decoration: none; diff --git a/server/utils/boot/MetaGenerator.js b/server/utils/boot/MetaGenerator.js index 924f52b8c7a..84630a6c5fc 100644 --- a/server/utils/boot/MetaGenerator.js +++ b/server/utils/boot/MetaGenerator.js @@ -139,6 +139,18 @@ class MetaGenerator { content: "black-translucent", }, }, + { + tag: "meta", + props: { name: "theme-color", content: "white" }, + }, + { + tag: "meta", + props: { + name: "theme-color", + content: "#0E0F0F", + media: "(prefers-color-scheme: dark)", + }, + }, { tag: "link", props: { rel: "manifest", href: "/manifest.json" } }, ]; } From 28177b067cead9f05e39c806876c4deddf7dcb90 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Thu, 30 Oct 2025 00:04:19 -0400 Subject: [PATCH 4/9] fix: prevent overscroll behavior for mobile --- frontend/src/index.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/index.css b/frontend/src/index.css index 46a2ca08d44..ece72b16af2 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -261,6 +261,12 @@ body { } } +@media (max-width: 600px) { + html { + overscroll-behavior: none; + } +} + a { color: inherit; text-decoration: none; From 2ad9701998d5301b7a60c72576a9dfe64f57d45d Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Thu, 30 Oct 2025 00:24:10 -0400 Subject: [PATCH 5/9] fix: prevent ios safari auto-zoom on chat input --- .../WorkspaceChat/ChatContainer/PromptInput/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index 8a498f0b350..bb77a8f803b 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -281,7 +281,7 @@ export default function PromptInput({ }} value={promptInput} spellCheck={Appearance.get("enableSpellCheck")} - className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] mx-2 md:mx-0 pt-[12px] w-full leading-5 md:text-md text-white bg-transparent placeholder:text-white/60 light:placeholder:text-theme-text-primary resize-none active:outline-none focus:outline-none flex-grow mb-1 ${textSizeClass}`} + className={`border-none cursor-text max-h-[50vh] md:max-h-[350px] md:min-h-[40px] mx-2 md:mx-0 pt-[12px] w-full leading-5 text-white bg-transparent placeholder:text-white/60 light:placeholder:text-theme-text-primary resize-none active:outline-none focus:outline-none flex-grow mb-1 !text-[16px] md:${textSizeClass}`} placeholder={t("chat_window.send_message")} /> {isStreaming ? ( From d67f0f0d2c1b886ee63b957b257d77b25af8474a Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Thu, 30 Oct 2025 09:34:32 -0400 Subject: [PATCH 6/9] fix: remove theme-color meta tags conflicting with ios status bar --- server/utils/boot/MetaGenerator.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/server/utils/boot/MetaGenerator.js b/server/utils/boot/MetaGenerator.js index 84630a6c5fc..924f52b8c7a 100644 --- a/server/utils/boot/MetaGenerator.js +++ b/server/utils/boot/MetaGenerator.js @@ -139,18 +139,6 @@ class MetaGenerator { content: "black-translucent", }, }, - { - tag: "meta", - props: { name: "theme-color", content: "white" }, - }, - { - tag: "meta", - props: { - name: "theme-color", - content: "#0E0F0F", - media: "(prefers-color-scheme: dark)", - }, - }, { tag: "link", props: { rel: "manifest", href: "/manifest.json" } }, ]; } From dbf0cc356eb3404973731c3d045b5aa96ecefe54 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Thu, 30 Oct 2025 10:50:20 -0400 Subject: [PATCH 7/9] fix: add missing apple-mobile-web-app-capable meta tag for ios pwa --- server/utils/boot/MetaGenerator.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/utils/boot/MetaGenerator.js b/server/utils/boot/MetaGenerator.js index 924f52b8c7a..2ba9b6f9afd 100644 --- a/server/utils/boot/MetaGenerator.js +++ b/server/utils/boot/MetaGenerator.js @@ -132,6 +132,10 @@ class MetaGenerator { tag: "meta", props: { name: "mobile-web-app-capable", content: "yes" }, }, + { + tag: "meta", + props: { name: "apple-mobile-web-app-capable", content: "yes" }, + }, { tag: "meta", props: { From 1b424b236bf31e2e88f5e93138ad58ddd22c0445 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Thu, 30 Oct 2025 10:50:57 -0400 Subject: [PATCH 8/9] fix: move catch-all route after manifest endpoint to prevent interception --- server/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/index.js b/server/index.js index 2781ff928f3..54cee74ef73 100644 --- a/server/index.js +++ b/server/index.js @@ -101,11 +101,6 @@ if (process.env.NODE_ENV !== "development") { }) ); - app.use("/", function (_, response) { - IndexPage.generate(response); - return; - }); - app.get("/robots.txt", function (_, response) { response.type("text/plain"); response.send("User-agent: *\nDisallow: /").end(); @@ -172,6 +167,11 @@ if (process.env.NODE_ENV !== "development") { .end(); } }); + + app.use("/", function (_, response) { + IndexPage.generate(response); + return; + }); } else { // Debug route for development connections to vectorDBs apiRouter.post("/v/:command", async (request, response) => { From 0dae7cd03f6cb7c3ccdfe43dacfd2fa5ab5f3ee7 Mon Sep 17 00:00:00 2001 From: Christian De Santis Date: Thu, 30 Oct 2025 11:10:58 -0400 Subject: [PATCH 9/9] feat: add pwa detection helper and conditional styling for standalone mode --- .../ChatContainer/PromptInput/index.jsx | 10 ++++++++-- frontend/src/utils/pwa.js | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/pwa.js diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx index bb77a8f803b..8f4a34bde78 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/index.jsx @@ -24,6 +24,7 @@ import { import useTextSize from "@/hooks/useTextSize"; import { useTranslation } from "react-i18next"; import Appearance from "@/models/appearance"; +import { isStandalonePWA } from "@/utils/pwa"; export const PROMPT_INPUT_ID = "primary-prompt-input"; export const PROMPT_INPUT_EVENT = "set_prompt_input"; @@ -47,6 +48,7 @@ export default function PromptInput({ const undoStack = useRef([]); const redoStack = useRef([]); const { textSizeClass } = useTextSize(); + const isPWA = isStandalonePWA(); /** * To prevent too many re-renders we remotely listen for updates from the parent @@ -243,7 +245,9 @@ export default function PromptInput({ } return ( -
+
-
+