Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,16 @@

<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>
<div id="root" class="h-screen"></div>
<script type="module" src="/src/main.jsx"></script>
</body>

</html>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -243,7 +245,9 @@ 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 ${isPWA ? "pb-5" : "pb-4"} md:pb-0`}
>
<SlashCommands
showing={showSlashCommand}
setShowing={setShowSlashCommand}
Expand All @@ -261,7 +265,9 @@ 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 ${isPWA ? "rounded-3xl" : "rounded-2xl"} md:rounded-2xl 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
Expand All @@ -281,7 +287,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 ? (
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/utils/pwa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Detects if the application is running as a standalone PWA
* @returns {boolean} True if running as standalone PWA
*/
export function isStandalonePWA() {
if (typeof window === "undefined") return false;

// Check if running in standalone mode (PWA installed)
const isStandalone =
window.matchMedia("(display-mode: standalone)").matches ||
window.navigator.standalone === true || // iOS Safari
document.referrer.includes("android-app://"); // Android TWA

return isStandalone;
}
72 changes: 67 additions & 5 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,77 @@ 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();
});

// 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();
}
});

app.use("/", function (_, response) {
IndexPage.generate(response);
return;
});
} else {
// Debug route for development connections to vectorDBs
apiRouter.post("/v/:command", async (request, response) => {
Expand Down
103 changes: 90 additions & 13 deletions server/utils/boot/MetaGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ 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-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" } },
];
}

Expand Down Expand Up @@ -181,19 +199,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;
Expand Down