diff --git a/frontend/__tests__/routes/_oh.test.tsx b/frontend/__tests__/routes/_oh.test.tsx index 9dbf3b786f2..a5e106f1fc9 100644 --- a/frontend/__tests__/routes/_oh.test.tsx +++ b/frontend/__tests__/routes/_oh.test.tsx @@ -3,7 +3,7 @@ import { createRemixStub } from "@remix-run/testing"; import { screen, waitFor, within } from "@testing-library/react"; import { renderWithProviders } from "test-utils"; import userEvent from "@testing-library/user-event"; -import MainApp from "#/routes/_oh"; +import MainApp from "#/routes/_oh/route"; import * as CaptureConsent from "#/utils/handle-capture-consent"; import i18n from "#/i18n"; diff --git a/frontend/src/components/modals/account-settings-modal.tsx b/frontend/src/components/modals/account-settings-form.tsx similarity index 96% rename from frontend/src/components/modals/account-settings-modal.tsx rename to frontend/src/components/modals/account-settings-form.tsx index 59b25cc0de9..3b41ab4332f 100644 --- a/frontend/src/components/modals/account-settings-modal.tsx +++ b/frontend/src/components/modals/account-settings-form.tsx @@ -14,19 +14,19 @@ import { useAuth } from "#/context/auth-context"; import { useUserPrefs } from "#/context/user-prefs-context"; import { handleCaptureConsent } from "#/utils/handle-capture-consent"; -interface AccountSettingsModalProps { +interface AccountSettingsFormProps { onClose: () => void; selectedLanguage: string; gitHubError: boolean; analyticsConsent: string | null; } -function AccountSettingsModal({ +export function AccountSettingsForm({ onClose, selectedLanguage, gitHubError, analyticsConsent, -}: AccountSettingsModalProps) { +}: AccountSettingsFormProps) { const { gitHubToken, setGitHubToken, logout } = useAuth(); const { saveSettings } = useUserPrefs(); const { t } = useTranslation(); @@ -136,5 +136,3 @@ function AccountSettingsModal({ ); } - -export default AccountSettingsModal; diff --git a/frontend/src/routes/_oh.tsx b/frontend/src/routes/_oh.tsx deleted file mode 100644 index 9a37b8b4d82..00000000000 --- a/frontend/src/routes/_oh.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import React from "react"; -import { - useRouteError, - isRouteErrorResponse, - useLocation, - Outlet, -} from "@remix-run/react"; -import { useDispatch } from "react-redux"; -import CogTooth from "#/assets/cog-tooth"; -import { SettingsForm } from "#/components/form/settings-form"; -import AccountSettingsModal from "#/components/modals/account-settings-modal"; -import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal"; -import { LoadingSpinner } from "#/components/modals/loading-project"; -import { ModalBackdrop } from "#/components/modals/modal-backdrop"; -import { UserActions } from "#/components/user-actions"; -import i18n from "#/i18n"; -import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react"; -import NewProjectIcon from "#/icons/new-project.svg?react"; -import DocsIcon from "#/icons/docs.svg?react"; -import { WaitlistModal } from "#/components/waitlist-modal"; -import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal"; -import { setCurrentAgentState } from "#/state/agent-slice"; -import AgentState from "#/types/agent-state"; -import { useConfig } from "#/hooks/query/use-config"; -import { useGitHubUser } from "#/hooks/query/use-github-user"; -import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url"; -import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options"; -import { useIsAuthed } from "#/hooks/query/use-is-authed"; -import { useAuth } from "#/context/auth-context"; -import { useEndSession } from "#/hooks/use-end-session"; -import { useUserPrefs } from "#/context/user-prefs-context"; - -export function ErrorBoundary() { - const error = useRouteError(); - - if (isRouteErrorResponse(error)) { - return ( -
-

{error.status}

-

{error.statusText}

-
-          {error.data instanceof Object
-            ? JSON.stringify(error.data)
-            : error.data}
-        
-
- ); - } - if (error instanceof Error) { - return ( -
-

Uh oh, an error occurred!

-
{error.message}
-
- ); - } - - return ( -
-

Uh oh, an unknown error occurred!

-
- ); -} - -export default function MainApp() { - const { token, gitHubToken, clearToken, logout } = useAuth(); - const { settings, settingsAreUpToDate } = useUserPrefs(); - - const location = useLocation(); - const dispatch = useDispatch(); - const endSession = useEndSession(); - - // FIXME: Bad practice to use localStorage directly - const analyticsConsent = localStorage.getItem("analytics-consent"); - - const [accountSettingsModalOpen, setAccountSettingsModalOpen] = - React.useState(false); - const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false); - const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] = - React.useState(false); - const [consentFormIsOpen, setConsentFormIsOpen] = React.useState( - !localStorage.getItem("analytics-consent"), - ); - - const config = useConfig(); - const user = useGitHubUser(); - const { - data: isAuthed, - isFetched, - isFetching: isFetchingAuth, - } = useIsAuthed(); - const aiConfigOptions = useAIConfigOptions(); - - const gitHubAuthUrl = useGitHubAuthUrl({ - gitHubToken, - appMode: config.data?.APP_MODE || null, - gitHubClientId: config.data?.GITHUB_CLIENT_ID || null, - }); - - React.useEffect(() => { - if (isFetched && !isAuthed) clearToken(); - }, [isFetched, isAuthed]); - - React.useEffect(() => { - if (settings.LANGUAGE) { - i18n.changeLanguage(settings.LANGUAGE); - } - }, [settings.LANGUAGE]); - - React.useEffect(() => { - // If the github token is invalid, open the account settings modal again - if (user.isError) { - setAccountSettingsModalOpen(true); - } - }, [user.isError]); - - const handleAccountSettingsModalClose = () => { - // If the user closes the modal without connecting to GitHub, - // we need to log them out to clear the invalid token from the - // local storage - if (user.isError) logout(); - setAccountSettingsModalOpen(false); - }; - - const handleEndSession = () => { - setStartNewProjectModalIsOpen(false); - dispatch(setCurrentAgentState(AgentState.LOADING)); - endSession(); - }; - - return ( -
- -
- -
- - {isAuthed && (!settingsAreUpToDate || settingsModalIsOpen) && ( - setSettingsModalIsOpen(false)}> -
- {aiConfigOptions.error && ( -

- {aiConfigOptions.error.message} -

- )} - - AI Provider Configuration - -

- To continue, connect an OpenAI, Anthropic, or other LLM account -

-

- Changing settings during an active session will end the session -

- {aiConfigOptions.isLoading && ( -
- -
- )} - {aiConfigOptions.data && ( - { - setSettingsModalIsOpen(false); - }} - /> - )} -
-
- )} - {accountSettingsModalOpen && ( - - - - )} - {startNewProjectModalIsOpen && ( - setStartNewProjectModalIsOpen(false)}> - setStartNewProjectModalIsOpen(false), - }, - }} - /> - - )} - {!isFetchingAuth && !isAuthed && config.data?.APP_MODE === "saas" && ( - - )} - {consentFormIsOpen && ( - setConsentFormIsOpen(false)} - /> - )} -
- ); -} diff --git a/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx b/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx new file mode 100644 index 00000000000..74ab5f3cee0 --- /dev/null +++ b/frontend/src/routes/_oh/buttons/all-hands-logo-button.tsx @@ -0,0 +1,13 @@ +import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react"; + +interface AllHandsLogoButtonProps { + onClick: () => void; +} + +export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) { + return ( + + ); +} diff --git a/frontend/src/routes/_oh/buttons/docs-button.tsx b/frontend/src/routes/_oh/buttons/docs-button.tsx new file mode 100644 index 00000000000..3b805556e3d --- /dev/null +++ b/frontend/src/routes/_oh/buttons/docs-button.tsx @@ -0,0 +1,15 @@ +import DocsIcon from "#/icons/docs.svg?react"; + +export function DocsButton() { + return ( + + + + ); +} diff --git a/frontend/src/routes/_oh/buttons/exit-project-button.tsx b/frontend/src/routes/_oh/buttons/exit-project-button.tsx new file mode 100644 index 00000000000..e187fe99d19 --- /dev/null +++ b/frontend/src/routes/_oh/buttons/exit-project-button.tsx @@ -0,0 +1,18 @@ +import NewProjectIcon from "#/icons/new-project.svg?react"; + +interface ExitProjectButtonProps { + onClick: () => void; +} + +export function ExitProjectButton({ onClick }: ExitProjectButtonProps) { + return ( + + ); +} diff --git a/frontend/src/routes/_oh/buttons/settings-button.tsx b/frontend/src/routes/_oh/buttons/settings-button.tsx new file mode 100644 index 00000000000..3e739bcb698 --- /dev/null +++ b/frontend/src/routes/_oh/buttons/settings-button.tsx @@ -0,0 +1,18 @@ +import CogTooth from "#/assets/cog-tooth"; + +interface SettingsButtonProps { + onClick: () => void; +} + +export function SettingsButton({ onClick }: SettingsButtonProps) { + return ( + + ); +} diff --git a/frontend/src/routes/_oh/modals/account-settings-modal.tsx b/frontend/src/routes/_oh/modals/account-settings-modal.tsx new file mode 100644 index 00000000000..d1568940486 --- /dev/null +++ b/frontend/src/routes/_oh/modals/account-settings-modal.tsx @@ -0,0 +1,27 @@ +import { AccountSettingsForm } from "#/components/modals/account-settings-form"; +import { ModalBackdrop } from "#/components/modals/modal-backdrop"; +import { useUserPrefs } from "#/context/user-prefs-context"; +import { useGitHubUser } from "#/hooks/query/use-github-user"; + +interface AccountSettingsModalProps { + onClose: () => void; +} + +export function AccountSettingsModal({ onClose }: AccountSettingsModalProps) { + const user = useGitHubUser(); + const { settings } = useUserPrefs(); + + // FIXME: Bad practice to use localStorage directly + const analyticsConsent = localStorage.getItem("analytics-consent"); + + return ( + + + + ); +} diff --git a/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx b/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx new file mode 100644 index 00000000000..5d425fd4a6a --- /dev/null +++ b/frontend/src/routes/_oh/modals/exit-project-confirmation-modal.tsx @@ -0,0 +1,42 @@ +import { useDispatch } from "react-redux"; +import { DangerModal } from "#/components/modals/confirmation-modals/danger-modal"; +import { ModalBackdrop } from "#/components/modals/modal-backdrop"; +import { useEndSession } from "#/hooks/use-end-session"; +import { setCurrentAgentState } from "#/state/agent-slice"; +import AgentState from "#/types/agent-state"; + +interface ExitProjectConfirmationModalProps { + onClose: () => void; +} + +export function ExitProjectConfirmationModal({ + onClose, +}: ExitProjectConfirmationModalProps) { + const dispatch = useDispatch(); + const endSession = useEndSession(); + + const handleEndSession = () => { + onClose(); + dispatch(setCurrentAgentState(AgentState.LOADING)); + endSession(); + }; + + return ( + + + + ); +} diff --git a/frontend/src/routes/_oh/modals/settings-modal.tsx b/frontend/src/routes/_oh/modals/settings-modal.tsx new file mode 100644 index 00000000000..d09b73a33eb --- /dev/null +++ b/frontend/src/routes/_oh/modals/settings-modal.tsx @@ -0,0 +1,50 @@ +import { SettingsForm } from "#/components/form/settings-form"; +import { LoadingSpinner } from "#/components/modals/loading-project"; +import { ModalBackdrop } from "#/components/modals/modal-backdrop"; +import { useUserPrefs } from "#/context/user-prefs-context"; +import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options"; + +interface SettingsModalProps { + onClose: () => void; +} + +export function SettingsModal({ onClose }: SettingsModalProps) { + const { settings } = useUserPrefs(); + const aiConfigOptions = useAIConfigOptions(); + + return ( + +
+ {aiConfigOptions.error && ( +

{aiConfigOptions.error.message}

+ )} + + AI Provider Configuration + +

+ To continue, connect an OpenAI, Anthropic, or other LLM account +

+

+ Changing settings during an active session will end the session +

+ {aiConfigOptions.isLoading && ( +
+ +
+ )} + {aiConfigOptions.data && ( + + )} +
+
+ ); +} diff --git a/frontend/src/routes/_oh/route.tsx b/frontend/src/routes/_oh/route.tsx new file mode 100644 index 00000000000..328c271add9 --- /dev/null +++ b/frontend/src/routes/_oh/route.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import { useRouteError, isRouteErrorResponse, Outlet } from "@remix-run/react"; +import i18n from "#/i18n"; +import { WaitlistModal } from "#/components/waitlist-modal"; +import { AnalyticsConsentFormModal } from "#/components/analytics-consent-form-modal"; +import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url"; +import { useIsAuthed } from "#/hooks/query/use-is-authed"; +import { useAuth } from "#/context/auth-context"; +import { useUserPrefs } from "#/context/user-prefs-context"; +import { Sidebar } from "./sidebar"; +import { useConfig } from "#/hooks/query/use-config"; + +export function ErrorBoundary() { + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + return ( +
+

{error.status}

+

{error.statusText}

+
+          {error.data instanceof Object
+            ? JSON.stringify(error.data)
+            : error.data}
+        
+
+ ); + } + if (error instanceof Error) { + return ( +
+

Uh oh, an error occurred!

+
{error.message}
+
+ ); + } + + return ( +
+

Uh oh, an unknown error occurred!

+
+ ); +} + +export default function MainApp() { + const { gitHubToken, clearToken } = useAuth(); + const { settings } = useUserPrefs(); + + const [consentFormIsOpen, setConsentFormIsOpen] = React.useState( + !localStorage.getItem("analytics-consent"), + ); + + const config = useConfig(); + const { + data: isAuthed, + isFetched, + isFetching: isFetchingAuth, + } = useIsAuthed(); + + const gitHubAuthUrl = useGitHubAuthUrl({ + gitHubToken, + appMode: config.data?.APP_MODE || null, + gitHubClientId: config.data?.GITHUB_CLIENT_ID || null, + }); + + React.useEffect(() => { + if (isFetched && !isAuthed) clearToken(); + }, [isFetched, isAuthed]); + + React.useEffect(() => { + if (settings.LANGUAGE) { + i18n.changeLanguage(settings.LANGUAGE); + } + }, [settings.LANGUAGE]); + + const isInWaitlist = + !isFetchingAuth && !isAuthed && config.data?.APP_MODE === "saas"; + + return ( +
+ + +
+ +
+ + {isInWaitlist && ( + + )} + {consentFormIsOpen && ( + setConsentFormIsOpen(false)} + /> + )} +
+ ); +} diff --git a/frontend/src/routes/_oh/sidebar.tsx b/frontend/src/routes/_oh/sidebar.tsx new file mode 100644 index 00000000000..b02974fa9e1 --- /dev/null +++ b/frontend/src/routes/_oh/sidebar.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { useLocation } from "react-router-dom"; +import { LoadingSpinner } from "#/components/modals/loading-project"; +import { UserActions } from "#/components/user-actions"; +import { useAuth } from "#/context/auth-context"; +import { useUserPrefs } from "#/context/user-prefs-context"; +import { useGitHubUser } from "#/hooks/query/use-github-user"; +import { useIsAuthed } from "#/hooks/query/use-is-authed"; +import { SettingsModal } from "./modals/settings-modal"; +import { ExitProjectConfirmationModal } from "./modals/exit-project-confirmation-modal"; +import { AllHandsLogoButton } from "./buttons/all-hands-logo-button"; +import { SettingsButton } from "./buttons/settings-button"; +import { DocsButton } from "./buttons/docs-button"; +import { ExitProjectButton } from "./buttons/exit-project-button"; +import { AccountSettingsModal } from "./modals/account-settings-modal"; + +export function Sidebar() { + const location = useLocation(); + + const user = useGitHubUser(); + const { data: isAuthed } = useIsAuthed(); + + const { token, logout } = useAuth(); + const { settingsAreUpToDate } = useUserPrefs(); + + const [accountSettingsModalOpen, setAccountSettingsModalOpen] = + React.useState(false); + const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false); + const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] = + React.useState(false); + + React.useEffect(() => { + // If the github token is invalid, open the account settings modal again + if (user.isError) { + setAccountSettingsModalOpen(true); + } + }, [user.isError]); + + const handleAccountSettingsModalClose = () => { + // If the user closes the modal without connecting to GitHub, + // we need to log them out to clear the invalid token from the + // local storage + if (user.isError) logout(); + setAccountSettingsModalOpen(false); + }; + + const handleClickLogo = () => { + if (location.pathname.startsWith("/app")) + setStartNewProjectModalIsOpen(true); + }; + + const showSettingsModal = + isAuthed && (!settingsAreUpToDate || settingsModalIsOpen); + + return ( + <> + + {accountSettingsModalOpen && ( + + )} + {showSettingsModal && ( + setSettingsModalIsOpen(false)} /> + )} + {startNewProjectModalIsOpen && ( + setStartNewProjectModalIsOpen(false)} + /> + )} + + ); +} diff --git a/frontend/src/sessions.ts b/frontend/src/sessions.ts deleted file mode 100644 index 612be965157..00000000000 --- a/frontend/src/sessions.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createCookieSessionStorage } from "@remix-run/node"; -import { Settings } from "./services/settings"; - -type SessionData = { - tosAccepted: boolean; - ghToken: string; - token: string; // Session token -}; - -export const { getSession, commitSession, destroySession } = - createCookieSessionStorage({ - cookie: { - name: "__session", - secrets: ["some_secret"], - }, - }); - -type SettingsSessionData = { settings: Settings }; - -export const { - getSession: getSettingsSession, - commitSession: commitSettingsSession, - destroySession: destroySettingsSession, -} = createCookieSessionStorage({ - cookie: { - name: "__settings", - secrets: ["some_other_secret"], - }, -});