diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 9cfa4244..0d049a80 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['4855-thinking-block-toggle'] # put your current branch to create a build. Core team only. + branches: ['onboarding-flag'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/src/components/PrivateRoute/index.jsx b/frontend/src/components/PrivateRoute/index.jsx index 0a3759fc..6220ecff 100644 --- a/frontend/src/components/PrivateRoute/index.jsx +++ b/frontend/src/components/PrivateRoute/index.jsx @@ -19,27 +19,18 @@ function useIsAuthenticated() { useEffect(() => { const validateSession = async () => { - const { - MultiUserMode, - RequiresAuth, - LLMProvider = null, - VectorDB = null, - } = await System.keys(); - + const onboardingComplete = await System.isOnboardingComplete(); + const { MultiUserMode, RequiresAuth } = await System.keys(); setMultiUserMode(MultiUserMode); // Check for the onboarding redirect condition - if ( - !MultiUserMode && - !RequiresAuth && // Not in Multi-user AND no password set. - !LLMProvider && - !VectorDB - ) { + if (onboardingComplete === false) { setShouldRedirectToOnboarding(true); setIsAuthed(true); return; } + // Single User mode without password - no auth required if (!MultiUserMode && !RequiresAuth) { setIsAuthed(true); return; @@ -58,6 +49,7 @@ function useIsAuthenticated() { return; } + // Multi-user mode checks const localUser = localStorage.getItem(AUTH_USER); const localAuthToken = localStorage.getItem(AUTH_TOKEN); if (!localUser || !localAuthToken) { diff --git a/frontend/src/hooks/useOnboardingComplete.js b/frontend/src/hooks/useOnboardingComplete.js new file mode 100644 index 00000000..5d9a6a49 --- /dev/null +++ b/frontend/src/hooks/useOnboardingComplete.js @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import System from "@/models/system"; +import paths from "@/utils/paths"; + +export default function useRedirectToHomeOnOnboardingComplete() { + const navigate = useNavigate(); + useEffect(() => { + async function checkOnboardingComplete() { + const onboardingComplete = await System.isOnboardingComplete(); + if (onboardingComplete === false) return; + navigate(paths.home()); + } + checkOnboardingComplete(); + }, []); +} diff --git a/frontend/src/models/system.js b/frontend/src/models/system.js index e58764b8..3d27db3b 100644 --- a/frontend/src/models/system.js +++ b/frontend/src/models/system.js @@ -32,6 +32,32 @@ const System = { .then((res) => res.vectorCount) .catch(() => 0); }, + + /** + * Checks if the onboarding is complete. + * @returns {Promise} + */ + isOnboardingComplete: async function () { + return await fetch(`${API_BASE}/onboarding`) + .then((res) => { + if (!res.ok) throw new Error("Could not find onboarding information."); + return res.json(); + }) + .then((res) => res.onboardingComplete) + .catch(() => false); + }, + /** + * Marks the onboarding as complete. + * @returns {Promise} + */ + markOnboardingComplete: async function () { + return await fetch(`${API_BASE}/onboarding`, { + method: "POST", + headers: baseHeaders(), + }) + .then((res) => res.ok) + .catch(() => false); + }, keys: async function () { return await fetch(`${API_BASE}/setup-complete`) .then((res) => { diff --git a/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx index 48e3d21c..f0bdae5c 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx @@ -7,6 +7,7 @@ import AnythingLLMLogo from "@/media/logo/anything-llm.png"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@/hooks/useTheme"; import { useTranslation } from "react-i18next"; +import useRedirectToHomeOnOnboardingComplete from "@/hooks/useOnboardingComplete"; const IMG_SRCSET = { light: { @@ -21,6 +22,7 @@ const IMG_SRCSET = { export default function OnboardingHome() { const navigate = useNavigate(); + useRedirectToHomeOnOnboardingComplete(); const { theme } = useTheme(); const { t } = useTranslation(); const srcSet = IMG_SRCSET?.[theme] || IMG_SRCSET.default; diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx index 3f32dcbd..f56bb6bd 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -337,9 +337,16 @@ export default function LLMPreference({ fetchKeys(); }, []); - function handleForward() { - if (hiddenSubmitButtonRef.current) { - hiddenSubmitButtonRef.current.click(); + async function handleForward() { + try { + await System.markOnboardingComplete(); + console.log("Onboarding complete"); + } catch (error) { + console.error("Onboarding complete failed", error); + } finally { + if (hiddenSubmitButtonRef.current) { + hiddenSubmitButtonRef.current.click(); + } } } diff --git a/frontend/src/pages/OnboardingFlow/Steps/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/index.jsx index 6444cae3..b3e3a9ab 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/index.jsx @@ -1,6 +1,7 @@ import { ArrowLeft, ArrowRight } from "@phosphor-icons/react"; import { useState } from "react"; import { isMobile } from "react-device-detect"; +import useRedirectToHomeOnOnboardingComplete from "@/hooks/useOnboardingComplete"; import Home from "./Home"; import LLMPreference from "./LLMPreference"; import UserSetup from "./UserSetup"; @@ -18,6 +19,7 @@ const OnboardingSteps = { export default OnboardingSteps; export function OnboardingLayout({ children }) { + useRedirectToHomeOnOnboardingComplete(); const [header, setHeader] = useState({ title: "", description: "", diff --git a/server/endpoints/system.js b/server/endpoints/system.js index f096f90a..ccd6a989 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -80,6 +80,26 @@ function systemEndpoints(app) { response.sendStatus(200).end(); }); + app.get("/onboarding", async (_, response) => { + try { + const results = await SystemSettings.isOnboardingComplete(); + response.status(200).json({ onboardingComplete: results }); + } catch (e) { + console.error(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/onboarding", [validatedRequest], async (_, response) => { + try { + await SystemSettings.markOnboardingComplete(); + response.sendStatus(200).end(); + } catch (e) { + console.error(e.message, e); + response.sendStatus(500).end(); + } + }); + app.get("/setup-complete", async (_, response) => { try { const results = await SystemSettings.currentSettings(); diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index af4eb998..394dfe43 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -71,9 +71,6 @@ function workspaceEndpoints(app) { }, user?.id ); - if (onboardingComplete === true) - await Telemetry.sendTelemetry("onboarding_complete"); - response.status(200).json({ workspace, message }); } catch (e) { console.error(e.message, e); diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index c626f73a..9cc89a5d 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -20,7 +20,7 @@ const SystemSettings = { /** A default system prompt that is used when no other system prompt is set or available to the function caller. */ saneDefaultSystemPrompt: "Given the following conversation, relevant context, and a follow up question, reply with an answer to the current question the user is asking. Return only your response to the question given the above information following the users instructions as needed.", - protectedFields: ["multi_user_mode", "hub_api_key"], + protectedFields: ["multi_user_mode", "hub_api_key", "onboarding_complete"], publicFields: [ "footer_data", "support_email", @@ -411,6 +411,28 @@ const SystemSettings = { } }, + isOnboardingComplete: async function () { + try { + const setting = await this.get({ label: "onboarding_complete" }); + return setting?.value === "true"; + } catch (error) { + console.error(error.message); + return false; + } + }, + + markOnboardingComplete: async function () { + try { + await this._updateSettings({ onboarding_complete: true }); + const { Telemetry } = require("./telemetry"); + await Telemetry.sendTelemetry("onboarding_complete"); + return true; + } catch (error) { + console.error(error.message); + return false; + } + }, + currentLogoFilename: async function () { try { const setting = await this.get({ label: "logo_filename" }); diff --git a/server/utils/boot/index.js b/server/utils/boot/index.js index 3a1d1e17..ee853da9 100644 --- a/server/utils/boot/index.js +++ b/server/utils/boot/index.js @@ -4,6 +4,7 @@ const { EncryptionManager } = require("../EncryptionManager"); const { CommunicationKey } = require("../comKey"); const setupTelemetry = require("../telemetry"); const eagerLoadContextWindows = require("./eagerLoadContextWindows"); +const markOnboarded = require("./markOnboarded"); // Testing SSL? You can make a self signed certificate and point the ENVs to that location // make a directory in server called 'sslcert' - cd into it @@ -28,6 +29,7 @@ function bootSSL(app, port = 3001) { server .listen(port, async () => { + await markOnboarded(); await setupTelemetry(); new CommunicationKey(true); new EncryptionManager(); @@ -58,6 +60,7 @@ function bootHTTP(app, port = 3001) { app .listen(port, async () => { + await markOnboarded(); await setupTelemetry(); new CommunicationKey(true); new EncryptionManager(); diff --git a/server/utils/boot/markOnboarded.js b/server/utils/boot/markOnboarded.js new file mode 100644 index 00000000..9d1f6166 --- /dev/null +++ b/server/utils/boot/markOnboarded.js @@ -0,0 +1,52 @@ +const { SystemSettings } = require("../../models/systemSettings"); + +/** + * Mark the onboarding as completed for legacy users prior to this change where onboarding is now a flag in the DB. + * This is a legacy patch to ensure that existing users are not redirected to the onboarding page who have been using the app for a while. + */ +async function markOnboarded() { + try { + const onboardingStatus = await SystemSettings.isOnboardingComplete(); + if (onboardingStatus === true) return; + + // Check if the server is already onboarded by the old way of checking if the server in any way has been setup. + // If it is, then we can mark the onboarding as complete in the DB to persist this + const alreadyOnboarded = await isLegacyOnboarded(); + if (alreadyOnboarded === true) { + console.log( + "\x1b[33m[ONBOARDING PATCH]\x1b[0m Legacy instance is already onboarded, marking onboarding as complete. You will not see this message again." + ); + await SystemSettings.markOnboardingComplete(); + return true; + } + return false; + } catch (e) { + console.error( + "\x1b[31m[ONBOARDING PATCH]\x1b[0m Error marking onboarding as complete", + e.message, + e + ); + return false; + } +} + +/** + * Check if the server is already onboarded by the old way of checking if the server in any way has been setup. + * @returns {Promise} + */ +async function isLegacyOnboarded() { + // LLM Provider is set, so we can assume onboarding is complete since this is default null in SystemSettings.js + if (!!process.env.LLM_PROVIDER) return true; + + // Vector DB is set, so we can assume onboarding is complete since this is default null in SystemSettings.js (default is lancedb in frontend) + if (!!process.env.VECTOR_DB) return true; + + // Check if the AUTH_TOKEN/JWT_SECRET is set, so we can assume onboarding is complete since this is default null in SystemSettings.js + if (!!process.env.AUTH_TOKEN || !!process.env.JWT_SECRET) return true; + + // Check multi-user mode is enabled, if it is, then they are already using the app. + if ((await SystemSettings.isMultiUserMode()) === true) return true; + return false; +} + +module.exports = markOnboarded;