| import type { FC } from 'react' | |||||
| import React from 'react' | import React from 'react' | ||||
| import Main from '@/app/components/explore/installed-app' | import Main from '@/app/components/explore/installed-app' | ||||
| export type IInstalledAppProps = { | export type IInstalledAppProps = { | ||||
| params: Promise<{ | |||||
| params: { | |||||
| appId: string | appId: string | ||||
| }> | |||||
| } | |||||
| } | } | ||||
| const InstalledApp: FC<IInstalledAppProps> = async ({ params }) => { | |||||
| // Using Next.js page convention for async server components | |||||
| async function InstalledApp({ params }: IInstalledAppProps) { | |||||
| const appId = (await params).appId | |||||
| return ( | return ( | ||||
| <Main id={(await params).appId} /> | |||||
| <Main id={appId} /> | |||||
| ) | ) | ||||
| } | } | ||||
| export default React.memo(InstalledApp) | |||||
| export default InstalledApp |
| 'use client' | 'use client' | ||||
| import React from 'react' | import React from 'react' | ||||
| import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' | import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history' | ||||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||||
| const Chat = () => { | const Chat = () => { | ||||
| return ( | return ( | ||||
| <ChatWithHistoryWrap /> | |||||
| <AuthenticatedLayout> | |||||
| <ChatWithHistoryWrap /> | |||||
| </AuthenticatedLayout> | |||||
| ) | ) | ||||
| } | } | ||||
| 'use client' | 'use client' | ||||
| import React from 'react' | import React from 'react' | ||||
| import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' | import EmbeddedChatbot from '@/app/components/base/chat/embedded-chatbot' | ||||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||||
| const Chatbot = () => { | const Chatbot = () => { | ||||
| return ( | return ( | ||||
| <EmbeddedChatbot /> | |||||
| <AuthenticatedLayout> | |||||
| <EmbeddedChatbot /> | |||||
| </AuthenticatedLayout> | |||||
| ) | ) | ||||
| } | } | ||||
| import React from 'react' | import React from 'react' | ||||
| import Main from '@/app/components/share/text-generation' | import Main from '@/app/components/share/text-generation' | ||||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||||
| const Completion = () => { | const Completion = () => { | ||||
| return ( | return ( | ||||
| <Main /> | |||||
| <AuthenticatedLayout> | |||||
| <Main /> | |||||
| </AuthenticatedLayout> | |||||
| ) | ) | ||||
| } | } | ||||
| 'use client' | |||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||||
| import Loading from '@/app/components/base/loading' | |||||
| import { removeAccessToken } from '@/app/components/share/utils' | |||||
| import { useWebAppStore } from '@/context/web-app-context' | |||||
| import { useGetUserCanAccessApp } from '@/service/access-control' | |||||
| import { useGetWebAppInfo, useGetWebAppMeta, useGetWebAppParams } from '@/service/use-share' | |||||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||||
| import React, { useCallback, useEffect } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => { | |||||
| const { t } = useTranslation() | |||||
| const updateAppInfo = useWebAppStore(s => s.updateAppInfo) | |||||
| const updateAppParams = useWebAppStore(s => s.updateAppParams) | |||||
| const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta) | |||||
| const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp) | |||||
| const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams() | |||||
| const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo() | |||||
| const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta() | |||||
| const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false }) | |||||
| useEffect(() => { | |||||
| if (appInfo) | |||||
| updateAppInfo(appInfo) | |||||
| if (appParams) | |||||
| updateAppParams(appParams) | |||||
| if (appMeta) | |||||
| updateWebAppMeta(appMeta) | |||||
| updateUserCanAccessApp(Boolean(userCanAccessApp && userCanAccessApp?.result)) | |||||
| }, [appInfo, appMeta, appParams, updateAppInfo, updateAppParams, updateUserCanAccessApp, updateWebAppMeta, userCanAccessApp]) | |||||
| const router = useRouter() | |||||
| const pathname = usePathname() | |||||
| const searchParams = useSearchParams() | |||||
| 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 (appInfoError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={appInfoError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (appParamsError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={appParamsError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (appMetaError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={appMetaError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (useCanAccessAppError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={useCanAccessAppError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (userCanAccessApp && !userCanAccessApp.result) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-2'> | |||||
| <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> | |||||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span> | |||||
| </div> | |||||
| } | |||||
| if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| return <>{children}</> | |||||
| } | |||||
| export default React.memo(AuthenticatedLayout) |
| 'use client' | |||||
| import type { FC, PropsWithChildren } from 'react' | |||||
| import { useEffect } from 'react' | |||||
| import { useCallback } from 'react' | |||||
| import { useWebAppStore } from '@/context/web-app-context' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||||
| import { checkOrSetAccessToken, removeAccessToken, setAccessToken } from '@/app/components/share/utils' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { fetchAccessToken } from '@/service/share' | |||||
| import Loading from '@/app/components/base/loading' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| const Splash: FC<PropsWithChildren> = ({ children }) => { | |||||
| const { t } = useTranslation() | |||||
| const shareCode = useWebAppStore(s => s.shareCode) | |||||
| const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) | |||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter() | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const tokenFromUrl = searchParams.get('web_sso_token') | |||||
| const message = searchParams.get('message') | |||||
| const code = searchParams.get('code') | |||||
| const getSigninUrl = useCallback(() => { | |||||
| const params = new URLSearchParams(searchParams) | |||||
| params.delete('message') | |||||
| params.delete('code') | |||||
| return `/webapp-signin?${params.toString()}` | |||||
| }, [searchParams]) | |||||
| const backToHome = useCallback(() => { | |||||
| removeAccessToken() | |||||
| const url = getSigninUrl() | |||||
| router.replace(url) | |||||
| }, [getSigninUrl, router]) | |||||
| useEffect(() => { | |||||
| (async () => { | |||||
| if (message) | |||||
| return | |||||
| if (shareCode && tokenFromUrl && redirectUrl) { | |||||
| localStorage.setItem('webapp_access_token', tokenFromUrl) | |||||
| const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: tokenFromUrl }) | |||||
| await setAccessToken(shareCode, tokenResp.access_token) | |||||
| router.replace(decodeURIComponent(redirectUrl)) | |||||
| return | |||||
| } | |||||
| if (shareCode && redirectUrl && localStorage.getItem('webapp_access_token')) { | |||||
| const tokenResp = await fetchAccessToken({ appCode: shareCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) | |||||
| await setAccessToken(shareCode, tokenResp.access_token) | |||||
| router.replace(decodeURIComponent(redirectUrl)) | |||||
| return | |||||
| } | |||||
| if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { | |||||
| await checkOrSetAccessToken(shareCode) | |||||
| router.replace(decodeURIComponent(redirectUrl)) | |||||
| } | |||||
| })() | |||||
| }, [shareCode, redirectUrl, router, tokenFromUrl, message, webAppAccessMode]) | |||||
| if (message) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | |||||
| <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} /> | |||||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span> | |||||
| </div> | |||||
| } | |||||
| if (tokenFromUrl) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| if (webAppAccessMode === AccessMode.PUBLIC && redirectUrl) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| return <>{children}</> | |||||
| } | |||||
| export default Splash |
| 'use client' | |||||
| import React, { useEffect, useState } from 'react' | |||||
| import type { FC } from 'react' | |||||
| import { usePathname, useSearchParams } from 'next/navigation' | |||||
| import Loading from '../components/base/loading' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| import { getAppAccessModeByAppCode } from '@/service/share' | |||||
| import type { FC, PropsWithChildren } from 'react' | |||||
| import WebAppStoreProvider from '@/context/web-app-context' | |||||
| import Splash from './components/splash' | |||||
| const Layout: FC<{ | |||||
| children: React.ReactNode | |||||
| }> = ({ children }) => { | |||||
| const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending) | |||||
| const setWebAppAccessMode = useGlobalPublicStore(s => s.setWebAppAccessMode) | |||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||||
| const pathname = usePathname() | |||||
| const searchParams = useSearchParams() | |||||
| const redirectUrl = searchParams.get('redirect_url') | |||||
| const [isLoading, setIsLoading] = useState(true) | |||||
| useEffect(() => { | |||||
| (async () => { | |||||
| if (!isGlobalPending && !systemFeatures.webapp_auth.enabled) { | |||||
| setIsLoading(false) | |||||
| return | |||||
| } | |||||
| let appCode: string | null = null | |||||
| if (redirectUrl) { | |||||
| const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) | |||||
| appCode = url.pathname.split('/').pop() || null | |||||
| } | |||||
| else { | |||||
| appCode = pathname.split('/').pop() || null | |||||
| } | |||||
| if (!appCode) | |||||
| return | |||||
| setIsLoading(true) | |||||
| const ret = await getAppAccessModeByAppCode(appCode) | |||||
| setWebAppAccessMode(ret?.accessMode || AccessMode.PUBLIC) | |||||
| setIsLoading(false) | |||||
| })() | |||||
| }, [pathname, redirectUrl, setWebAppAccessMode, isGlobalPending, systemFeatures.webapp_auth.enabled]) | |||||
| if (isLoading || isGlobalPending) { | |||||
| return <div className='flex h-full w-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| const Layout: FC<PropsWithChildren> = ({ children }) => { | |||||
| return ( | return ( | ||||
| <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> | <div className="h-full min-w-[300px] pb-[env(safe-area-inset-bottom)]"> | ||||
| {children} | |||||
| <WebAppStoreProvider> | |||||
| <Splash> | |||||
| {children} | |||||
| </Splash> | |||||
| </WebAppStoreProvider> | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import useDocumentTitle from '@/hooks/use-document-title' | import useDocumentTitle from '@/hooks/use-document-title' | ||||
| import type { PropsWithChildren } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| export default function SignInLayout({ children }: any) { | |||||
| const { systemFeatures } = useGlobalPublicStore() | |||||
| useDocumentTitle('') | |||||
| export default function SignInLayout({ children }: PropsWithChildren) { | |||||
| const { t } = useTranslation() | |||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||||
| useDocumentTitle(t('login.webapp.login')) | |||||
| return <> | return <> | ||||
| <div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}> | <div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}> | ||||
| <div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}> | <div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}> |
| 'use client' | |||||
| import React, { useCallback, useEffect, useState } from 'react' | import React, { useCallback, useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import Link from 'next/link' | import Link from 'next/link' |
| 'use client' | 'use client' | ||||
| import { useRouter, useSearchParams } from 'next/navigation' | import { useRouter, useSearchParams } from 'next/navigation' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useCallback, useEffect } from 'react' | |||||
| import React, { useCallback } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import Toast from '@/app/components/base/toast' | |||||
| import { removeAccessToken, setAccessToken } from '@/app/components/share/utils' | |||||
| import { removeAccessToken } from '@/app/components/share/utils' | |||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import Loading from '@/app/components/base/loading' | |||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | import AppUnavailable from '@/app/components/base/app-unavailable' | ||||
| import NormalForm from './normalForm' | import NormalForm from './normalForm' | ||||
| import { AccessMode } from '@/models/access-control' | import { AccessMode } from '@/models/access-control' | ||||
| import ExternalMemberSsoAuth from './components/external-member-sso-auth' | import ExternalMemberSsoAuth from './components/external-member-sso-auth' | ||||
| import { fetchAccessToken } from '@/service/share' | |||||
| import { useWebAppStore } from '@/context/web-app-context' | |||||
| const WebSSOForm: FC = () => { | const WebSSOForm: FC = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | ||||
| const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) | |||||
| const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) | |||||
| const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
| const router = useRouter() | const router = useRouter() | ||||
| const redirectUrl = searchParams.get('redirect_url') | const redirectUrl = searchParams.get('redirect_url') | ||||
| const tokenFromUrl = searchParams.get('web_sso_token') | |||||
| const message = searchParams.get('message') | |||||
| const code = searchParams.get('code') | |||||
| const getSigninUrl = useCallback(() => { | const getSigninUrl = useCallback(() => { | ||||
| const params = new URLSearchParams(searchParams) | |||||
| params.delete('message') | |||||
| params.delete('code') | |||||
| const params = new URLSearchParams() | |||||
| params.append('redirect_url', redirectUrl || '') | |||||
| return `/webapp-signin?${params.toString()}` | return `/webapp-signin?${params.toString()}` | ||||
| }, [searchParams]) | |||||
| }, [redirectUrl]) | |||||
| const backToHome = useCallback(() => { | const backToHome = useCallback(() => { | ||||
| removeAccessToken() | removeAccessToken() | ||||
| router.replace(url) | router.replace(url) | ||||
| }, [getSigninUrl, router]) | }, [getSigninUrl, router]) | ||||
| const showErrorToast = (msg: string) => { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: msg, | |||||
| }) | |||||
| } | |||||
| const getAppCodeFromRedirectUrl = useCallback(() => { | |||||
| if (!redirectUrl) | |||||
| return null | |||||
| const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) | |||||
| const appCode = url.pathname.split('/').pop() | |||||
| if (!appCode) | |||||
| return null | |||||
| return appCode | |||||
| }, [redirectUrl]) | |||||
| useEffect(() => { | |||||
| (async () => { | |||||
| if (message) | |||||
| return | |||||
| const appCode = getAppCodeFromRedirectUrl() | |||||
| if (appCode && tokenFromUrl && redirectUrl) { | |||||
| localStorage.setItem('webapp_access_token', tokenFromUrl) | |||||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: tokenFromUrl }) | |||||
| await setAccessToken(appCode, tokenResp.access_token) | |||||
| router.replace(decodeURIComponent(redirectUrl)) | |||||
| return | |||||
| } | |||||
| if (appCode && redirectUrl && localStorage.getItem('webapp_access_token')) { | |||||
| const tokenResp = await fetchAccessToken({ appCode, webAppAccessToken: localStorage.getItem('webapp_access_token') }) | |||||
| await setAccessToken(appCode, tokenResp.access_token) | |||||
| router.replace(decodeURIComponent(redirectUrl)) | |||||
| } | |||||
| })() | |||||
| }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl, message]) | |||||
| useEffect(() => { | |||||
| if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC && redirectUrl) | |||||
| router.replace(decodeURIComponent(redirectUrl)) | |||||
| }, [webAppAccessMode, router, redirectUrl]) | |||||
| if (tokenFromUrl) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| if (message) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-4'> | |||||
| <AppUnavailable className='h-auto w-auto' code={code || t('share.common.appUnavailable')} unknownReason={message} /> | |||||
| <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{code === '403' ? t('common.userProfile.logout') : t('share.login.backToHome')}</span> | |||||
| </div> | |||||
| } | |||||
| if (!redirectUrl) { | if (!redirectUrl) { | ||||
| showErrorToast('redirect url is invalid.') | |||||
| return <div className='flex h-full items-center justify-center'> | return <div className='flex h-full items-center justify-center'> | ||||
| <AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' /> | <AppUnavailable code={t('share.common.appUnavailable')} unknownReason='redirect url is invalid.' /> | ||||
| </div> | </div> | ||||
| } | } | ||||
| if (webAppAccessMode && webAppAccessMode === AccessMode.PUBLIC) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| if (!systemFeatures.webapp_auth.enabled) { | if (!systemFeatures.webapp_auth.enabled) { | ||||
| return <div className="flex h-full items-center justify-center"> | return <div className="flex h-full items-center justify-center"> | ||||
| <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> | <p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p> |
| import React from 'react' | import React from 'react' | ||||
| import Main from '@/app/components/share/text-generation' | import Main from '@/app/components/share/text-generation' | ||||
| import AuthenticatedLayout from '../../components/authenticated-layout' | |||||
| const Workflow = () => { | const Workflow = () => { | ||||
| return ( | return ( | ||||
| <Main isWorkflow /> | |||||
| <AuthenticatedLayout> | |||||
| <Main isWorkflow /> | |||||
| </AuthenticatedLayout> | |||||
| ) | ) | ||||
| } | } | ||||
| import { noop } from 'lodash-es' | import { noop } from 'lodash-es' | ||||
| export type ChatWithHistoryContextValue = { | export type ChatWithHistoryContextValue = { | ||||
| appInfoError?: any | |||||
| appInfoLoading?: boolean | |||||
| appMeta?: AppMeta | |||||
| appData?: AppData | |||||
| userCanAccess?: boolean | |||||
| appMeta?: AppMeta | null | |||||
| appData?: AppData | null | |||||
| appParams?: ChatConfig | appParams?: ChatConfig | ||||
| appChatListDataLoading?: boolean | appChatListDataLoading?: boolean | ||||
| currentConversationId: string | currentConversationId: string | ||||
| } | } | ||||
| export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | ||||
| userCanAccess: false, | |||||
| currentConversationId: '', | currentConversationId: '', | ||||
| appPrevChatTree: [], | appPrevChatTree: [], | ||||
| pinnedConversationList: [], | pinnedConversationList: [], |
| import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils' | ||||
| import { | import { | ||||
| delConversation, | delConversation, | ||||
| fetchAppInfo, | |||||
| fetchAppMeta, | |||||
| fetchAppParams, | |||||
| fetchChatList, | fetchChatList, | ||||
| fetchConversations, | fetchConversations, | ||||
| generationConversationName, | generationConversationName, | ||||
| import { InputVarType } from '@/app/components/workflow/types' | import { InputVarType } from '@/app/components/workflow/types' | ||||
| import { TransferMethod } from '@/types/app' | import { TransferMethod } from '@/types/app' | ||||
| import { noop } from 'lodash-es' | 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[]) { | function getFormattedChatList(messages: any[]) { | ||||
| const newChatList: ChatItem[] = [] | const newChatList: ChatItem[] = [] | ||||
| export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | ||||
| const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) | 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({ | useAppFavicon({ | ||||
| enable: !installedAppInfo, | enable: !installedAppInfo, | ||||
| use_icon_as_answer_icon: app.use_icon_as_answer_icon, | use_icon_as_answer_icon: app.use_icon_as_answer_icon, | ||||
| }, | }, | ||||
| plan: 'basic', | plan: 'basic', | ||||
| custom_config: null, | |||||
| } as AppData | } as AppData | ||||
| } | } | ||||
| return currentConversationId | return currentConversationId | ||||
| }, [currentConversationId, newConversationId]) | }, [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: 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: 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)) | const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) | ||||
| }, [isInstalledApp, appId, t, notify]) | }, [isInstalledApp, appId, t, notify]) | ||||
| return { | return { | ||||
| appInfoError, | |||||
| appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission), | |||||
| userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true, | |||||
| isInstalledApp, | isInstalledApp, | ||||
| appId, | appId, | ||||
| currentConversationId, | currentConversationId, |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { | import { | ||||
| useCallback, | |||||
| useEffect, | useEffect, | ||||
| useState, | useState, | ||||
| } from 'react' | } from 'react' | ||||
| import type { InstalledApp } from '@/models/explore' | import type { InstalledApp } from '@/models/explore' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | 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 AppUnavailable from '@/app/components/base/app-unavailable' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import useDocumentTitle from '@/hooks/use-document-title' | import useDocumentTitle from '@/hooks/use-document-title' | ||||
| import { useTranslation } from 'react-i18next' | |||||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||||
| type ChatWithHistoryProps = { | type ChatWithHistoryProps = { | ||||
| className?: string | className?: string | ||||
| className, | className, | ||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| userCanAccess, | |||||
| appInfoError, | |||||
| appData, | appData, | ||||
| appInfoLoading, | |||||
| appChatListDataLoading, | appChatListDataLoading, | ||||
| chatShouldReloadKey, | chatShouldReloadKey, | ||||
| isMobile, | isMobile, | ||||
| themeBuilder, | themeBuilder, | ||||
| sidebarCollapseState, | sidebarCollapseState, | ||||
| isInstalledApp, | |||||
| } = useChatWithHistoryContext() | } = useChatWithHistoryContext() | ||||
| const isSidebarCollapsed = sidebarCollapseState | const isSidebarCollapsed = sidebarCollapseState | ||||
| const customConfig = appData?.custom_config | const customConfig = appData?.custom_config | ||||
| useDocumentTitle(site?.title || 'Chat') | 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 ( | |||||
| <Loading type='app' /> | |||||
| ) | |||||
| } | |||||
| if (!userCanAccess) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-2'> | |||||
| <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> | |||||
| {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} | |||||
| </div> | |||||
| } | |||||
| if (appInfoError) { | |||||
| return ( | |||||
| <AppUnavailable /> | |||||
| ) | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={cn( | <div className={cn( | ||||
| 'flex h-full bg-background-default-burn', | 'flex h-full bg-background-default-burn', | ||||
| const themeBuilder = useThemeContext() | const themeBuilder = useThemeContext() | ||||
| const { | const { | ||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| userCanAccess, | |||||
| appData, | appData, | ||||
| appParams, | appParams, | ||||
| appMeta, | appMeta, | ||||
| return ( | return ( | ||||
| <ChatWithHistoryContext.Provider value={{ | <ChatWithHistoryContext.Provider value={{ | ||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | appData, | ||||
| userCanAccess, | |||||
| appParams, | appParams, | ||||
| appMeta, | appMeta, | ||||
| appChatListDataLoading, | appChatListDataLoading, |
| 'use client' | 'use client' | ||||
| import { | import { | ||||
| useCallback, | |||||
| useEffect, | useEffect, | ||||
| useState, | |||||
| } from 'react' | } from 'react' | ||||
| import { useAsyncEffect } from 'ahooks' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { | import { | ||||
| EmbeddedChatbotContext, | EmbeddedChatbotContext, | ||||
| import { isDify } from './utils' | import { isDify } from './utils' | ||||
| import { useThemeContext } from './theme/theme-context' | import { useThemeContext } from './theme/theme-context' | ||||
| import { CssTransform } from './theme/utils' | import { CssTransform } from './theme/utils' | ||||
| import { checkOrSetAccessToken, removeAccessToken } from '@/app/components/share/utils' | |||||
| import AppUnavailable from '@/app/components/base/app-unavailable' | |||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header' | import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import useDocumentTitle from '@/hooks/use-document-title' | import useDocumentTitle from '@/hooks/use-document-title' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||||
| const Chatbot = () => { | const Chatbot = () => { | ||||
| const { | const { | ||||
| userCanAccess, | |||||
| isMobile, | isMobile, | ||||
| allowResetChat, | allowResetChat, | ||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | appData, | ||||
| appChatListDataLoading, | appChatListDataLoading, | ||||
| chatShouldReloadKey, | chatShouldReloadKey, | ||||
| handleNewConversation, | handleNewConversation, | ||||
| themeBuilder, | themeBuilder, | ||||
| isInstalledApp, | |||||
| } = useEmbeddedChatbotContext() | } = useEmbeddedChatbotContext() | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | ||||
| useDocumentTitle(site?.title || 'Chat') | 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 && <Loading type='app' />} | |||||
| {isMobile && ( | |||||
| <div className={cn('relative')}> | |||||
| <div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}> | |||||
| <Loading type='app' /> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| if (!userCanAccess) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-2'> | |||||
| <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> | |||||
| {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} | |||||
| </div> | |||||
| } | |||||
| if (appInfoError) { | |||||
| return ( | |||||
| <> | |||||
| {!isMobile && <AppUnavailable />} | |||||
| {isMobile && ( | |||||
| <div className={cn('relative')}> | |||||
| <div className={cn('flex h-[calc(100vh_-_60px)] flex-col rounded-2xl border-[0.5px] border-components-panel-border shadow-xs')}> | |||||
| <AppUnavailable /> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| return ( | return ( | ||||
| <div className='relative'> | <div className='relative'> | ||||
| <div | <div | ||||
| const themeBuilder = useThemeContext() | const themeBuilder = useThemeContext() | ||||
| const { | const { | ||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | appData, | ||||
| userCanAccess, | userCanAccess, | ||||
| appParams, | appParams, | ||||
| return <EmbeddedChatbotContext.Provider value={{ | return <EmbeddedChatbotContext.Provider value={{ | ||||
| userCanAccess, | userCanAccess, | ||||
| appInfoError, | |||||
| appInfoLoading, | |||||
| appData, | appData, | ||||
| appParams, | appParams, | ||||
| appMeta, | appMeta, | ||||
| } | } | ||||
| const EmbeddedChatbot = () => { | const EmbeddedChatbot = () => { | ||||
| const [initialized, setInitialized] = useState(false) | |||||
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) | |||||
| const [isUnknownReason, setIsUnknownReason] = useState<boolean>(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 <AppUnavailable isUnknownReason={isUnknownReason} /> | |||||
| return <EmbeddedChatbotWrapper /> | return <EmbeddedChatbotWrapper /> | ||||
| } | } | ||||
| questionEditEnable?: boolean | questionEditEnable?: boolean | ||||
| supportFeedback?: boolean | supportFeedback?: boolean | ||||
| supportCitationHitInfo?: boolean | supportCitationHitInfo?: boolean | ||||
| system_parameters: { | |||||
| audio_file_size_limit: number | |||||
| file_size_limit: number | |||||
| image_file_size_limit: number | |||||
| video_file_size_limit: number | |||||
| workflow_file_upload_limit: number | |||||
| } | |||||
| more_like_this: { | |||||
| enabled: boolean | |||||
| } | |||||
| } | } | ||||
| export type WorkflowProcess = { | export type WorkflowProcess = { |
| const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() | const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() | ||||
| const [hasEditPermission, setHasEditPermission] = useState(false) | const [hasEditPermission, setHasEditPermission] = useState(false) | ||||
| const [installedApps, setInstalledApps] = useState<InstalledApp[]>([]) | const [installedApps, setInstalledApps] = useState<InstalledApp[]>([]) | ||||
| const [isFetchingInstalledApps, setIsFetchingInstalledApps] = useState(false) | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| useDocumentTitle(t('common.menus.explore')) | useDocumentTitle(t('common.menus.explore')) | ||||
| hasEditPermission, | hasEditPermission, | ||||
| installedApps, | installedApps, | ||||
| setInstalledApps, | setInstalledApps, | ||||
| isFetchingInstalledApps, | |||||
| setIsFetchingInstalledApps, | |||||
| } | } | ||||
| } | } | ||||
| > | > |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useEffect } from 'react' | |||||
| import React from 'react' | import React from 'react' | ||||
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import ExploreContext from '@/context/explore-context' | import ExploreContext from '@/context/explore-context' | ||||
| import TextGenerationApp from '@/app/components/share/text-generation' | import TextGenerationApp from '@/app/components/share/text-generation' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import ChatWithHistory from '@/app/components/base/chat/chat-with-history' | 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 = { | export type IInstalledAppProps = { | ||||
| id: string | id: string | ||||
| const InstalledApp: FC<IInstalledAppProps> = ({ | const InstalledApp: FC<IInstalledAppProps> = ({ | ||||
| id, | id, | ||||
| }) => { | }) => { | ||||
| const { installedApps } = useContext(ExploreContext) | |||||
| const { installedApps, isFetchingInstalledApps } = useContext(ExploreContext) | |||||
| const updateAppInfo = useWebAppStore(s => s.updateAppInfo) | |||||
| const installedApp = installedApps.find(item => item.id === id) | 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 }) | |||||
| 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 <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={appParamsError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (appMetaError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={appMetaError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (useCanAccessAppError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={useCanAccessAppError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (webAppAccessModeError) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable unknownReason={webAppAccessModeError.message} /> | |||||
| </div> | |||||
| } | |||||
| if (userCanAccessApp && !userCanAccessApp.result) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-2'> | |||||
| <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> | |||||
| </div> | |||||
| } | |||||
| if (isFetchingAppParams || isFetchingAppMeta || isFetchingWebAppAccessMode || isFetchingInstalledApps) { | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| if (!installedApp) { | if (!installedApp) { | ||||
| return ( | |||||
| <div className='flex h-full items-center'> | |||||
| <Loading type='area' /> | |||||
| </div> | |||||
| ) | |||||
| return <div className='flex h-full items-center justify-center'> | |||||
| <AppUnavailable code={404} isUnknownReason /> | |||||
| </div> | |||||
| } | } | ||||
| return ( | return ( | ||||
| <div className='h-full bg-background-default py-2 pl-0 pr-2 sm:p-2'> | <div className='h-full bg-background-default py-2 pl-0 pr-2 sm:p-2'> | ||||
| {installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && ( | |||||
| {installedApp?.app.mode !== 'completion' && installedApp?.app.mode !== 'workflow' && ( | |||||
| <ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' /> | <ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' /> | ||||
| )} | )} | ||||
| {installedApp.app.mode === 'completion' && ( | |||||
| {installedApp?.app.mode === 'completion' && ( | |||||
| <TextGenerationApp isInstalledApp installedAppInfo={installedApp} /> | <TextGenerationApp isInstalledApp installedAppInfo={installedApp} /> | ||||
| )} | )} | ||||
| {installedApp.app.mode === 'workflow' && ( | |||||
| {installedApp?.app.mode === 'workflow' && ( | |||||
| <TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} /> | <TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} /> | ||||
| )} | )} | ||||
| </div> | </div> |
| import Toast from '../../base/toast' | import Toast from '../../base/toast' | ||||
| import Item from './app-nav-item' | import Item from './app-nav-item' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { fetchInstalledAppList as doFetchInstalledAppList, uninstallApp, updatePinStatus } from '@/service/explore' | |||||
| import ExploreContext from '@/context/explore-context' | import ExploreContext from '@/context/explore-context' | ||||
| import Confirm from '@/app/components/base/confirm' | import Confirm from '@/app/components/base/confirm' | ||||
| import Divider from '@/app/components/base/divider' | import Divider from '@/app/components/base/divider' | ||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | ||||
| import { useGetInstalledApps, useUninstallApp, useUpdateAppPinStatus } from '@/service/use-explore' | |||||
| const SelectedDiscoveryIcon = () => ( | const SelectedDiscoveryIcon = () => ( | ||||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg"> | <svg width="16" height="16" viewBox="0 0 16 16" fill="current" xmlns="http://www.w3.org/2000/svg"> | ||||
| const lastSegment = segments.slice(-1)[0] | const lastSegment = segments.slice(-1)[0] | ||||
| const isDiscoverySelected = lastSegment === 'apps' | const isDiscoverySelected = lastSegment === 'apps' | ||||
| const isChatSelected = lastSegment === 'chat' | const isChatSelected = lastSegment === 'chat' | ||||
| const { installedApps, setInstalledApps } = useContext(ExploreContext) | |||||
| const { installedApps, setInstalledApps, setIsFetchingInstalledApps } = useContext(ExploreContext) | |||||
| const { isFetching: isFetchingInstalledApps, data: ret, refetch: fetchInstalledAppList } = useGetInstalledApps() | |||||
| const { mutateAsync: uninstallApp } = useUninstallApp() | |||||
| const { mutateAsync: updatePinStatus } = useUpdateAppPinStatus() | |||||
| const media = useBreakpoints() | const media = useBreakpoints() | ||||
| const isMobile = media === MediaType.mobile | const isMobile = media === MediaType.mobile | ||||
| const fetchInstalledAppList = async () => { | |||||
| const { installed_apps }: any = await doFetchInstalledAppList() | |||||
| setInstalledApps(installed_apps) | |||||
| } | |||||
| const [showConfirm, setShowConfirm] = useState(false) | const [showConfirm, setShowConfirm] = useState(false) | ||||
| const [currId, setCurrId] = useState('') | const [currId, setCurrId] = useState('') | ||||
| const handleDelete = async () => { | const handleDelete = async () => { | ||||
| type: 'success', | type: 'success', | ||||
| message: t('common.api.remove'), | message: t('common.api.remove'), | ||||
| }) | }) | ||||
| fetchInstalledAppList() | |||||
| } | } | ||||
| const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { | const handleUpdatePinStatus = async (id: string, isPinned: boolean) => { | ||||
| await updatePinStatus(id, isPinned) | |||||
| await updatePinStatus({ appId: id, isPinned }) | |||||
| Toast.notify({ | Toast.notify({ | ||||
| type: 'success', | type: 'success', | ||||
| message: t('common.api.success'), | message: t('common.api.success'), | ||||
| }) | }) | ||||
| fetchInstalledAppList() | |||||
| } | } | ||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchInstalledAppList() | |||||
| }, []) | |||||
| const installed_apps = (ret as any)?.installed_apps | |||||
| if (installed_apps && installed_apps.length > 0) | |||||
| setInstalledApps(installed_apps) | |||||
| else | |||||
| setInstalledApps([]) | |||||
| }, [ret, setInstalledApps]) | |||||
| useEffect(() => { | |||||
| setIsFetchingInstalledApps(isFetchingInstalledApps) | |||||
| }, [isFetchingInstalledApps, setIsFetchingInstalledApps]) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchInstalledAppList() | fetchInstalledAppList() | ||||
| }, [controlUpdateInstalledApps]) | |||||
| }, [controlUpdateInstalledApps, fetchInstalledAppList]) | |||||
| const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length | const pinnedAppsCount = installedApps.filter(({ is_pinned }) => is_pinned).length | ||||
| return ( | return ( |
| RiErrorWarningFill, | RiErrorWarningFill, | ||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import { useBoolean } from 'ahooks' | import { useBoolean } from 'ahooks' | ||||
| import { usePathname, useRouter, useSearchParams } from 'next/navigation' | |||||
| import { useSearchParams } from 'next/navigation' | |||||
| import TabHeader from '../../base/tab-header' | import TabHeader from '../../base/tab-header' | ||||
| import { checkOrSetAccessToken, removeAccessToken } from '../utils' | |||||
| import MenuDropdown from './menu-dropdown' | import MenuDropdown from './menu-dropdown' | ||||
| import RunBatch from './run-batch' | import RunBatch from './run-batch' | ||||
| import ResDownload from './run-batch/res-download' | import ResDownload from './run-batch/res-download' | ||||
| import AppUnavailable from '../../base/app-unavailable' | |||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | ||||
| import RunOnce from '@/app/components/share/text-generation/run-once' | import RunOnce from '@/app/components/share/text-generation/run-once' | ||||
| import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share' | |||||
| import { fetchSavedMessage as doFetchSavedMessage, removeMessage, saveMessage } from '@/service/share' | |||||
| import type { SiteInfo } from '@/models/share' | import type { SiteInfo } from '@/models/share' | ||||
| import type { | import type { | ||||
| MoreLikeThisConfig, | MoreLikeThisConfig, | ||||
| import { useAppFavicon } from '@/hooks/use-app-favicon' | import { useAppFavicon } from '@/hooks/use-app-favicon' | ||||
| import DifyLogo from '@/app/components/base/logo/dify-logo' | import DifyLogo from '@/app/components/base/logo/dify-logo' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control' | |||||
| import { AccessMode } from '@/models/access-control' | import { AccessMode } from '@/models/access-control' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | import { useGlobalPublicStore } from '@/context/global-public-context' | ||||
| import useDocumentTitle from '@/hooks/use-document-title' | import useDocumentTitle from '@/hooks/use-document-title' | ||||
| import { useWebAppStore } from '@/context/web-app-context' | |||||
| const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. | const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. | ||||
| enum TaskStatus { | enum TaskStatus { | ||||
| const mode = searchParams.get('mode') || 'create' | const mode = searchParams.get('mode') || 'create' | ||||
| const [currentTab, setCurrentTab] = useState<string>(['create', 'batch'].includes(mode) ? mode : 'create') | const [currentTab, setCurrentTab] = useState<string>(['create', 'batch'].includes(mode) ? mode : 'create') | ||||
| const router = useRouter() | |||||
| const pathname = usePathname() | |||||
| // Notice this situation isCallBatchAPI but not in batch tab | // Notice this situation isCallBatchAPI but not in batch tab | ||||
| const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) | const [isCallBatchAPI, setIsCallBatchAPI] = useState(false) | ||||
| const isInBatchTab = currentTab === 'batch' | const isInBatchTab = currentTab === 'batch' | ||||
| const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null) | const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null) | ||||
| const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) | const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) | ||||
| const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ | |||||
| appId, | |||||
| isInstalledApp, | |||||
| enabled: systemFeatures.webapp_auth.enabled, | |||||
| }) | |||||
| const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ | |||||
| appId, | |||||
| isInstalledApp, | |||||
| enabled: systemFeatures.webapp_auth.enabled, | |||||
| }) | |||||
| // save message | // save message | ||||
| const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([]) | const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([]) | ||||
| const fetchSavedMessage = async () => { | |||||
| const res: any = await doFetchSavedMessage(isInstalledApp, installedAppInfo?.id) | |||||
| const fetchSavedMessage = useCallback(async () => { | |||||
| const res: any = await doFetchSavedMessage(isInstalledApp, appId) | |||||
| setSavedMessages(res.data) | setSavedMessages(res.data) | ||||
| } | |||||
| }, [isInstalledApp, appId]) | |||||
| const handleSaveMessage = async (messageId: string) => { | const handleSaveMessage = async (messageId: string) => { | ||||
| await saveMessage(messageId, isInstalledApp, installedAppInfo?.id) | |||||
| await saveMessage(messageId, isInstalledApp, appId) | |||||
| notify({ type: 'success', message: t('common.api.saved') }) | notify({ type: 'success', message: t('common.api.saved') }) | ||||
| fetchSavedMessage() | fetchSavedMessage() | ||||
| } | } | ||||
| const handleRemoveSavedMessage = async (messageId: string) => { | const handleRemoveSavedMessage = async (messageId: string) => { | ||||
| await removeMessage(messageId, isInstalledApp, installedAppInfo?.id) | |||||
| await removeMessage(messageId, isInstalledApp, appId) | |||||
| notify({ type: 'success', message: t('common.api.remove') }) | notify({ type: 'success', message: t('common.api.remove') }) | ||||
| fetchSavedMessage() | fetchSavedMessage() | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| const fetchInitData = async () => { | |||||
| if (!isInstalledApp) | |||||
| await checkOrSetAccessToken() | |||||
| return Promise.all([ | |||||
| isInstalledApp | |||||
| ? { | |||||
| app_id: installedAppInfo?.id, | |||||
| site: { | |||||
| title: installedAppInfo?.app.name, | |||||
| prompt_public: false, | |||||
| copyright: '', | |||||
| icon: installedAppInfo?.app.icon, | |||||
| icon_background: installedAppInfo?.app.icon_background, | |||||
| }, | |||||
| plan: 'basic', | |||||
| } | |||||
| : fetchAppInfo(), | |||||
| fetchAppParams(isInstalledApp, installedAppInfo?.id), | |||||
| !isWorkflow | |||||
| ? fetchSavedMessage() | |||||
| : {}, | |||||
| ]) | |||||
| } | |||||
| const appData = useWebAppStore(s => s.appInfo) | |||||
| const appParams = useWebAppStore(s => s.appParams) | |||||
| const accessMode = useWebAppStore(s => s.webAppAccessMode) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| (async () => { | (async () => { | ||||
| const [appData, appParams]: any = await fetchInitData() | |||||
| if (!appData || !appParams) | |||||
| return | |||||
| !isWorkflow && fetchSavedMessage() | |||||
| const { app_id: appId, site: siteInfo, custom_config } = appData | const { app_id: appId, site: siteInfo, custom_config } = appData | ||||
| setAppId(appId) | setAppId(appId) | ||||
| setSiteInfo(siteInfo as SiteInfo) | setSiteInfo(siteInfo as SiteInfo) | ||||
| setVisionConfig({ | setVisionConfig({ | ||||
| // legacy of image upload compatible | // legacy of image upload compatible | ||||
| ...file_upload, | ...file_upload, | ||||
| transfer_methods: file_upload.allowed_file_upload_methods || file_upload.allowed_upload_methods, | |||||
| transfer_methods: file_upload?.allowed_file_upload_methods || file_upload?.allowed_upload_methods, | |||||
| // legacy of image upload compatible | // legacy of image upload compatible | ||||
| image_file_size_limit: appParams?.system_parameters?.image_file_size_limit, | |||||
| image_file_size_limit: appParams?.system_parameters.image_file_size_limit, | |||||
| fileUploadConfig: appParams?.system_parameters, | fileUploadConfig: appParams?.system_parameters, | ||||
| }) | |||||
| } as any) | |||||
| const prompt_variables = userInputsFormToPromptVariables(user_input_form) | const prompt_variables = userInputsFormToPromptVariables(user_input_form) | ||||
| setPromptConfig({ | setPromptConfig({ | ||||
| prompt_template: '', // placeholder for future | prompt_template: '', // placeholder for future | ||||
| setMoreLikeThisConfig(more_like_this) | setMoreLikeThisConfig(more_like_this) | ||||
| setTextToSpeechConfig(text_to_speech) | setTextToSpeechConfig(text_to_speech) | ||||
| })() | })() | ||||
| }, []) | |||||
| }, [appData, appParams, fetchSavedMessage, isWorkflow]) | |||||
| // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. | // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. | ||||
| useDocumentTitle(siteInfo?.title || t('share.generation.title')) | useDocumentTitle(siteInfo?.title || t('share.generation.title')) | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| 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 (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) { | |||||
| if (!appId || !siteInfo || !promptConfig) { | |||||
| return ( | return ( | ||||
| <div className='flex h-screen items-center'> | <div className='flex h-screen items-center'> | ||||
| <Loading type='app' /> | <Loading type='app' /> | ||||
| </div>) | </div>) | ||||
| } | } | ||||
| if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result) { | |||||
| return <div className='flex h-full flex-col items-center justify-center gap-y-2'> | |||||
| <AppUnavailable className='h-auto w-auto' code={403} unknownReason='no permission.' /> | |||||
| {!isInstalledApp && <span className='system-sm-regular cursor-pointer text-text-tertiary' onClick={backToHome}>{t('common.userProfile.logout')}</span>} | |||||
| </div> | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={cn( | <div className={cn( | ||||
| 'bg-background-default-burn', | 'bg-background-default-burn', | ||||
| imageUrl={siteInfo.icon_url} | imageUrl={siteInfo.icon_url} | ||||
| /> | /> | ||||
| <div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div> | <div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div> | ||||
| <MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} data={siteInfo} /> | |||||
| <MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} data={siteInfo} /> | |||||
| </div> | </div> | ||||
| {siteInfo.description && ( | {siteInfo.description && ( | ||||
| <div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div> | <div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div> |
| import ThemeSwitcher from '@/app/components/base/theme-switcher' | import ThemeSwitcher from '@/app/components/base/theme-switcher' | ||||
| import type { SiteInfo } from '@/models/share' | import type { SiteInfo } from '@/models/share' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { AccessMode } from '@/models/access-control' | import { AccessMode } from '@/models/access-control' | ||||
| import { useWebAppStore } from '@/context/web-app-context' | |||||
| type Props = { | type Props = { | ||||
| data?: SiteInfo | data?: SiteInfo | ||||
| placement, | placement, | ||||
| hideLogout, | hideLogout, | ||||
| }) => { | }) => { | ||||
| const webAppAccessMode = useGlobalPublicStore(s => s.webAppAccessMode) | |||||
| const webAppAccessMode = useWebAppStore(s => s.webAppAccessMode) | |||||
| const router = useRouter() | const router = useRouter() | ||||
| const pathname = usePathname() | const pathname = usePathname() | ||||
| const { t } = useTranslation() | const { t } = useTranslation() |
| version: 2, | version: 2, | ||||
| }) | }) | ||||
| export const checkOrSetAccessToken = async (appCode?: string) => { | |||||
| export const checkOrSetAccessToken = async (appCode?: string | null) => { | |||||
| const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] | const sharedToken = appCode || globalThis.location.pathname.split('/').slice(-1)[0] | ||||
| const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id | const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id | ||||
| const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) | const accessToken = localStorage.getItem('token') || JSON.stringify(getInitialTokenV2()) |
| hasEditPermission: boolean | hasEditPermission: boolean | ||||
| installedApps: InstalledApp[] | installedApps: InstalledApp[] | ||||
| setInstalledApps: (installedApps: InstalledApp[]) => void | setInstalledApps: (installedApps: InstalledApp[]) => void | ||||
| isFetchingInstalledApps: boolean | |||||
| setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void | |||||
| } | } | ||||
| const ExploreContext = createContext<IExplore>({ | const ExploreContext = createContext<IExplore>({ | ||||
| hasEditPermission: false, | hasEditPermission: false, | ||||
| installedApps: [], | installedApps: [], | ||||
| setInstalledApps: noop, | setInstalledApps: noop, | ||||
| isFetchingInstalledApps: false, | |||||
| setIsFetchingInstalledApps: noop, | |||||
| }) | }) | ||||
| export default ExploreContext | export default ExploreContext |
| import { defaultSystemFeatures } from '@/types/feature' | import { defaultSystemFeatures } from '@/types/feature' | ||||
| import { getSystemFeatures } from '@/service/common' | import { getSystemFeatures } from '@/service/common' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import { AccessMode } from '@/models/access-control' | |||||
| type GlobalPublicStore = { | type GlobalPublicStore = { | ||||
| isGlobalPending: boolean | isGlobalPending: boolean | ||||
| setIsGlobalPending: (isPending: boolean) => void | setIsGlobalPending: (isPending: boolean) => void | ||||
| systemFeatures: SystemFeatures | systemFeatures: SystemFeatures | ||||
| setSystemFeatures: (systemFeatures: SystemFeatures) => void | setSystemFeatures: (systemFeatures: SystemFeatures) => void | ||||
| webAppAccessMode: AccessMode, | |||||
| setWebAppAccessMode: (webAppAccessMode: AccessMode) => void | |||||
| } | } | ||||
| export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ | export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({ | ||||
| setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), | setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })), | ||||
| systemFeatures: defaultSystemFeatures, | systemFeatures: defaultSystemFeatures, | ||||
| setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), | setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })), | ||||
| webAppAccessMode: AccessMode.PUBLIC, | |||||
| setWebAppAccessMode: (webAppAccessMode: AccessMode) => set(() => ({ webAppAccessMode })), | |||||
| })) | })) | ||||
| const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ | const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({ |
| 'use client' | |||||
| import type { ChatConfig } from '@/app/components/base/chat/types' | |||||
| import Loading from '@/app/components/base/loading' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| import type { AppData, AppMeta } from '@/models/share' | |||||
| import { useGetWebAppAccessModeByCode } from '@/service/use-share' | |||||
| import { usePathname, useSearchParams } from 'next/navigation' | |||||
| import type { FC, PropsWithChildren } from 'react' | |||||
| import { useEffect } from 'react' | |||||
| import { useState } from 'react' | |||||
| import { create } from 'zustand' | |||||
| type WebAppStore = { | |||||
| shareCode: string | null | |||||
| updateShareCode: (shareCode: string | null) => void | |||||
| appInfo: AppData | null | |||||
| updateAppInfo: (appInfo: AppData | null) => void | |||||
| appParams: ChatConfig | null | |||||
| updateAppParams: (appParams: ChatConfig | null) => void | |||||
| webAppAccessMode: AccessMode | |||||
| updateWebAppAccessMode: (accessMode: AccessMode) => void | |||||
| appMeta: AppMeta | null | |||||
| updateWebAppMeta: (appMeta: AppMeta | null) => void | |||||
| userCanAccessApp: boolean | |||||
| updateUserCanAccessApp: (canAccess: boolean) => void | |||||
| } | |||||
| export const useWebAppStore = create<WebAppStore>(set => ({ | |||||
| shareCode: null, | |||||
| updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })), | |||||
| appInfo: null, | |||||
| updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })), | |||||
| appParams: null, | |||||
| updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })), | |||||
| webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS, | |||||
| updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })), | |||||
| appMeta: null, | |||||
| updateWebAppMeta: (appMeta: AppMeta | null) => set(() => ({ appMeta })), | |||||
| userCanAccessApp: false, | |||||
| updateUserCanAccessApp: (canAccess: boolean) => set(() => ({ userCanAccessApp: canAccess })), | |||||
| })) | |||||
| const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => { | |||||
| if (!redirectUrl || redirectUrl.length === 0) | |||||
| return null | |||||
| const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`) | |||||
| return url.pathname.split('/').pop() || null | |||||
| } | |||||
| const getShareCodeFromPathname = (pathname: string): string | null => { | |||||
| const code = pathname.split('/').pop() || null | |||||
| if (code === 'webapp-signin') | |||||
| return null | |||||
| return code | |||||
| } | |||||
| const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => { | |||||
| const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode) | |||||
| const updateShareCode = useWebAppStore(state => state.updateShareCode) | |||||
| const pathname = usePathname() | |||||
| const searchParams = useSearchParams() | |||||
| const redirectUrlParam = searchParams.get('redirect_url') | |||||
| const [shareCode, setShareCode] = useState<string | null>(null) | |||||
| useEffect(() => { | |||||
| const shareCodeFromRedirect = getShareCodeFromRedirectUrl(redirectUrlParam) | |||||
| const shareCodeFromPathname = getShareCodeFromPathname(pathname) | |||||
| const newShareCode = shareCodeFromRedirect || shareCodeFromPathname | |||||
| setShareCode(newShareCode) | |||||
| updateShareCode(newShareCode) | |||||
| }, [pathname, redirectUrlParam, updateShareCode]) | |||||
| const { isFetching, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode) | |||||
| useEffect(() => { | |||||
| if (accessModeResult?.accessMode) | |||||
| updateWebAppAccessMode(accessModeResult.accessMode) | |||||
| }, [accessModeResult, updateWebAppAccessMode]) | |||||
| if (isFetching) { | |||||
| return <div className='flex h-full w-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| {children} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default WebAppStoreProvider |
| licenseInactive: 'License Inactive', | licenseInactive: 'License Inactive', | ||||
| licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', | licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', | ||||
| webapp: { | webapp: { | ||||
| login: 'Login', | |||||
| noLoginMethod: 'Authentication method not configured for web app', | noLoginMethod: 'Authentication method not configured for web app', | ||||
| noLoginMethodTip: 'Please contact the system admin to add an authentication method.', | noLoginMethodTip: 'Please contact the system admin to add an authentication method.', | ||||
| disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.', | disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.', |
| licenseExpired: 'ライセンスの有効期限が切れています', | licenseExpired: 'ライセンスの有効期限が切れています', | ||||
| licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', | licenseLostTip: 'Dify ライセンスサーバーへの接続に失敗しました。続けて Dify を使用するために管理者に連絡してください。', | ||||
| webapp: { | webapp: { | ||||
| login: 'ログイン', | |||||
| noLoginMethod: 'Web アプリに対して認証方法が構成されていません', | noLoginMethod: 'Web アプリに対して認証方法が構成されていません', | ||||
| noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', | noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。', | ||||
| disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', | disabled: 'Web アプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。', |
| licenseInactive: '许可证未激活', | licenseInactive: '许可证未激活', | ||||
| licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', | licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', | ||||
| webapp: { | webapp: { | ||||
| login: '登录', | |||||
| noLoginMethod: 'Web 应用未配置身份认证方式', | noLoginMethod: 'Web 应用未配置身份认证方式', | ||||
| noLoginMethodTip: '请联系系统管理员添加身份认证方式', | noLoginMethodTip: '请联系系统管理员添加身份认证方式', | ||||
| disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', | disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。', |
| export type AppData = { | export type AppData = { | ||||
| app_id: string | app_id: string | ||||
| can_replace_logo?: boolean | can_replace_logo?: boolean | ||||
| custom_config?: Record<string, any> | |||||
| custom_config: Record<string, any> | null | |||||
| enable_site?: boolean | enable_site?: boolean | ||||
| end_user_id?: string | end_user_id?: string | ||||
| site: SiteInfo | site: SiteInfo |
| import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | ||||
| import { get, post } from './base' | 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 { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control' | ||||
| import type { App } from '@/types/app' | import type { App } from '@/types/app' | ||||
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| const NAME_SPACE = 'access-control' | const NAME_SPACE = 'access-control' | ||||
| }) | }) | ||||
| } | } | ||||
| 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, enabled }: { appId?: string; isInstalledApp?: boolean; enabled: boolean }) => { | |||||
| export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean; }) => { | |||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||||
| return useQuery({ | return useQuery({ | ||||
| queryKey: [NAME_SPACE, 'user-can-access-app', appId], | queryKey: [NAME_SPACE, 'user-can-access-app', appId], | ||||
| queryFn: () => getUserCanAccess(appId!, isInstalledApp), | |||||
| enabled: !!appId && enabled, | |||||
| queryFn: () => { | |||||
| if (systemFeatures.webapp_auth.enabled) | |||||
| return getUserCanAccess(appId!, isInstalledApp) | |||||
| else | |||||
| return { result: true } | |||||
| }, | |||||
| enabled: !!appId, | |||||
| staleTime: 0, | staleTime: 0, | ||||
| gcTime: 0, | gcTime: 0, | ||||
| initialData: { | |||||
| result: !enabled, | |||||
| }, | |||||
| }) | }) | ||||
| } | } |
| if (data.code === 'unauthorized') { | if (data.code === 'unauthorized') { | ||||
| removeAccessToken() | removeAccessToken() | ||||
| globalThis.location.reload() | |||||
| requiredWebSSOLogin() | |||||
| } | } | ||||
| } | } | ||||
| }) | }) | ||||
| } = otherOptionsForBaseFetch | } = otherOptionsForBaseFetch | ||||
| if (isPublicAPI && code === 'unauthorized') { | if (isPublicAPI && code === 'unauthorized') { | ||||
| removeAccessToken() | removeAccessToken() | ||||
| globalThis.location.reload() | |||||
| requiredWebSSOLogin() | |||||
| return Promise.reject(err) | return Promise.reject(err) | ||||
| } | } | ||||
| if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { | if (code === 'init_validate_failed' && IS_CE_EDITION && !silent) { |
| import { del, get, patch, post } from './base' | import { del, get, patch, post } from './base' | ||||
| import type { App, AppCategory } from '@/models/explore' | import type { App, AppCategory } from '@/models/explore' | ||||
| import type { AccessMode } from '@/models/access-control' | |||||
| export const fetchAppList = () => { | export const fetchAppList = () => { | ||||
| return get<{ | return get<{ | ||||
| export const getToolProviders = () => { | export const getToolProviders = () => { | ||||
| return get('/workspaces/current/tool-providers') | return get('/workspaces/current/tool-providers') | ||||
| } | } | ||||
| export const getAppAccessModeByAppId = (appId: string) => { | |||||
| return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) | |||||
| } |
| return get(url, { headers }) as Promise<{ access_token: string }> | 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) => { | export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => { | ||||
| if (isInstalledApp) | if (isInstalledApp) | ||||
| return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) | return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`) |
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' | |||||
| import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' | |||||
| import { fetchAppMeta, fetchAppParams } from './share' | |||||
| const NAME_SPACE = 'explore' | |||||
| export const useGetInstalledApps = () => { | |||||
| return useQuery({ | |||||
| queryKey: [NAME_SPACE, 'installedApps'], | |||||
| queryFn: () => { | |||||
| return fetchInstalledAppList() | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export const useUninstallApp = () => { | |||||
| const client = useQueryClient() | |||||
| return useMutation({ | |||||
| mutationKey: [NAME_SPACE, 'uninstallApp'], | |||||
| mutationFn: (appId: string) => uninstallApp(appId), | |||||
| onSuccess: () => { | |||||
| client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] }) | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export const useUpdateAppPinStatus = () => { | |||||
| const client = useQueryClient() | |||||
| return useMutation({ | |||||
| mutationKey: [NAME_SPACE, 'updateAppPinStatus'], | |||||
| mutationFn: ({ appId, isPinned }: { appId: string; isPinned: boolean }) => updatePinStatus(appId, isPinned), | |||||
| onSuccess: () => { | |||||
| client.invalidateQueries({ queryKey: [NAME_SPACE, 'installedApps'] }) | |||||
| }, | |||||
| }) | |||||
| } | |||||
| 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, | |||||
| }) | |||||
| } |
| import { useGlobalPublicStore } from '@/context/global-public-context' | |||||
| import { AccessMode } from '@/models/access-control' | |||||
| import { useQuery } from '@tanstack/react-query' | import { useQuery } from '@tanstack/react-query' | ||||
| import { getAppAccessModeByAppCode } from './share' | |||||
| import { fetchAppInfo, fetchAppMeta, fetchAppParams, getAppAccessModeByAppCode } from './share' | |||||
| const NAME_SPACE = 'webapp' | const NAME_SPACE = 'webapp' | ||||
| export const useAppAccessModeByCode = (code: string | null) => { | |||||
| export const useGetWebAppAccessModeByCode = (code: string | null) => { | |||||
| const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) | |||||
| return useQuery({ | return useQuery({ | ||||
| queryKey: [NAME_SPACE, 'appAccessMode', code], | queryKey: [NAME_SPACE, 'appAccessMode', code], | ||||
| queryFn: () => { | queryFn: () => { | ||||
| if (!code) | |||||
| return null | |||||
| if (systemFeatures.webapp_auth.enabled === false) { | |||||
| return { | |||||
| accessMode: AccessMode.PUBLIC, | |||||
| } | |||||
| } | |||||
| if (!code || code.length === 0) | |||||
| return Promise.reject(new Error('App code is required to get access mode')) | |||||
| return getAppAccessModeByAppCode(code) | return getAppAccessModeByAppCode(code) | ||||
| }, | }, | ||||
| enabled: !!code, | enabled: !!code, | ||||
| }) | }) | ||||
| } | } | ||||
| export const useGetWebAppInfo = () => { | |||||
| return useQuery({ | |||||
| queryKey: [NAME_SPACE, 'appInfo'], | |||||
| queryFn: () => { | |||||
| return fetchAppInfo() | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export const useGetWebAppParams = () => { | |||||
| return useQuery({ | |||||
| queryKey: [NAME_SPACE, 'appParams'], | |||||
| queryFn: () => { | |||||
| return fetchAppParams(false) | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export const useGetWebAppMeta = () => { | |||||
| return useQuery({ | |||||
| queryKey: [NAME_SPACE, 'appMeta'], | |||||
| queryFn: () => { | |||||
| return fetchAppMeta(false) | |||||
| }, | |||||
| }) | |||||
| } |