mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
4595 refactor PWA (#4664)
* feat: add web app manifest and mobile PWA meta tags * feat: serve dynamic manifest.json with custom branding for pwa * feat: add ios status bar theming for pwa * fix: prevent overscroll behavior for mobile * fix: prevent ios safari auto-zoom on chat input * fix: remove theme-color meta tags conflicting with ios status bar * fix: add missing apple-mobile-web-app-capable meta tag for ios pwa * fix: move catch-all route after manifest endpoint to prevent interception * feat: add pwa detection helper and conditional styling for standalone mode * PWA refactor * undo changes to native CSS * class fix * proper response obj * fix patch for import * fix manifest errors --------- Co-authored-by: Christian De Santis <christian.constantino98@gmail.com>
This commit is contained in:
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@@ -6,7 +6,7 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['4534-disable-prisma-telemetry'] # put your current branch to create a build. Core team only.
|
||||
branches: ['4595-refactor-pwa'] # put your current branch to create a build. Core team only.
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'cloud-deployments/*'
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||
|
||||
<!-- PWA -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -35,4 +40,4 @@
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { PfpProvider } from "./PfpContext";
|
||||
import { LogoProvider } from "./LogoContext";
|
||||
import { FullScreenLoader } from "./components/Preloader";
|
||||
import { ThemeProvider } from "./ThemeContext";
|
||||
import { PWAModeProvider } from "./PWAContext";
|
||||
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
|
||||
|
||||
const Main = lazy(() => import("@/pages/Main"));
|
||||
@@ -96,190 +97,208 @@ const MobileConnections = lazy(
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Suspense fallback={<FullScreenLoader />}>
|
||||
<AuthProvider>
|
||||
<LogoProvider>
|
||||
<PfpProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Routes>
|
||||
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/sso/simple"
|
||||
element={<SimpleSSOPassthrough />}
|
||||
/>
|
||||
<PWAModeProvider>
|
||||
<Suspense fallback={<FullScreenLoader />}>
|
||||
<AuthProvider>
|
||||
<LogoProvider>
|
||||
<PfpProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={<PrivateRoute Component={Main} />}
|
||||
/>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/sso/simple"
|
||||
element={<SimpleSSOPassthrough />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/workspace/:slug/settings/:tab"
|
||||
element={<ManagerRoute Component={WorkspaceSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/:slug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/:slug/t/:threadSlug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
||||
<Route
|
||||
path="/workspace/:slug/settings/:tab"
|
||||
element={<ManagerRoute Component={WorkspaceSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/:slug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/:slug/t/:threadSlug"
|
||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||
/>
|
||||
<Route
|
||||
path="/accept-invite/:code"
|
||||
element={<InvitePage />}
|
||||
/>
|
||||
|
||||
{/* Admin */}
|
||||
<Route
|
||||
path="/settings/llm-preference"
|
||||
element={<AdminRoute Component={GeneralLLMPreference} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/transcription-preference"
|
||||
element={
|
||||
<AdminRoute Component={GeneralTranscriptionPreference} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/audio-preference"
|
||||
element={<AdminRoute Component={GeneralAudioPreference} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/embedding-preference"
|
||||
element={
|
||||
<AdminRoute Component={GeneralEmbeddingPreference} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/text-splitter-preference"
|
||||
element={
|
||||
<AdminRoute Component={EmbeddingTextSplitterPreference} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/vector-database"
|
||||
element={<AdminRoute Component={GeneralVectorDatabase} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents"
|
||||
element={<AdminRoute Component={AdminAgents} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents/builder"
|
||||
element={
|
||||
<AdminRoute
|
||||
Component={AgentBuilder}
|
||||
hideUserMenu={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents/builder/:flowId"
|
||||
element={
|
||||
<AdminRoute
|
||||
Component={AgentBuilder}
|
||||
hideUserMenu={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/event-logs"
|
||||
element={<AdminRoute Component={AdminLogs} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/embed-chat-widgets"
|
||||
element={<AdminRoute Component={ChatEmbedWidgets} />}
|
||||
/>
|
||||
{/* Manager */}
|
||||
<Route
|
||||
path="/settings/security"
|
||||
element={<ManagerRoute Component={GeneralSecurity} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/privacy"
|
||||
element={<AdminRoute Component={PrivacyAndData} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/interface"
|
||||
element={<ManagerRoute Component={InterfaceSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/branding"
|
||||
element={<ManagerRoute Component={BrandingSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/chat"
|
||||
element={<ManagerRoute Component={ChatSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/beta-features"
|
||||
element={<AdminRoute Component={ExperimentalFeatures} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/api-keys"
|
||||
element={<AdminRoute Component={GeneralApiKeys} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/system-prompt-variables"
|
||||
element={<AdminRoute Component={SystemPromptVariables} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/browser-extension"
|
||||
element={
|
||||
<ManagerRoute Component={GeneralBrowserExtension} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/workspace-chats"
|
||||
element={<ManagerRoute Component={GeneralChats} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/invites"
|
||||
element={<ManagerRoute Component={AdminInvites} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/users"
|
||||
element={<ManagerRoute Component={AdminUsers} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/workspaces"
|
||||
element={<ManagerRoute Component={AdminWorkspaces} />}
|
||||
/>
|
||||
{/* Onboarding Flow */}
|
||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||
<Route
|
||||
path="/onboarding/:step"
|
||||
element={<OnboardingFlow />}
|
||||
/>
|
||||
{/* Admin */}
|
||||
<Route
|
||||
path="/settings/llm-preference"
|
||||
element={<AdminRoute Component={GeneralLLMPreference} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/transcription-preference"
|
||||
element={
|
||||
<AdminRoute
|
||||
Component={GeneralTranscriptionPreference}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/audio-preference"
|
||||
element={
|
||||
<AdminRoute Component={GeneralAudioPreference} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/embedding-preference"
|
||||
element={
|
||||
<AdminRoute Component={GeneralEmbeddingPreference} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/text-splitter-preference"
|
||||
element={
|
||||
<AdminRoute
|
||||
Component={EmbeddingTextSplitterPreference}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/vector-database"
|
||||
element={<AdminRoute Component={GeneralVectorDatabase} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents"
|
||||
element={<AdminRoute Component={AdminAgents} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents/builder"
|
||||
element={
|
||||
<AdminRoute
|
||||
Component={AgentBuilder}
|
||||
hideUserMenu={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/agents/builder/:flowId"
|
||||
element={
|
||||
<AdminRoute
|
||||
Component={AgentBuilder}
|
||||
hideUserMenu={true}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/event-logs"
|
||||
element={<AdminRoute Component={AdminLogs} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/embed-chat-widgets"
|
||||
element={<AdminRoute Component={ChatEmbedWidgets} />}
|
||||
/>
|
||||
{/* Manager */}
|
||||
<Route
|
||||
path="/settings/security"
|
||||
element={<ManagerRoute Component={GeneralSecurity} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/privacy"
|
||||
element={<AdminRoute Component={PrivacyAndData} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/interface"
|
||||
element={<ManagerRoute Component={InterfaceSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/branding"
|
||||
element={<ManagerRoute Component={BrandingSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/chat"
|
||||
element={<ManagerRoute Component={ChatSettings} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/beta-features"
|
||||
element={<AdminRoute Component={ExperimentalFeatures} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/api-keys"
|
||||
element={<AdminRoute Component={GeneralApiKeys} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/system-prompt-variables"
|
||||
element={<AdminRoute Component={SystemPromptVariables} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/browser-extension"
|
||||
element={
|
||||
<ManagerRoute Component={GeneralBrowserExtension} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/workspace-chats"
|
||||
element={<ManagerRoute Component={GeneralChats} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/invites"
|
||||
element={<ManagerRoute Component={AdminInvites} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/users"
|
||||
element={<ManagerRoute Component={AdminUsers} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/workspaces"
|
||||
element={<ManagerRoute Component={AdminWorkspaces} />}
|
||||
/>
|
||||
{/* Onboarding Flow */}
|
||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||
<Route
|
||||
path="/onboarding/:step"
|
||||
element={<OnboardingFlow />}
|
||||
/>
|
||||
|
||||
{/* Experimental feature pages */}
|
||||
{/* Live Document Sync feature */}
|
||||
<Route
|
||||
path="/settings/beta-features/live-document-sync/manage"
|
||||
element={<AdminRoute Component={LiveDocumentSyncManage} />}
|
||||
/>
|
||||
{/* Experimental feature pages */}
|
||||
{/* Live Document Sync feature */}
|
||||
<Route
|
||||
path="/settings/beta-features/live-document-sync/manage"
|
||||
element={
|
||||
<AdminRoute Component={LiveDocumentSyncManage} />
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/community-hub/trending"
|
||||
element={<AdminRoute Component={CommunityHubTrending} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/community-hub/authentication"
|
||||
element={
|
||||
<AdminRoute Component={CommunityHubAuthentication} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/community-hub/import-item"
|
||||
element={<AdminRoute Component={CommunityHubImportItem} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/community-hub/trending"
|
||||
element={<AdminRoute Component={CommunityHubTrending} />}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/community-hub/authentication"
|
||||
element={
|
||||
<AdminRoute Component={CommunityHubAuthentication} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings/community-hub/import-item"
|
||||
element={
|
||||
<AdminRoute Component={CommunityHubImportItem} />
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings/mobile-connections"
|
||||
element={<ManagerRoute Component={MobileConnections} />}
|
||||
/>
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
<KeyboardShortcutsHelp />
|
||||
</I18nextProvider>
|
||||
</PfpProvider>
|
||||
</LogoProvider>
|
||||
</AuthProvider>
|
||||
</Suspense>
|
||||
<Route
|
||||
path="/settings/mobile-connections"
|
||||
element={<ManagerRoute Component={MobileConnections} />}
|
||||
/>
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
<KeyboardShortcutsHelp />
|
||||
</I18nextProvider>
|
||||
</PfpProvider>
|
||||
</LogoProvider>
|
||||
</AuthProvider>
|
||||
</Suspense>
|
||||
</PWAModeProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
93
frontend/src/PWAContext.jsx
Normal file
93
frontend/src/PWAContext.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* Detects if the application is running as a standalone PWA
|
||||
* @returns {boolean} True if running as standalone PWA
|
||||
*/
|
||||
function isStandalonePWA() {
|
||||
if (typeof window === "undefined") return false;
|
||||
|
||||
const matchesStandaloneDisplayMode =
|
||||
typeof window.matchMedia === "function"
|
||||
? window.matchMedia("(display-mode: standalone)")?.matches
|
||||
: false;
|
||||
|
||||
const isIOSStandalone = window.navigator?.standalone === true; // iOS Safari
|
||||
const androidReferrer =
|
||||
typeof document !== "undefined" && document?.referrer
|
||||
? document.referrer.includes("android-app://")
|
||||
: false;
|
||||
|
||||
return Boolean(
|
||||
matchesStandaloneDisplayMode || isIOSStandalone || androidReferrer
|
||||
);
|
||||
}
|
||||
|
||||
const PWAModeContext = createContext({ isPWA: false });
|
||||
export function PWAModeProvider({ children }) {
|
||||
const [isPWA, setIsPWA] = useState(() => isStandalonePWA());
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
const mediaQuery =
|
||||
typeof window.matchMedia === "function"
|
||||
? window.matchMedia("(display-mode: standalone)")
|
||||
: null;
|
||||
|
||||
const updateStatus = () => setIsPWA(isStandalonePWA());
|
||||
|
||||
updateStatus();
|
||||
|
||||
if (mediaQuery?.addEventListener) {
|
||||
mediaQuery.addEventListener("change", updateStatus);
|
||||
} else if (mediaQuery?.addListener) {
|
||||
mediaQuery.addListener(updateStatus);
|
||||
}
|
||||
|
||||
window.addEventListener("appinstalled", updateStatus);
|
||||
window.addEventListener("visibilitychange", updateStatus);
|
||||
|
||||
return () => {
|
||||
if (mediaQuery?.removeEventListener) {
|
||||
mediaQuery.removeEventListener("change", updateStatus);
|
||||
} else if (mediaQuery?.removeListener) {
|
||||
mediaQuery.removeListener(updateStatus);
|
||||
}
|
||||
|
||||
window.removeEventListener("appinstalled", updateStatus);
|
||||
window.removeEventListener("visibilitychange", updateStatus);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return undefined;
|
||||
|
||||
document.body.classList.toggle("pwa", isPWA);
|
||||
document.documentElement?.setAttribute(
|
||||
"data-pwa",
|
||||
isPWA ? "true" : "false"
|
||||
);
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("pwa");
|
||||
document.documentElement?.removeAttribute("data-pwa");
|
||||
};
|
||||
}, [isPWA]);
|
||||
|
||||
const value = useMemo(() => ({ isPWA }), [isPWA]);
|
||||
|
||||
return (
|
||||
<PWAModeContext.Provider value={value}>{children}</PWAModeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePWAMode() {
|
||||
return useContext(PWAModeContext);
|
||||
}
|
||||
@@ -243,7 +243,7 @@ export default function PromptInput({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center">
|
||||
<div className="w-full fixed md:absolute bottom-0 left-0 z-10 md:z-0 flex justify-center items-center pwa:pb-5">
|
||||
<SlashCommands
|
||||
showing={showSlashCommand}
|
||||
setShowing={setShowSlashCommand}
|
||||
@@ -261,7 +261,7 @@ export default function PromptInput({
|
||||
className="flex flex-col gap-y-1 rounded-t-lg md:w-3/4 w-full mx-auto max-w-xl items-center"
|
||||
>
|
||||
<div className="flex items-center rounded-lg md:mb-4 md:w-full">
|
||||
<div className="w-[95vw] md:w-[635px] bg-theme-bg-chat-input light:bg-white light:border-solid light:border-[1px] light:border-theme-chat-input-border shadow-sm rounded-2xl flex flex-col px-2 overflow-hidden">
|
||||
<div className="w-[95vw] md:w-[635px] bg-theme-bg-chat-input light:bg-white light:border-solid light:border-[1px] light:border-theme-chat-input-border shadow-sm rounded-2xl pwa:rounded-3xl flex flex-col px-2 overflow-hidden">
|
||||
<AttachmentManager attachments={attachments} />
|
||||
<div className="flex items-center border-b border-theme-chat-input-border mx-3">
|
||||
<textarea
|
||||
@@ -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 pwa:!text-[16px] ${textSizeClass}`}
|
||||
placeholder={t("chat_window.send_message")}
|
||||
/>
|
||||
{isStreaming ? (
|
||||
|
||||
@@ -255,6 +255,18 @@ body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #0e0f0f;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -289,6 +289,7 @@ export default {
|
||||
plugins: [
|
||||
function ({ addVariant }) {
|
||||
addVariant('light', '.light &') // Add the `light:` variant
|
||||
addVariant('pwa', '.pwa &') // Add the `pwa:` variant
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -101,15 +101,20 @@ 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();
|
||||
});
|
||||
|
||||
app.get("/manifest.json", async function (_, response) {
|
||||
IndexPage.generateManifest(response);
|
||||
return;
|
||||
});
|
||||
|
||||
app.use("/", function (_, response) {
|
||||
IndexPage.generate(response);
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
// Debug route for development connections to vectorDBs
|
||||
apiRouter.post("/v/:command", async (request, response) => {
|
||||
|
||||
@@ -26,6 +26,20 @@ class MetaGenerator {
|
||||
/** @type {MetaTagDefinition[]|null} */
|
||||
#customConfig = null;
|
||||
|
||||
#defaultManifest = {
|
||||
name: "AnythingLLM",
|
||||
short_name: "AnythingLLM",
|
||||
display: "standalone",
|
||||
orientation: "portrait",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: "/favicon.png",
|
||||
sizes: "any",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (MetaGenerator._instance) return MetaGenerator._instance;
|
||||
MetaGenerator._instance = this;
|
||||
@@ -126,6 +140,24 @@ class MetaGenerator {
|
||||
|
||||
{ tag: "link", props: { rel: "icon", href: "/favicon.png" } },
|
||||
{ tag: "link", props: { rel: "apple-touch-icon", href: "/favicon.png" } },
|
||||
|
||||
// PWA specific tags
|
||||
{
|
||||
tag: "meta",
|
||||
props: { name: "mobile-web-app-capable", content: "yes" },
|
||||
},
|
||||
{
|
||||
tag: "meta",
|
||||
props: { name: "apple-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" } },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -181,19 +213,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;
|
||||
@@ -228,6 +319,58 @@ class MetaGenerator {
|
||||
</body>
|
||||
</html>`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the manifest.json file for the PWA application on the fly.
|
||||
* @param {import('express').Response} response
|
||||
* @param {number} code
|
||||
*/
|
||||
async generateManifest(response) {
|
||||
try {
|
||||
const { SystemSettings } = require("../../models/systemSettings");
|
||||
const manifestName = await SystemSettings.getValueOrFallback(
|
||||
{ label: "meta_page_title" },
|
||||
"AnythingLLM"
|
||||
);
|
||||
const faviconURL = await SystemSettings.getValueOrFallback(
|
||||
{ label: "meta_page_favicon" },
|
||||
null
|
||||
);
|
||||
|
||||
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",
|
||||
start_url: "/",
|
||||
icons: [
|
||||
{
|
||||
src: iconUrl,
|
||||
sizes: "any",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
response.type("application/json").status(200).send(manifest).end();
|
||||
} catch (error) {
|
||||
this.#log(`error generating manifest: ${error.message}`, error);
|
||||
response
|
||||
.type("application/json")
|
||||
.status(200)
|
||||
.send(this.#defaultManifest)
|
||||
.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.MetaGenerator = MetaGenerator;
|
||||
|
||||
Reference in New Issue
Block a user