diff --git a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx index 938a03992b..e288c62b5d 100644 --- a/web/app/(commonLayout)/explore/installed/[appId]/page.tsx +++ b/web/app/(commonLayout)/explore/installed/[appId]/page.tsx @@ -1,16 +1,18 @@ -import type { FC } from 'react' import React from 'react' import Main from '@/app/components/explore/installed-app' export type IInstalledAppProps = { - params: Promise<{ + params: { appId: string - }> + } } -const InstalledApp: FC = async ({ params }) => { +// Using Next.js page convention for async server components +async function InstalledApp({ params }: IInstalledAppProps) { + const appId = (await params).appId return ( -
+
) } -export default React.memo(InstalledApp) + +export default InstalledApp diff --git a/web/app/(shareLayout)/chat/[token]/page.tsx b/web/app/(shareLayout)/chat/[token]/page.tsx index 640c40378f..8ce67585f0 100644 --- a/web/app/(shareLayout)/chat/[token]/page.tsx +++ b/web/app/(shareLayout)/chat/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chat = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/chatbot/[token]/page.tsx b/web/app/(shareLayout)/chatbot/[token]/page.tsx index 6196afecc4..5323d0dacc 100644 --- a/web/app/(shareLayout)/chatbot/[token]/page.tsx +++ b/web/app/(shareLayout)/chatbot/[token]/page.tsx @@ -1,10 +1,13 @@ 'use client' import React from 'react' import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' +import AuthenticatedLayout from '../../components/authenticated-layout' const Chatbot = () => { return ( - + + + ) } diff --git a/web/app/(shareLayout)/completion/[token]/page.tsx b/web/app/(shareLayout)/completion/[token]/page.tsx index e8bc9d79f5..ae91338b9a 100644 --- a/web/app/(shareLayout)/completion/[token]/page.tsx +++ b/web/app/(shareLayout)/completion/[token]/page.tsx @@ -1,9 +1,12 @@ import React from 'react' import Main from '@/app/components/share/text-generation' +import AuthenticatedLayout from '../../components/authenticated-layout' const Completion = () => { return ( -
+ +
+ ) } diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx index 5bf1514774..3a5dc793d6 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -18,11 +18,8 @@ import type { import { noop } from 'lodash-es' export type ChatWithHistoryContextValue = { - appInfoError?: any - appInfoLoading?: boolean - appMeta?: AppMeta - appData?: AppData - userCanAccess?: boolean + appMeta?: AppMeta | null + appData?: AppData | null appParams?: ChatConfig appChatListDataLoading?: boolean currentConversationId: string @@ -62,7 +59,6 @@ export type ChatWithHistoryContextValue = { } export const ChatWithHistoryContext = createContext({ - userCanAccess: false, currentConversationId: '', appPrevChatTree: [], pinnedConversationList: [], diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 32f74e6457..be935a70ba 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -21,9 +21,6 @@ import { addFileInfos, sortAgentSorts } from '../../../tools/utils' import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' import { delConversation, - fetchAppInfo, - fetchAppMeta, - fetchAppParams, fetchChatList, fetchConversations, generationConversationName, @@ -43,8 +40,7 @@ import { useAppFavicon } from '@/hooks/use-app-favicon' import { InputVarType } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import { noop } from 'lodash-es' -import { useGetUserCanAccessApp } from '@/service/access-control' -import { useGlobalPublicStore } from '@/context/global-public-context' +import { useWebAppStore } from '@/context/web-app-context' function getFormattedChatList(messages: any[]) { const newChatList: ChatItem[] = [] @@ -74,13 +70,9 @@ function getFormattedChatList(messages: any[]) { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) - const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) - const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) - const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ - appId: installedAppInfo?.app.id || appInfo?.app_id, - isInstalledApp, - enabled: systemFeatures.webapp_auth.enabled, - }) + const appInfo = useWebAppStore(s => s.appInfo) + const appParams = useWebAppStore(s => s.appParams) + const appMeta = useWebAppStore(s => s.appMeta) useAppFavicon({ enable: !installedAppInfo, @@ -107,6 +99,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { use_icon_as_answer_icon: app.use_icon_as_answer_icon, }, plan: 'basic', + custom_config: null, } as AppData } @@ -166,8 +159,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { return currentConversationId }, [currentConversationId, newConversationId]) - const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId)) - const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId)) const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100)) const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100)) const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) @@ -485,9 +476,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { }, [isInstalledApp, appId, t, notify]) return { - appInfoError, - appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), - userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, isInstalledApp, appId, currentConversationId, diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx index fe8e7b430d..cceb21b295 100644 --- a/web/app/components/base/chat/chat-with-history/index.tsx +++ b/web/app/components/base/chat/chat-with-history/index.tsx @@ -1,7 +1,6 @@ 'use client' import type { FC } from 'react' import { - useCallback, useEffect, useState, } from 'react' @@ -19,12 +18,10 @@ import ChatWrapper from './chat-wrapper' import type { InstalledApp } from '@/models/explore' import Loading from '@/app/components/base/loading' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' +import { checkOrSetAccessToken } from '@/app/components/share/utils' import AppUnavailable from '@/app/components/base/app-unavailable' import cn from '@/utils/classnames' import useDocumentTitle from '@/hooks/use-document-title' -import { useTranslation } from 'react-i18next' -import { usePathname, useRouter, useSearchParams } from 'next/navigation' type ChatWithHistoryProps = { className?: string @@ -33,16 +30,12 @@ const ChatWithHistory: FC = ({ className, }) => { const { - userCanAccess, - appInfoError, appData, - appInfoLoading, appChatListDataLoading, chatShouldReloadKey, isMobile, themeBuilder, sidebarCollapseState, - isInstalledApp, } = useChatWithHistoryContext() const isSidebarCollapsed = sidebarCollapseState const customConfig = appData?.custom_config @@ -56,41 +49,6 @@ const ChatWithHistory: FC = ({ useDocumentTitle(site?.title || 'Chat') - const { t } = useTranslation() - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (appInfoLoading) { - return ( - - ) - } - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - - ) - } - return (
= ({ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, - userCanAccess, appData, appParams, appMeta, @@ -191,10 +146,7 @@ const ChatWithHistoryWrap: FC = ({ return ( { const { - userCanAccess, isMobile, allowResetChat, - appInfoError, - appInfoLoading, appData, appChatListDataLoading, chatShouldReloadKey, handleNewConversation, themeBuilder, - isInstalledApp, } = useEmbeddedChatbotContext() const { t } = useTranslation() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -55,58 +45,6 @@ const Chatbot = () => { useDocumentTitle(site?.title || 'Chat') - const searchParams = useSearchParams() - const router = useRouter() - const pathname = usePathname() - const getSigninUrl = useCallback(() => { - const params = new URLSearchParams(searchParams) - params.delete('message') - params.set('redirect_url', pathname) - return `/webapp-signin?${params.toString()}` - }, [searchParams, pathname]) - - const backToHome = useCallback(() => { - removeAccessToken() - const url = getSigninUrl() - router.replace(url) - }, [getSigninUrl, router]) - - if (appInfoLoading) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } - - if (!userCanAccess) { - return
- - {!isInstalledApp && {t('common.userProfile.logout')}} -
- } - - if (appInfoError) { - return ( - <> - {!isMobile && } - {isMobile && ( -
-
- -
-
- )} - - ) - } return (
{ const themeBuilder = useThemeContext() const { - appInfoError, - appInfoLoading, appData, userCanAccess, appParams, @@ -200,8 +136,6 @@ const EmbeddedChatbotWrapper = () => { return { } const EmbeddedChatbot = () => { - const [initialized, setInitialized] = useState(false) - const [appUnavailable, setAppUnavailable] = useState(false) - const [isUnknownReason, setIsUnknownReason] = useState(false) - - useAsyncEffect(async () => { - if (!initialized) { - try { - await checkOrSetAccessToken() - } - catch (e: any) { - if (e.status === 404) { - setAppUnavailable(true) - } - else { - setIsUnknownReason(true) - setAppUnavailable(true) - } - } - setInitialized(true) - } - }, []) - - if (!initialized) - return null - - if (appUnavailable) - return - return } diff --git a/web/app/components/explore/installed-app/index.tsx b/web/app/components/explore/installed-app/index.tsx index 71013fc2e1..212b44934a 100644 --- a/web/app/components/explore/installed-app/index.tsx +++ b/web/app/components/explore/installed-app/index.tsx @@ -1,11 +1,17 @@ 'use client' import type { FC } from 'react' +import { useEffect } from 'react' import React from 'react' import { useContext } from 'use-context-selector' import ExploreContext from '@/context/explore-context' import TextGenerationApp from '@/app/components/share/text-generation' import Loading from '@/app/components/base/loading' import ChatWithHistory from '@/app/components/base/chat/chat-with-history' +import { useWebAppStore } from '@/context/web-app-context' +import AppUnavailable from '../../base/app-unavailable' +import { useGetUserCanAccessApp } from '@/service/access-control' +import { useGetInstalledAppAccessModeByAppId, useGetInstalledAppMeta, useGetInstalledAppParams } from '@/service/use-explore' +import type { AppData } from '@/models/share' export type IInstalledAppProps = { id: string @@ -15,14 +21,79 @@ const InstalledApp: FC = ({ id, }) => { const { installedApps } = useContext(ExploreContext) + const updateAppInfo = useWebAppStore(s => s.updateAppInfo) const installedApp = installedApps.find(item => item.id === id) + const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode) + const updateAppParams = useWebAppStore(s => s.updateAppParams) + const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) + const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) + const { isFetching: isFetchingWebAppAccessMode, data: webAppAccessMode, error: webAppAccessModeError } = useGetInstalledAppAccessModeByAppId(installedApp?.id ?? null) + const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetInstalledAppParams(installedApp?.id ?? null) + const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetInstalledAppMeta(installedApp?.id ?? null) + const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: installedApp?.app.id, isInstalledApp: true }) - if (!installedApp) { - return ( -
- -
- ) + useEffect(() => { + if (!installedApp) { + updateAppInfo(null) + } + else { + const { id, app } = installedApp + updateAppInfo({ + app_id: id, + site: { + title: app.name, + icon_type: app.icon_type, + icon: app.icon, + icon_background: app.icon_background, + icon_url: app.icon_url, + prompt_public: false, + copyright: '', + show_workflow_steps: true, + use_icon_as_answer_icon: app.use_icon_as_answer_icon, + }, + plan: 'basic', + custom_config: null, + } as AppData) + } + + if (appParams) + updateAppParams(appParams) + if (appMeta) + updateWebAppMeta(appMeta) + if (webAppAccessMode) + updateWebAppAccessMode(webAppAccessMode.accessMode) + updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) + }, [installedApp, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp, webAppAccessMode, updateWebAppAccessMode]) + + if (appParamsError) { + return
+ +
+ } + if (appMetaError) { + return
+ +
+ } + if (useCanAccessAppError) { + return
+ +
+ } + if (webAppAccessModeError) { + return
+ +
+ } + if (userCanAccessApp && !userCanAccessApp.result) { + return
+ +
+ } + if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || !installedApp) { + return
+ +
} return ( diff --git a/web/app/components/share/text-generation/index.tsx b/web/app/components/share/text-generation/index.tsx index 9b2d2d38a6..ae6e733e49 100644 --- a/web/app/components/share/text-generation/index.tsx +++ b/web/app/components/share/text-generation/index.tsx @@ -101,16 +101,16 @@ const TextGeneration: FC = ({ // save message const [savedMessages, setSavedMessages] = useState([]) const fetchSavedMessage = useCallback(async () => { - const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) + const res: any = await doFetchSavedMessage(isInstalledApp, appId) setSavedMessages(res.data) - }, [isInstalledApp, installedAppInfo?.id]) + }, [isInstalledApp, appId]) const handleSaveMessage = async (messageId: string) => { - await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) + await saveMessage(messageId, isInstalledApp, appId) notify({ type: 'success', message: t('common.api.saved') }) fetchSavedMessage() } const handleRemoveSavedMessage = async (messageId: string) => { - await removeMessage(messageId, isInstalledApp, installedAppInfo?.id) + await removeMessage(messageId, isInstalledApp, appId) notify({ type: 'success', message: t('common.api.remove') }) fetchSavedMessage() } diff --git a/web/models/share.ts b/web/models/share.ts index b4fde6d89d..1e3b6d6bb7 100644 --- a/web/models/share.ts +++ b/web/models/share.ts @@ -35,7 +35,7 @@ export type AppMeta = { export type AppData = { app_id: string can_replace_logo?: boolean - custom_config: Record + custom_config: Record | null enable_site?: boolean end_user_id?: string site: SiteInfo diff --git a/web/service/access-control.ts b/web/service/access-control.ts index 07984bd57c..d4cc9eb792 100644 --- a/web/service/access-control.ts +++ b/web/service/access-control.ts @@ -1,6 +1,6 @@ import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { get, post } from './base' -import { getAppAccessMode, getUserCanAccess } from './share' +import { getUserCanAccess } from './share' import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' import type { App } from '@/types/app' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -70,16 +70,6 @@ export const useUpdateAccessMode = () => { }) } -export const useGetAppAccessMode = ({ appId, isInstalledApp = true, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { - return useQuery({ - queryKey: [NAME_SPACE, 'app-access-mode', appId], - queryFn: () => getAppAccessMode(appId!, isInstalledApp), - enabled: !!appId && enabled, - staleTime: 0, - gcTime: 0, - }) -} - export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean; }) => { const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) return useQuery({ diff --git a/web/service/explore.ts b/web/service/explore.ts index e9e17416d1..6a440d7f5d 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -1,5 +1,6 @@ import { del, get, patch, post } from './base' import type { App, AppCategory } from '@/models/explore' +import type { AccessMode } from '@/models/access-control' export const fetchAppList = () => { return get<{ @@ -39,3 +40,7 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { export const getToolProviders = () => { return get('/workspaces/current/tool-providers') } + +export const getAppAccessModeByAppId = (appId: string) => { + return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) +} diff --git a/web/service/share.ts b/web/service/share.ts index 6a2a7e5b16..8c33b85522 100644 --- a/web/service/share.ts +++ b/web/service/share.ts @@ -296,13 +296,6 @@ export const fetchAccessToken = async ({ appCode, userId, webAppAccessToken }: { return get(url, { headers }) as Promise<{ access_token: string }> } -export const getAppAccessMode = (appId: string, isInstalledApp: boolean) => { - if (isInstalledApp) - return consoleGet<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) - - return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appId=${appId}`) -} - export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { if (isInstalledApp) return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts new file mode 100644 index 0000000000..1c892a041c --- /dev/null +++ b/web/service/use-explore.ts @@ -0,0 +1,50 @@ +import { useGlobalPublicStore } from '@/context/global-public-context' +import { AccessMode } from '@/models/access-control' +import { useQuery } from '@tanstack/react-query' +import { getAppAccessModeByAppId } from './explore' +import { fetchAppMeta, fetchAppParams } from './share' + +const NAME_SPACE = 'explore' + +export const useGetInstalledAppAccessModeByAppId = (appId: string | null) => { + const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) + return useQuery({ + queryKey: [NAME_SPACE, 'appAccessMode', appId], + queryFn: () => { + if (systemFeatures.webapp_auth.enabled === false) { + return { + accessMode: AccessMode.PUBLIC, + } + } + if (!appId || appId.length === 0) + return Promise.reject(new Error('App code is required to get access mode')) + + return getAppAccessModeByAppId(appId) + }, + enabled: !!appId, + }) +} + +export const useGetInstalledAppParams = (appId: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appParams', appId], + queryFn: () => { + if (!appId || appId.length === 0) + return Promise.reject(new Error('App ID is required to get app params')) + return fetchAppParams(true, appId) + }, + enabled: !!appId, + }) +} + +export const useGetInstalledAppMeta = (appId: string | null) => { + return useQuery({ + queryKey: [NAME_SPACE, 'appMeta', appId], + queryFn: () => { + if (!appId || appId.length === 0) + return Promise.reject(new Error('App ID is required to get app meta')) + return fetchAppMeta(true, appId) + }, + enabled: !!appId, + }) +}