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:
parent
fd9256b361
commit
6b1b8bbc94
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@ -6,7 +6,7 @@ concurrency:
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
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:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'cloud-deployments/*'
|
- 'cloud-deployments/*'
|
||||||
|
|||||||
@ -28,6 +28,11 @@
|
|||||||
|
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
<link rel="apple-touch-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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { PfpProvider } from "./PfpContext";
|
|||||||
import { LogoProvider } from "./LogoContext";
|
import { LogoProvider } from "./LogoContext";
|
||||||
import { FullScreenLoader } from "./components/Preloader";
|
import { FullScreenLoader } from "./components/Preloader";
|
||||||
import { ThemeProvider } from "./ThemeContext";
|
import { ThemeProvider } from "./ThemeContext";
|
||||||
|
import { PWAModeProvider } from "./PWAContext";
|
||||||
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
|
import KeyboardShortcutsHelp from "@/components/KeyboardShortcutsHelp";
|
||||||
|
|
||||||
const Main = lazy(() => import("@/pages/Main"));
|
const Main = lazy(() => import("@/pages/Main"));
|
||||||
@ -96,190 +97,208 @@ const MobileConnections = lazy(
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Suspense fallback={<FullScreenLoader />}>
|
<PWAModeProvider>
|
||||||
<AuthProvider>
|
<Suspense fallback={<FullScreenLoader />}>
|
||||||
<LogoProvider>
|
<AuthProvider>
|
||||||
<PfpProvider>
|
<LogoProvider>
|
||||||
<I18nextProvider i18n={i18n}>
|
<PfpProvider>
|
||||||
<Routes>
|
<I18nextProvider i18n={i18n}>
|
||||||
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route
|
||||||
<Route
|
path="/"
|
||||||
path="/sso/simple"
|
element={<PrivateRoute Component={Main} />}
|
||||||
element={<SimpleSSOPassthrough />}
|
/>
|
||||||
/>
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/sso/simple"
|
||||||
|
element={<SimpleSSOPassthrough />}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/workspace/:slug/settings/:tab"
|
path="/workspace/:slug/settings/:tab"
|
||||||
element={<ManagerRoute Component={WorkspaceSettings} />}
|
element={<ManagerRoute Component={WorkspaceSettings} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/workspace/:slug"
|
path="/workspace/:slug"
|
||||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/workspace/:slug/t/:threadSlug"
|
path="/workspace/:slug/t/:threadSlug"
|
||||||
element={<PrivateRoute Component={WorkspaceChat} />}
|
element={<PrivateRoute Component={WorkspaceChat} />}
|
||||||
/>
|
/>
|
||||||
<Route path="/accept-invite/:code" element={<InvitePage />} />
|
<Route
|
||||||
|
path="/accept-invite/:code"
|
||||||
|
element={<InvitePage />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Admin */}
|
{/* Admin */}
|
||||||
<Route
|
<Route
|
||||||
path="/settings/llm-preference"
|
path="/settings/llm-preference"
|
||||||
element={<AdminRoute Component={GeneralLLMPreference} />}
|
element={<AdminRoute Component={GeneralLLMPreference} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/settings/transcription-preference"
|
path="/settings/transcription-preference"
|
||||||
element={
|
element={
|
||||||
<AdminRoute Component={GeneralTranscriptionPreference} />
|
<AdminRoute
|
||||||
}
|
Component={GeneralTranscriptionPreference}
|
||||||
/>
|
/>
|
||||||
<Route
|
}
|
||||||
path="/settings/audio-preference"
|
/>
|
||||||
element={<AdminRoute Component={GeneralAudioPreference} />}
|
<Route
|
||||||
/>
|
path="/settings/audio-preference"
|
||||||
<Route
|
element={
|
||||||
path="/settings/embedding-preference"
|
<AdminRoute Component={GeneralAudioPreference} />
|
||||||
element={
|
}
|
||||||
<AdminRoute Component={GeneralEmbeddingPreference} />
|
/>
|
||||||
}
|
<Route
|
||||||
/>
|
path="/settings/embedding-preference"
|
||||||
<Route
|
element={
|
||||||
path="/settings/text-splitter-preference"
|
<AdminRoute Component={GeneralEmbeddingPreference} />
|
||||||
element={
|
}
|
||||||
<AdminRoute Component={EmbeddingTextSplitterPreference} />
|
/>
|
||||||
}
|
<Route
|
||||||
/>
|
path="/settings/text-splitter-preference"
|
||||||
<Route
|
element={
|
||||||
path="/settings/vector-database"
|
<AdminRoute
|
||||||
element={<AdminRoute Component={GeneralVectorDatabase} />}
|
Component={EmbeddingTextSplitterPreference}
|
||||||
/>
|
/>
|
||||||
<Route
|
}
|
||||||
path="/settings/agents"
|
/>
|
||||||
element={<AdminRoute Component={AdminAgents} />}
|
<Route
|
||||||
/>
|
path="/settings/vector-database"
|
||||||
<Route
|
element={<AdminRoute Component={GeneralVectorDatabase} />}
|
||||||
path="/settings/agents/builder"
|
/>
|
||||||
element={
|
<Route
|
||||||
<AdminRoute
|
path="/settings/agents"
|
||||||
Component={AgentBuilder}
|
element={<AdminRoute Component={AdminAgents} />}
|
||||||
hideUserMenu={true}
|
/>
|
||||||
/>
|
<Route
|
||||||
}
|
path="/settings/agents/builder"
|
||||||
/>
|
element={
|
||||||
<Route
|
<AdminRoute
|
||||||
path="/settings/agents/builder/:flowId"
|
Component={AgentBuilder}
|
||||||
element={
|
hideUserMenu={true}
|
||||||
<AdminRoute
|
/>
|
||||||
Component={AgentBuilder}
|
}
|
||||||
hideUserMenu={true}
|
/>
|
||||||
/>
|
<Route
|
||||||
}
|
path="/settings/agents/builder/:flowId"
|
||||||
/>
|
element={
|
||||||
<Route
|
<AdminRoute
|
||||||
path="/settings/event-logs"
|
Component={AgentBuilder}
|
||||||
element={<AdminRoute Component={AdminLogs} />}
|
hideUserMenu={true}
|
||||||
/>
|
/>
|
||||||
<Route
|
}
|
||||||
path="/settings/embed-chat-widgets"
|
/>
|
||||||
element={<AdminRoute Component={ChatEmbedWidgets} />}
|
<Route
|
||||||
/>
|
path="/settings/event-logs"
|
||||||
{/* Manager */}
|
element={<AdminRoute Component={AdminLogs} />}
|
||||||
<Route
|
/>
|
||||||
path="/settings/security"
|
<Route
|
||||||
element={<ManagerRoute Component={GeneralSecurity} />}
|
path="/settings/embed-chat-widgets"
|
||||||
/>
|
element={<AdminRoute Component={ChatEmbedWidgets} />}
|
||||||
<Route
|
/>
|
||||||
path="/settings/privacy"
|
{/* Manager */}
|
||||||
element={<AdminRoute Component={PrivacyAndData} />}
|
<Route
|
||||||
/>
|
path="/settings/security"
|
||||||
<Route
|
element={<ManagerRoute Component={GeneralSecurity} />}
|
||||||
path="/settings/interface"
|
/>
|
||||||
element={<ManagerRoute Component={InterfaceSettings} />}
|
<Route
|
||||||
/>
|
path="/settings/privacy"
|
||||||
<Route
|
element={<AdminRoute Component={PrivacyAndData} />}
|
||||||
path="/settings/branding"
|
/>
|
||||||
element={<ManagerRoute Component={BrandingSettings} />}
|
<Route
|
||||||
/>
|
path="/settings/interface"
|
||||||
<Route
|
element={<ManagerRoute Component={InterfaceSettings} />}
|
||||||
path="/settings/chat"
|
/>
|
||||||
element={<ManagerRoute Component={ChatSettings} />}
|
<Route
|
||||||
/>
|
path="/settings/branding"
|
||||||
<Route
|
element={<ManagerRoute Component={BrandingSettings} />}
|
||||||
path="/settings/beta-features"
|
/>
|
||||||
element={<AdminRoute Component={ExperimentalFeatures} />}
|
<Route
|
||||||
/>
|
path="/settings/chat"
|
||||||
<Route
|
element={<ManagerRoute Component={ChatSettings} />}
|
||||||
path="/settings/api-keys"
|
/>
|
||||||
element={<AdminRoute Component={GeneralApiKeys} />}
|
<Route
|
||||||
/>
|
path="/settings/beta-features"
|
||||||
<Route
|
element={<AdminRoute Component={ExperimentalFeatures} />}
|
||||||
path="/settings/system-prompt-variables"
|
/>
|
||||||
element={<AdminRoute Component={SystemPromptVariables} />}
|
<Route
|
||||||
/>
|
path="/settings/api-keys"
|
||||||
<Route
|
element={<AdminRoute Component={GeneralApiKeys} />}
|
||||||
path="/settings/browser-extension"
|
/>
|
||||||
element={
|
<Route
|
||||||
<ManagerRoute Component={GeneralBrowserExtension} />
|
path="/settings/system-prompt-variables"
|
||||||
}
|
element={<AdminRoute Component={SystemPromptVariables} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/settings/workspace-chats"
|
path="/settings/browser-extension"
|
||||||
element={<ManagerRoute Component={GeneralChats} />}
|
element={
|
||||||
/>
|
<ManagerRoute Component={GeneralBrowserExtension} />
|
||||||
<Route
|
}
|
||||||
path="/settings/invites"
|
/>
|
||||||
element={<ManagerRoute Component={AdminInvites} />}
|
<Route
|
||||||
/>
|
path="/settings/workspace-chats"
|
||||||
<Route
|
element={<ManagerRoute Component={GeneralChats} />}
|
||||||
path="/settings/users"
|
/>
|
||||||
element={<ManagerRoute Component={AdminUsers} />}
|
<Route
|
||||||
/>
|
path="/settings/invites"
|
||||||
<Route
|
element={<ManagerRoute Component={AdminInvites} />}
|
||||||
path="/settings/workspaces"
|
/>
|
||||||
element={<ManagerRoute Component={AdminWorkspaces} />}
|
<Route
|
||||||
/>
|
path="/settings/users"
|
||||||
{/* Onboarding Flow */}
|
element={<ManagerRoute Component={AdminUsers} />}
|
||||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/onboarding/:step"
|
path="/settings/workspaces"
|
||||||
element={<OnboardingFlow />}
|
element={<ManagerRoute Component={AdminWorkspaces} />}
|
||||||
/>
|
/>
|
||||||
|
{/* Onboarding Flow */}
|
||||||
|
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||||
|
<Route
|
||||||
|
path="/onboarding/:step"
|
||||||
|
element={<OnboardingFlow />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Experimental feature pages */}
|
{/* Experimental feature pages */}
|
||||||
{/* Live Document Sync feature */}
|
{/* Live Document Sync feature */}
|
||||||
<Route
|
<Route
|
||||||
path="/settings/beta-features/live-document-sync/manage"
|
path="/settings/beta-features/live-document-sync/manage"
|
||||||
element={<AdminRoute Component={LiveDocumentSyncManage} />}
|
element={
|
||||||
/>
|
<AdminRoute Component={LiveDocumentSyncManage} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/community-hub/trending"
|
path="/settings/community-hub/trending"
|
||||||
element={<AdminRoute Component={CommunityHubTrending} />}
|
element={<AdminRoute Component={CommunityHubTrending} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/settings/community-hub/authentication"
|
path="/settings/community-hub/authentication"
|
||||||
element={
|
element={
|
||||||
<AdminRoute Component={CommunityHubAuthentication} />
|
<AdminRoute Component={CommunityHubAuthentication} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/settings/community-hub/import-item"
|
path="/settings/community-hub/import-item"
|
||||||
element={<AdminRoute Component={CommunityHubImportItem} />}
|
element={
|
||||||
/>
|
<AdminRoute Component={CommunityHubImportItem} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/mobile-connections"
|
path="/settings/mobile-connections"
|
||||||
element={<ManagerRoute Component={MobileConnections} />}
|
element={<ManagerRoute Component={MobileConnections} />}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<KeyboardShortcutsHelp />
|
<KeyboardShortcutsHelp />
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</PfpProvider>
|
</PfpProvider>
|
||||||
</LogoProvider>
|
</LogoProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
</PWAModeProvider>
|
||||||
</ThemeProvider>
|
</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 (
|
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
|
<SlashCommands
|
||||||
showing={showSlashCommand}
|
showing={showSlashCommand}
|
||||||
setShowing={setShowSlashCommand}
|
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"
|
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="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} />
|
<AttachmentManager attachments={attachments} />
|
||||||
<div className="flex items-center border-b border-theme-chat-input-border mx-3">
|
<div className="flex items-center border-b border-theme-chat-input-border mx-3">
|
||||||
<textarea
|
<textarea
|
||||||
@ -281,7 +281,7 @@ export default function PromptInput({
|
|||||||
}}
|
}}
|
||||||
value={promptInput}
|
value={promptInput}
|
||||||
spellCheck={Appearance.get("enableSpellCheck")}
|
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")}
|
placeholder={t("chat_window.send_message")}
|
||||||
/>
|
/>
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
|
|||||||
@ -255,6 +255,18 @@ body {
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #0e0f0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
html {
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@ -289,6 +289,7 @@ export default {
|
|||||||
plugins: [
|
plugins: [
|
||||||
function ({ addVariant }) {
|
function ({ addVariant }) {
|
||||||
addVariant('light', '.light &') // Add the `light:` variant
|
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) {
|
app.get("/robots.txt", function (_, response) {
|
||||||
response.type("text/plain");
|
response.type("text/plain");
|
||||||
response.send("User-agent: *\nDisallow: /").end();
|
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 {
|
} else {
|
||||||
// Debug route for development connections to vectorDBs
|
// Debug route for development connections to vectorDBs
|
||||||
apiRouter.post("/v/:command", async (request, response) => {
|
apiRouter.post("/v/:command", async (request, response) => {
|
||||||
|
|||||||
@ -26,6 +26,20 @@ class MetaGenerator {
|
|||||||
/** @type {MetaTagDefinition[]|null} */
|
/** @type {MetaTagDefinition[]|null} */
|
||||||
#customConfig = null;
|
#customConfig = null;
|
||||||
|
|
||||||
|
#defaultManifest = {
|
||||||
|
name: "AnythingLLM",
|
||||||
|
short_name: "AnythingLLM",
|
||||||
|
display: "standalone",
|
||||||
|
orientation: "portrait",
|
||||||
|
start_url: "/",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "/favicon.png",
|
||||||
|
sizes: "any",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (MetaGenerator._instance) return MetaGenerator._instance;
|
if (MetaGenerator._instance) return MetaGenerator._instance;
|
||||||
MetaGenerator._instance = this;
|
MetaGenerator._instance = this;
|
||||||
@ -126,6 +140,24 @@ class MetaGenerator {
|
|||||||
|
|
||||||
{ tag: "link", props: { rel: "icon", href: "/favicon.png" } },
|
{ tag: "link", props: { rel: "icon", href: "/favicon.png" } },
|
||||||
{ tag: "link", props: { rel: "apple-touch-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) {
|
if (customTitle === null && faviconURL === null) {
|
||||||
this.#customConfig = this.#defaultMeta();
|
this.#customConfig = this.#defaultMeta();
|
||||||
} else {
|
} else {
|
||||||
this.#customConfig = [
|
// When custom settings exist, include all default meta tags but override specific ones
|
||||||
{
|
this.#customConfig = this.#defaultMeta().map((tag) => {
|
||||||
tag: "link",
|
// Override favicon link
|
||||||
props: { rel: "icon", href: this.#validUrl(faviconURL) },
|
if (tag.tag === "link" && tag.props?.rel === "icon") {
|
||||||
},
|
return {
|
||||||
{
|
tag: "link",
|
||||||
tag: "title",
|
props: { rel: "icon", href: this.#validUrl(faviconURL) },
|
||||||
props: null,
|
};
|
||||||
content:
|
}
|
||||||
customTitle ??
|
// Override page title
|
||||||
"AnythingLLM | Your personal LLM trained on anything",
|
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;
|
return this.#customConfig;
|
||||||
@ -228,6 +319,58 @@ class MetaGenerator {
|
|||||||
</body>
|
</body>
|
||||||
</html>`);
|
</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;
|
module.exports.MetaGenerator = MetaGenerator;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user