This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { create } from 'zustand'
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control'
import type { App } from '@/types/app'
type AccessControlStore = {
appId: App['id']
setAppId: (appId: App['id']) => void
specificGroups: AccessControlGroup[]
setSpecificGroups: (specificGroups: AccessControlGroup[]) => void
specificMembers: AccessControlAccount[]
setSpecificMembers: (specificMembers: AccessControlAccount[]) => void
currentMenu: AccessMode
setCurrentMenu: (currentMenu: AccessMode) => void
selectedGroupsForBreadcrumb: AccessControlGroup[]
setSelectedGroupsForBreadcrumb: (selectedGroupsForBreadcrumb: AccessControlGroup[]) => void
}
const useAccessControlStore = create<AccessControlStore>((set) => {
return {
appId: '',
setAppId: appId => set({ appId }),
specificGroups: [],
setSpecificGroups: specificGroups => set({ specificGroups }),
specificMembers: [],
setSpecificMembers: specificMembers => set({ specificMembers }),
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
setCurrentMenu: currentMenu => set({ currentMenu }),
selectedGroupsForBreadcrumb: [],
setSelectedGroupsForBreadcrumb: selectedGroupsForBreadcrumb => set({ selectedGroupsForBreadcrumb }),
}
})
export default useAccessControlStore

View File

@@ -0,0 +1,188 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import type { FC, ReactNode } from 'react'
import { fetchCurrentWorkspace, fetchLangGeniusVersion, fetchUserProfile } from '@/service/common'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { noop } from 'lodash-es'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
import { useGlobalPublicStore } from './global-public-context'
export type AppContextValue = {
userProfile: UserProfileResponse
mutateUserProfile: VoidFunction
currentWorkspace: ICurrentWorkspace
isCurrentWorkspaceManager: boolean
isCurrentWorkspaceOwner: boolean
isCurrentWorkspaceEditor: boolean
isCurrentWorkspaceDatasetOperator: boolean
mutateCurrentWorkspace: VoidFunction
langGeniusVersionInfo: LangGeniusVersionResponse
useSelector: typeof useSelector
isLoadingCurrentWorkspace: boolean
}
const userProfilePlaceholder = {
id: '',
name: '',
email: '',
avatar: '',
avatar_url: '',
is_password_set: false,
}
const initialLangGeniusVersionInfo = {
current_env: '',
current_version: '',
latest_version: '',
release_date: '',
release_notes: '',
version: '',
can_auto_update: false,
}
const initialWorkspaceInfo: ICurrentWorkspace = {
id: '',
name: '',
plan: '',
status: '',
created_at: 0,
role: 'normal',
providers: [],
}
const AppContext = createContext<AppContextValue>({
userProfile: userProfilePlaceholder,
currentWorkspace: initialWorkspaceInfo,
isCurrentWorkspaceManager: false,
isCurrentWorkspaceOwner: false,
isCurrentWorkspaceEditor: false,
isCurrentWorkspaceDatasetOperator: false,
mutateUserProfile: noop,
mutateCurrentWorkspace: noop,
langGeniusVersionInfo: initialLangGeniusVersionInfo,
useSelector,
isLoadingCurrentWorkspace: false,
})
export function useSelector<T>(selector: (value: AppContextValue) => T): T {
return useContextSelector(AppContext, selector)
}
export type AppContextProviderProps = {
children: ReactNode
}
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: userProfileResponse, mutate: mutateUserProfile, error: userProfileError } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
const [userProfile, setUserProfile] = useState<UserProfileResponse>(userProfilePlaceholder)
const [langGeniusVersionInfo, setLangGeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangGeniusVersionInfo)
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
const updateUserProfileAndVersion = useCallback(async () => {
if (userProfileResponse && !userProfileResponse.bodyUsed) {
try {
const result = await userProfileResponse.json()
setUserProfile(result)
if (!systemFeatures.branding.enabled) {
const current_version = userProfileResponse.headers.get('x-version')
const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env')
const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } })
setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
}
}
catch (error) {
console.error('Failed to update user profile:', error)
if (userProfile.id === '')
setUserProfile(userProfilePlaceholder)
}
}
else if (userProfileError && userProfile.id === '') {
setUserProfile(userProfilePlaceholder)
}
}, [userProfileResponse, userProfileError, userProfile.id])
useEffect(() => {
updateUserProfileAndVersion()
}, [updateUserProfileAndVersion, userProfileResponse])
useEffect(() => {
if (currentWorkspaceResponse)
setCurrentWorkspace(currentWorkspaceResponse)
}, [currentWorkspaceResponse])
// #region Zendesk conversation fields
useEffect(() => {
if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
setZendeskConversationFields([{
id: ZENDESK_FIELD_IDS.ENVIRONMENT,
value: langGeniusVersionInfo.current_env.toLowerCase(),
}])
}
}, [langGeniusVersionInfo?.current_env])
useEffect(() => {
if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) {
setZendeskConversationFields([{
id: ZENDESK_FIELD_IDS.VERSION,
value: langGeniusVersionInfo.version,
}])
}
}, [langGeniusVersionInfo?.version])
useEffect(() => {
if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) {
setZendeskConversationFields([{
id: ZENDESK_FIELD_IDS.EMAIL,
value: userProfile.email,
}])
}
}, [userProfile?.email])
useEffect(() => {
if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) {
setZendeskConversationFields([{
id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
value: currentWorkspace.id,
}])
}
}, [currentWorkspace?.id])
// #endregion Zendesk conversation fields
return (
<AppContext.Provider value={{
userProfile,
mutateUserProfile,
langGeniusVersionInfo,
useSelector,
currentWorkspace,
isCurrentWorkspaceManager,
isCurrentWorkspaceOwner,
isCurrentWorkspaceEditor,
isCurrentWorkspaceDatasetOperator,
mutateCurrentWorkspace,
isLoadingCurrentWorkspace,
}}>
<div className='flex h-full flex-col overflow-y-auto'>
{globalThis.document?.body?.getAttribute('data-public-maintenance-notice') && <MaintenanceNotice />}
<div className='relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body'>
{children}
</div>
</div>
</AppContext.Provider>
)
}
export const useAppContext = () => useContext(AppContext)
export default AppContext

View File

@@ -0,0 +1,18 @@
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import type { DataSet } from '@/models/datasets'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { QueryObserverResult, RefetchOptions } from '@tanstack/react-query'
type DatasetDetailContextValue = {
indexingTechnique?: IndexingType
dataset?: DataSet
mutateDatasetRes?: (options?: RefetchOptions | undefined) => Promise<QueryObserverResult<DataSet, Error>>
}
const DatasetDetailContext = createContext<DatasetDetailContextValue>({})
export const useDatasetDetailContext = () => useContext(DatasetDetailContext)
export const useDatasetDetailContextWithSelector = <T>(selector: (value: DatasetDetailContextValue) => T): T => {
return useContextSelector(DatasetDetailContext, selector)
}
export default DatasetDetailContext

View File

@@ -0,0 +1,21 @@
'use client'
import { createContext, useContext } from 'use-context-selector'
import type { DataSet } from '@/models/datasets'
import { noop } from 'lodash-es'
export type DatasetsContextValue = {
datasets: DataSet[]
mutateDatasets: () => void
currentDataset?: DataSet
}
const DatasetsContext = createContext<DatasetsContextValue>({
datasets: [],
mutateDatasets: noop,
currentDataset: undefined,
})
export const useDatasetsContext = () => useContext(DatasetsContext)
export default DatasetsContext

View File

@@ -0,0 +1,276 @@
import type { RefObject } from 'react'
import { createContext, useContext } from 'use-context-selector'
import { PromptMode } from '@/models/debug'
import type {
AnnotationReplyConfig,
BlockStatus,
ChatPromptConfig,
CitationConfig,
CompletionPromptConfig,
ConversationHistoriesRole,
DatasetConfigs,
Inputs,
ModelConfig,
ModerationConfig,
MoreLikeThisConfig,
PromptConfig,
PromptItem,
SpeechToTextConfig,
SuggestedQuestionsAfterAnswerConfig,
TextToSpeechConfig,
} from '@/models/debug'
import type { ExternalDataTool } from '@/models/common'
import type { DataSet } from '@/models/datasets'
import type { VisionSettings } from '@/types/app'
import { AppModeEnum } from '@/types/app'
import { ModelModeType, RETRIEVE_TYPE, Resolution, TransferMethod } from '@/types/app'
import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Collection } from '@/app/components/tools/types'
import { noop } from 'lodash-es'
type IDebugConfiguration = {
appId: string
isAPIKeySet: boolean
isTrailFinished: boolean
mode: AppModeEnum
modelModeType: ModelModeType
promptMode: PromptMode
setPromptMode: (promptMode: PromptMode) => void
isAdvancedMode: boolean
isAgent: boolean
isFunctionCall: boolean
isOpenAI: boolean
collectionList: Collection[]
canReturnToSimpleMode: boolean
setCanReturnToSimpleMode: (canReturnToSimpleMode: boolean) => void
chatPromptConfig: ChatPromptConfig
completionPromptConfig: CompletionPromptConfig
currentAdvancedPrompt: PromptItem | PromptItem[]
setCurrentAdvancedPrompt: (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => void
showHistoryModal: () => void
conversationHistoriesRole: ConversationHistoriesRole
setConversationHistoriesRole: (conversationHistoriesRole: ConversationHistoriesRole) => void
hasSetBlockStatus: BlockStatus
conversationId: string | null // after first chat send
setConversationId: (conversationId: string | null) => void
introduction: string
setIntroduction: (introduction: string) => void
suggestedQuestions: string[]
setSuggestedQuestions: (questions: string[]) => void
controlClearChatMessage: number
setControlClearChatMessage: (controlClearChatMessage: number) => void
prevPromptConfig: PromptConfig
setPrevPromptConfig: (prevPromptConfig: PromptConfig) => void
moreLikeThisConfig: MoreLikeThisConfig
setMoreLikeThisConfig: (moreLikeThisConfig: MoreLikeThisConfig) => void
suggestedQuestionsAfterAnswerConfig: SuggestedQuestionsAfterAnswerConfig
setSuggestedQuestionsAfterAnswerConfig: (suggestedQuestionsAfterAnswerConfig: SuggestedQuestionsAfterAnswerConfig) => void
speechToTextConfig: SpeechToTextConfig
setSpeechToTextConfig: (speechToTextConfig: SpeechToTextConfig) => void
textToSpeechConfig: TextToSpeechConfig
setTextToSpeechConfig: (textToSpeechConfig: TextToSpeechConfig) => void
citationConfig: CitationConfig
setCitationConfig: (citationConfig: CitationConfig) => void
annotationConfig: AnnotationReplyConfig
setAnnotationConfig: (annotationConfig: AnnotationReplyConfig) => void
moderationConfig: ModerationConfig
setModerationConfig: (moderationConfig: ModerationConfig) => void
externalDataToolsConfig: ExternalDataTool[]
setExternalDataToolsConfig: (externalDataTools: ExternalDataTool[]) => void
formattingChanged: boolean
setFormattingChanged: (formattingChanged: boolean) => void
inputs: Inputs
setInputs: (inputs: Inputs) => void
query: string // user question
setQuery: (query: string) => void
// Belows are draft infos
completionParams: FormValue
setCompletionParams: (completionParams: FormValue) => void
// model_config
modelConfig: ModelConfig
setModelConfig: (modelConfig: ModelConfig) => void
dataSets: DataSet[]
setDataSets: (dataSet: DataSet[]) => void
showSelectDataSet: () => void
// dataset config
datasetConfigs: DatasetConfigs
datasetConfigsRef: RefObject<DatasetConfigs>
setDatasetConfigs: (config: DatasetConfigs) => void
hasSetContextVar: boolean
isShowVisionConfig: boolean
visionConfig: VisionSettings
setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void
isAllowVideoUpload: boolean
isShowDocumentConfig: boolean
isShowAudioConfig: boolean
rerankSettingModalOpen: boolean
setRerankSettingModalOpen: (rerankSettingModalOpen: boolean) => void
}
const DebugConfigurationContext = createContext<IDebugConfiguration>({
appId: '',
isAPIKeySet: false,
isTrailFinished: false,
mode: AppModeEnum.CHAT,
modelModeType: ModelModeType.chat,
promptMode: PromptMode.simple,
setPromptMode: noop,
isAdvancedMode: false,
isAgent: false,
isFunctionCall: false,
isOpenAI: false,
collectionList: [],
canReturnToSimpleMode: false,
setCanReturnToSimpleMode: noop,
chatPromptConfig: DEFAULT_CHAT_PROMPT_CONFIG,
completionPromptConfig: DEFAULT_COMPLETION_PROMPT_CONFIG,
currentAdvancedPrompt: [],
showHistoryModal: noop,
conversationHistoriesRole: {
user_prefix: 'user',
assistant_prefix: 'assistant',
},
setConversationHistoriesRole: noop,
setCurrentAdvancedPrompt: noop,
hasSetBlockStatus: {
context: false,
history: false,
query: false,
},
conversationId: '',
setConversationId: noop,
introduction: '',
setIntroduction: noop,
suggestedQuestions: [],
setSuggestedQuestions: noop,
controlClearChatMessage: 0,
setControlClearChatMessage: noop,
prevPromptConfig: {
prompt_template: '',
prompt_variables: [],
},
setPrevPromptConfig: noop,
moreLikeThisConfig: {
enabled: false,
},
setMoreLikeThisConfig: noop,
suggestedQuestionsAfterAnswerConfig: {
enabled: false,
},
setSuggestedQuestionsAfterAnswerConfig: noop,
speechToTextConfig: {
enabled: false,
},
setSpeechToTextConfig: noop,
textToSpeechConfig: {
enabled: false,
voice: '',
language: '',
},
setTextToSpeechConfig: noop,
citationConfig: {
enabled: false,
},
setCitationConfig: noop,
moderationConfig: {
enabled: false,
},
annotationConfig: {
id: '',
enabled: false,
score_threshold: ANNOTATION_DEFAULT.score_threshold,
embedding_model: {
embedding_model_name: '',
embedding_provider_name: '',
},
},
setAnnotationConfig: noop,
setModerationConfig: noop,
externalDataToolsConfig: [],
setExternalDataToolsConfig: noop,
formattingChanged: false,
setFormattingChanged: noop,
inputs: {},
setInputs: noop,
query: '',
setQuery: noop,
completionParams: {
max_tokens: 16,
temperature: 1, // 0-2
top_p: 1,
presence_penalty: 1, // -2-2
frequency_penalty: 1, // -2-2
},
setCompletionParams: noop,
modelConfig: {
provider: 'OPENAI', // 'OPENAI'
model_id: 'gpt-3.5-turbo', // 'gpt-3.5-turbo'
mode: ModelModeType.unset,
configs: {
prompt_template: '',
prompt_variables: [],
},
chat_prompt_config: DEFAULT_CHAT_PROMPT_CONFIG,
completion_prompt_config: DEFAULT_COMPLETION_PROMPT_CONFIG,
more_like_this: null,
opening_statement: '',
suggested_questions: [],
sensitive_word_avoidance: null,
speech_to_text: null,
text_to_speech: null,
file_upload: null,
suggested_questions_after_answer: null,
retriever_resource: null,
annotation_reply: null,
external_data_tools: [],
system_parameters: {
audio_file_size_limit: 0,
file_size_limit: 0,
image_file_size_limit: 0,
video_file_size_limit: 0,
workflow_file_upload_limit: 0,
},
dataSets: [],
agentConfig: DEFAULT_AGENT_SETTING,
},
setModelConfig: noop,
dataSets: [],
showSelectDataSet: noop,
setDataSets: noop,
datasetConfigs: {
retrieval_model: RETRIEVE_TYPE.multiWay,
reranking_model: {
reranking_provider_name: '',
reranking_model_name: '',
},
top_k: 4,
score_threshold_enabled: false,
score_threshold: 0.7,
datasets: {
datasets: [],
},
},
datasetConfigsRef: {
current: null,
} as unknown as RefObject<DatasetConfigs>,
setDatasetConfigs: noop,
hasSetContextVar: false,
isShowVisionConfig: false,
visionConfig: {
enabled: false,
number_limits: 2,
detail: Resolution.low,
transfer_methods: [TransferMethod.remote_url],
},
setVisionConfig: noop,
isAllowVideoUpload: false,
isShowDocumentConfig: false,
isShowAudioConfig: false,
rerankSettingModalOpen: false,
setRerankSettingModalOpen: noop,
})
export const useDebugConfigurationContext = () => useContext(DebugConfigurationContext)
export default DebugConfigurationContext

View File

@@ -0,0 +1,28 @@
'use client'
import { createContext, useContext } from 'use-context-selector'
import { useEventEmitter } from 'ahooks'
import type { EventEmitter } from 'ahooks/lib/useEventEmitter'
const EventEmitterContext = createContext<{ eventEmitter: EventEmitter<string> | null }>({
eventEmitter: null,
})
export const useEventEmitterContextContext = () => useContext(EventEmitterContext)
type EventEmitterContextProviderProps = {
children: React.ReactNode
}
export const EventEmitterContextProvider = ({
children,
}: EventEmitterContextProviderProps) => {
const eventEmitter = useEventEmitter<string>()
return (
<EventEmitterContext.Provider value={{ eventEmitter }}>
{children}
</EventEmitterContext.Provider>
)
}
export default EventEmitterContext

View File

@@ -0,0 +1,25 @@
import { createContext } from 'use-context-selector'
import type { InstalledApp } from '@/models/explore'
import { noop } from 'lodash-es'
type IExplore = {
controlUpdateInstalledApps: number
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
hasEditPermission: boolean
installedApps: InstalledApp[]
setInstalledApps: (installedApps: InstalledApp[]) => void
isFetchingInstalledApps: boolean
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
}
const ExploreContext = createContext<IExplore>({
controlUpdateInstalledApps: 0,
setControlUpdateInstalledApps: noop,
hasEditPermission: false,
installedApps: [],
setInstalledApps: noop,
isFetchingInstalledApps: false,
setIsFetchingInstalledApps: noop,
})
export default ExploreContext

View File

@@ -0,0 +1,28 @@
'use client'
import React, { createContext, useContext, useState } from 'react'
type ExternalApiPanelContextType = {
showExternalApiPanel: boolean
setShowExternalApiPanel: (show: boolean) => void
}
const ExternalApiPanelContext = createContext<ExternalApiPanelContextType | undefined>(undefined)
export const ExternalApiPanelProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
return (
<ExternalApiPanelContext.Provider value={{ showExternalApiPanel, setShowExternalApiPanel }}>
{children}
</ExternalApiPanelContext.Provider>
)
}
export const useExternalApiPanel = () => {
const context = useContext(ExternalApiPanelContext)
if (context === undefined)
throw new Error('useExternalApiPanel must be used within an ExternalApiPanelProvider')
return context
}

View File

@@ -0,0 +1,46 @@
'use client'
import { createContext, useContext, useMemo } from 'react'
import type { FC, ReactNode } from 'react'
import useSWR from 'swr'
import type { ExternalAPIItem, ExternalAPIListResponse } from '@/models/datasets'
import { fetchExternalAPIList } from '@/service/datasets'
type ExternalKnowledgeApiContextType = {
externalKnowledgeApiList: ExternalAPIItem[]
mutateExternalKnowledgeApis: () => Promise<ExternalAPIListResponse | undefined>
isLoading: boolean
}
const ExternalKnowledgeApiContext = createContext<ExternalKnowledgeApiContextType | undefined>(undefined)
export type ExternalKnowledgeApiProviderProps = {
children: ReactNode
}
export const ExternalKnowledgeApiProvider: FC<ExternalKnowledgeApiProviderProps> = ({ children }) => {
const { data, mutate: mutateExternalKnowledgeApis, isLoading } = useSWR<ExternalAPIListResponse>(
{ url: '/datasets/external-knowledge-api' },
fetchExternalAPIList,
)
const contextValue = useMemo<ExternalKnowledgeApiContextType>(() => ({
externalKnowledgeApiList: data?.data || [],
mutateExternalKnowledgeApis,
isLoading,
}), [data, mutateExternalKnowledgeApis, isLoading])
return (
<ExternalKnowledgeApiContext.Provider value={contextValue}>
{children}
</ExternalKnowledgeApiContext.Provider>
)
}
export const useExternalKnowledgeApi = () => {
const context = useContext(ExternalKnowledgeApiContext)
if (context === undefined)
throw new Error('useExternalKnowledgeApi must be used within a ExternalKnowledgeApiProvider')
return context
}

View File

@@ -0,0 +1,46 @@
'use client'
import { create } from 'zustand'
import { useQuery } from '@tanstack/react-query'
import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading'
type GlobalPublicStore = {
isGlobalPending: boolean
setIsGlobalPending: (isPending: boolean) => void
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
}
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
isGlobalPending: true,
setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
}))
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
children,
}) => {
const { isPending, data } = useQuery({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
const { setSystemFeatures, setIsGlobalPending: setIsPending } = useGlobalPublicStore()
useEffect(() => {
if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data })
}, [data, setSystemFeatures])
useEffect(() => {
setIsPending(isPending)
}, [isPending, setIsPending])
if (isPending)
return <div className='flex h-screen w-screen items-center justify-center'><Loading /></div>
return <>{children}</>
}
export default GlobalPublicStoreProvider

View File

@@ -0,0 +1,130 @@
import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
import dayjs from 'dayjs'
import { NUM_INFINITE } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type'
import { IS_CLOUD_EDITION } from '@/config'
import type { ModalState } from '../modal-context'
export type TriggerEventsLimitModalPayload = {
usage: number
total: number
resetInDays?: number
planType: Plan
storageKey?: string
persistDismiss?: boolean
}
type TriggerPlanInfo = {
type: Plan
usage: { triggerEvents: number }
total: { triggerEvents: number }
reset: { triggerEvents?: number | null }
}
type UseTriggerEventsLimitModalOptions = {
plan: TriggerPlanInfo
isFetchedPlan: boolean
currentWorkspaceId?: string
}
type UseTriggerEventsLimitModalResult = {
showTriggerEventsLimitModal: ModalState<TriggerEventsLimitModalPayload> | null
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
persistTriggerEventsLimitModalDismiss: () => void
}
const TRIGGER_EVENTS_LOCALSTORAGE_PREFIX = 'trigger-events-limit-dismissed'
export const useTriggerEventsLimitModal = ({
plan,
isFetchedPlan,
currentWorkspaceId,
}: UseTriggerEventsLimitModalOptions): UseTriggerEventsLimitModalResult => {
const [showTriggerEventsLimitModal, setShowTriggerEventsLimitModal] = useState<ModalState<TriggerEventsLimitModalPayload> | null>(null)
const dismissedTriggerEventsLimitStorageKeysRef = useRef<Record<string, boolean>>({})
useEffect(() => {
if (!IS_CLOUD_EDITION)
return
if (typeof window === 'undefined')
return
if (!currentWorkspaceId)
return
if (!isFetchedPlan) {
setShowTriggerEventsLimitModal(null)
return
}
const { type, usage, total, reset } = plan
const isUnlimited = total.triggerEvents === NUM_INFINITE
const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents
if (type === Plan.team || isUnlimited || !reachedLimit) {
if (showTriggerEventsLimitModal)
setShowTriggerEventsLimitModal(null)
return
}
const triggerResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
? reset.triggerEvents ?? undefined
: undefined
const cycleTag = (() => {
if (typeof reset.triggerEvents === 'number')
return dayjs().startOf('day').add(reset.triggerEvents, 'day').format('YYYY-MM-DD')
if (type === Plan.sandbox)
return dayjs().endOf('month').format('YYYY-MM-DD')
return 'none'
})()
const storageKey = `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${type}-${total.triggerEvents}-${cycleTag}`
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
return
let persistDismiss = true
let hasDismissed = false
try {
if (localStorage.getItem(storageKey) === '1')
hasDismissed = true
}
catch {
persistDismiss = false
}
if (hasDismissed)
return
if (showTriggerEventsLimitModal?.payload.storageKey === storageKey)
return
setShowTriggerEventsLimitModal({
payload: {
usage: usage.triggerEvents,
total: total.triggerEvents,
planType: type,
resetInDays: triggerResetInDays,
storageKey,
persistDismiss,
},
})
}, [plan, isFetchedPlan, showTriggerEventsLimitModal, currentWorkspaceId])
const persistTriggerEventsLimitModalDismiss = useCallback(() => {
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
if (!storageKey)
return
if (showTriggerEventsLimitModal?.payload.persistDismiss) {
try {
localStorage.setItem(storageKey, '1')
return
}
catch {
// ignore error and fall back to in-memory guard
}
}
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
}, [showTriggerEventsLimitModal])
return {
showTriggerEventsLimitModal,
setShowTriggerEventsLimitModal,
persistTriggerEventsLimitModalDismiss,
}
}

48
dify/web/context/i18n.ts Normal file
View File

@@ -0,0 +1,48 @@
import {
createContext,
useContext,
} from 'use-context-selector'
import type { Locale } from '@/i18n-config'
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
import { noop } from 'lodash-es'
type II18NContext = {
locale: Locale
i18n: Record<string, any>
setLocaleOnClient: (_lang: Locale, _reloadPage?: boolean) => Promise<void>
}
const I18NContext = createContext<II18NContext>({
locale: 'en-US',
i18n: {},
setLocaleOnClient: async (_lang: Locale, _reloadPage?: boolean) => {
noop()
},
})
export const useI18N = () => useContext(I18NContext)
export const useGetLanguage = () => {
const { locale } = useI18N()
return getLanguage(locale)
}
export const useGetPricingPageLanguage = () => {
const { locale } = useI18N()
return getPricingPageLanguage(locale)
}
export const defaultDocBaseUrl = 'https://docs.dify.ai'
export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => {
let baseDocUrl = baseUrl || defaultDocBaseUrl
baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl
const { locale } = useI18N()
const docLanguage = getDocLanguage(locale)
return (path?: string, pathMap?: { [index: string]: string }): string => {
const pathUrl = path || ''
let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl
targetPath = (targetPath.startsWith('/')) ? targetPath.slice(1) : targetPath
return `${baseDocUrl}/${docLanguage}/${targetPath}`
}
}
export default I18NContext

View File

@@ -0,0 +1,27 @@
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import { useMitt } from '@/hooks/use-mitt'
import { noop } from 'lodash-es'
type ContextValueType = ReturnType<typeof useMitt>
export const MittContext = createContext<ContextValueType>({
emit: noop,
useSubscribe: noop,
})
export const MittProvider = ({ children }: { children: React.ReactNode }) => {
const mitt = useMitt()
return (
<MittContext.Provider value={mitt}>
{children}
</MittContext.Provider>
)
}
export const useMittContext = () => {
return useContext(MittContext)
}
export function useMittContextSelector<T>(selector: (value: ContextValueType) => T): T {
return useContextSelector(MittContext, selector)
}

View File

@@ -0,0 +1,186 @@
import React from 'react'
import { act, render, screen, waitFor } from '@testing-library/react'
import { ModalContextProvider } from '@/context/modal-context'
import { Plan } from '@/app/components/billing/type'
import { defaultPlan } from '@/app/components/billing/config'
jest.mock('@/config', () => {
const actual = jest.requireActual('@/config')
return {
...actual,
IS_CLOUD_EDITION: true,
}
})
jest.mock('next/navigation', () => ({
useSearchParams: jest.fn(() => new URLSearchParams()),
}))
const mockUseProviderContext = jest.fn()
jest.mock('@/context/provider-context', () => ({
useProviderContext: () => mockUseProviderContext(),
}))
const mockUseAppContext = jest.fn()
jest.mock('@/context/app-context', () => ({
useAppContext: () => mockUseAppContext(),
}))
let latestTriggerEventsModalProps: any = null
const triggerEventsLimitModalMock = jest.fn((props: any) => {
latestTriggerEventsModalProps = props
return (
<div data-testid="trigger-limit-modal">
<button type="button" onClick={props.onDismiss}>dismiss</button>
<button type="button" onClick={props.onUpgrade}>upgrade</button>
</div>
)
})
jest.mock('@/app/components/billing/trigger-events-limit-modal', () => ({
__esModule: true,
default: (props: any) => triggerEventsLimitModalMock(props),
}))
type DefaultPlanShape = typeof defaultPlan
type ResetShape = {
apiRateLimit: number | null
triggerEvents: number | null
}
type PlanShape = Omit<DefaultPlanShape, 'reset'> & { reset: ResetShape }
type PlanOverrides = Partial<Omit<DefaultPlanShape, 'usage' | 'total' | 'reset'>> & {
usage?: Partial<DefaultPlanShape['usage']>
total?: Partial<DefaultPlanShape['total']>
reset?: Partial<ResetShape>
}
const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({
...defaultPlan,
...overrides,
usage: {
...defaultPlan.usage,
...overrides.usage,
},
total: {
...defaultPlan.total,
...overrides.total,
},
reset: {
...defaultPlan.reset,
...overrides.reset,
},
})
const renderProvider = () => render(
<ModalContextProvider>
<div data-testid="modal-context-test-child" />
</ModalContextProvider>,
)
describe('ModalContextProvider trigger events limit modal', () => {
beforeEach(() => {
latestTriggerEventsModalProps = null
triggerEventsLimitModalMock.mockClear()
mockUseAppContext.mockReset()
mockUseProviderContext.mockReset()
window.localStorage.clear()
mockUseAppContext.mockReturnValue({
currentWorkspace: {
id: 'workspace-1',
},
})
})
afterEach(() => {
jest.restoreAllMocks()
})
it('opens the trigger events limit modal and persists dismissal in localStorage', async () => {
const plan = createPlan({
type: Plan.professional,
usage: { triggerEvents: 3000 },
total: { triggerEvents: 3000 },
reset: { triggerEvents: 5 },
})
mockUseProviderContext.mockReturnValue({
plan,
isFetchedPlan: true,
})
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
renderProvider()
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
expect(latestTriggerEventsModalProps).toMatchObject({
usage: 3000,
total: 3000,
resetInDays: 5,
planType: Plan.professional,
})
act(() => {
latestTriggerEventsModalProps.onDismiss()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
const [key, value] = setItemSpy.mock.calls[0]
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
expect(value).toBe('1')
})
it('relies on the in-memory guard when localStorage reads throw', async () => {
const plan = createPlan({
type: Plan.professional,
usage: { triggerEvents: 200 },
total: { triggerEvents: 200 },
reset: { triggerEvents: 3 },
})
mockUseProviderContext.mockReturnValue({
plan,
isFetchedPlan: true,
})
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new Error('Storage disabled')
})
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
renderProvider()
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
act(() => {
latestTriggerEventsModalProps.onDismiss()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
expect(setItemSpy).not.toHaveBeenCalled()
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
})
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
const plan = createPlan({
type: Plan.professional,
usage: { triggerEvents: 120 },
total: { triggerEvents: 120 },
reset: { triggerEvents: 2 },
})
mockUseProviderContext.mockReturnValue({
plan,
isFetchedPlan: true,
})
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new Error('Quota exceeded')
})
renderProvider()
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
act(() => {
latestTriggerEventsModalProps.onDismiss()
})
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
})
})

View File

@@ -0,0 +1,506 @@
'use client'
import type { Dispatch, SetStateAction } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import { useSearchParams } from 'next/navigation'
import type {
ConfigurationMethodEnum,
Credential,
CustomConfigurationModelFixedFields,
CustomModel,
ModelProvider,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import {
EDUCATION_PRICING_SHOW_ACTION,
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
} from '@/app/education-apply/constants'
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
import {
ACCOUNT_SETTING_MODAL_ACTION,
DEFAULT_ACCOUNT_SETTING_TAB,
isValidAccountSettingTab,
} from '@/app/components/header/account-setting/constants'
import type { ModerationConfig, PromptVariable } from '@/models/debug'
import type {
ApiBasedExtension,
ExternalDataTool,
} from '@/models/common'
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
import type { OpeningStatement } from '@/app/components/base/features/types'
import type { InputVar } from '@/app/components/workflow/types'
import type { UpdatePluginPayload } from '@/app/components/plugins/types'
import { removeSpecificQueryParam } from '@/utils'
import { noop } from 'lodash-es'
import dynamic from 'next/dynamic'
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useProviderContext } from '@/context/provider-context'
import { useAppContext } from '@/context/app-context'
import {
type TriggerEventsLimitModalPayload,
useTriggerEventsLimitModal,
} from './hooks/use-trigger-events-limit-modal'
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
ssr: false,
})
const ApiBasedExtensionModal = dynamic(() => import('@/app/components/header/account-setting/api-based-extension-page/modal'), {
ssr: false,
})
const ModerationSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal'), {
ssr: false,
})
const ExternalDataToolModal = dynamic(() => import('@/app/components/app/configuration/tools/external-data-tool-modal'), {
ssr: false,
})
const Pricing = dynamic(() => import('@/app/components/billing/pricing'), {
ssr: false,
})
const AnnotationFullModal = dynamic(() => import('@/app/components/billing/annotation-full/modal'), {
ssr: false,
})
const ModelModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/model-modal'), {
ssr: false,
})
const ExternalAPIModal = dynamic(() => import('@/app/components/datasets/external-api/external-api-modal'), {
ssr: false,
})
const ModelLoadBalancingModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'), {
ssr: false,
})
const OpeningSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'), {
ssr: false,
})
const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugin'), {
ssr: false,
})
const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
ssr: false,
})
const TriggerEventsLimitModal = dynamic(() => import('@/app/components/billing/trigger-events-limit-modal'), {
ssr: false,
})
export type ModalState<T> = {
payload: T
onCancelCallback?: () => void
onSaveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
onRemoveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
onEditCallback?: (newPayload: T) => void
onValidateBeforeSaveCallback?: (newPayload: T) => boolean
isEditMode?: boolean
datasetBindings?: { id: string; name: string }[]
}
export type ModelModalType = {
currentProvider: ModelProvider
currentConfigurationMethod: ConfigurationMethodEnum
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
isModelCredential?: boolean
credential?: Credential
model?: CustomModel
mode?: ModelModalModeEnum
}
export type ModalContextState = {
setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<AccountSettingTab> | null>>
setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
setShowPricingModal: () => void
setShowAnnotationFullModal: () => void
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>>
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement & {
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}> | null>>
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
}
const PRICING_MODAL_QUERY_PARAM = 'pricing'
const PRICING_MODAL_QUERY_VALUE = 'open'
const ModalContext = createContext<ModalContextState>({
setShowAccountSettingModal: noop,
setShowApiBasedExtensionModal: noop,
setShowModerationSettingModal: noop,
setShowExternalDataToolModal: noop,
setShowPricingModal: noop,
setShowAnnotationFullModal: noop,
setShowModelModal: noop,
setShowExternalKnowledgeAPIModal: noop,
setShowModelLoadBalancingModal: noop,
setShowOpeningModal: noop,
setShowUpdatePluginModal: noop,
setShowEducationExpireNoticeModal: noop,
setShowTriggerEventsLimitModal: noop,
})
export const useModalContext = () => useContext(ModalContext)
// Adding a dangling comma to avoid the generic parsing issue in tsx, see:
// https://github.com/microsoft/TypeScript/issues/15713
export const useModalContextSelector = <T,>(selector: (state: ModalContextState) => T): T =>
useContextSelector(ModalContext, selector)
type ModalContextProviderProps = {
children: React.ReactNode
}
export const ModalContextProvider = ({
children,
}: ModalContextProviderProps) => {
const searchParams = useSearchParams()
const [showAccountSettingModal, setShowAccountSettingModal] = useState<ModalState<AccountSettingTab> | null>(() => {
if (searchParams.get('action') === ACCOUNT_SETTING_MODAL_ACTION) {
const tabParam = searchParams.get('tab')
const tab = isValidAccountSettingTab(tabParam) ? tabParam : DEFAULT_ACCOUNT_SETTING_TAB
return { payload: tab }
}
return null
})
const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null)
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null)
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement & {
promptVariables?: PromptVariable[]
workflowVariables?: InputVar[]
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
}> | null>(null)
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
const { currentWorkspace } = useAppContext()
const [showPricingModal, setShowPricingModal] = useState(
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
)
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
const handleCancelAccountSettingModal = () => {
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
if (educationVerifying === 'yes')
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
removeSpecificQueryParam('action')
removeSpecificQueryParam('tab')
setShowAccountSettingModal(null)
if (showAccountSettingModal?.onCancelCallback)
showAccountSettingModal?.onCancelCallback()
}
const handleAccountSettingTabChange = useCallback((tab: AccountSettingTab) => {
setShowAccountSettingModal((prev) => {
if (!prev)
return { payload: tab }
if (prev.payload === tab)
return prev
return { ...prev, payload: tab }
})
}, [setShowAccountSettingModal])
useEffect(() => {
if (typeof window === 'undefined')
return
const url = new URL(window.location.href)
if (!showAccountSettingModal?.payload) {
if (url.searchParams.get('action') !== ACCOUNT_SETTING_MODAL_ACTION)
return
url.searchParams.delete('action')
url.searchParams.delete('tab')
window.history.replaceState(null, '', url.toString())
return
}
url.searchParams.set('action', ACCOUNT_SETTING_MODAL_ACTION)
url.searchParams.set('tab', showAccountSettingModal.payload)
window.history.replaceState(null, '', url.toString())
}, [showAccountSettingModal])
useEffect(() => {
if (typeof window === 'undefined')
return
const url = new URL(window.location.href)
if (showPricingModal) {
url.searchParams.set(PRICING_MODAL_QUERY_PARAM, PRICING_MODAL_QUERY_VALUE)
}
else {
url.searchParams.delete(PRICING_MODAL_QUERY_PARAM)
if (url.searchParams.get('action') === EDUCATION_PRICING_SHOW_ACTION)
url.searchParams.delete('action')
}
window.history.replaceState(null, '', url.toString())
}, [showPricingModal])
const { plan, isFetchedPlan } = useProviderContext()
const {
showTriggerEventsLimitModal,
setShowTriggerEventsLimitModal,
persistTriggerEventsLimitModalDismiss,
} = useTriggerEventsLimitModal({
plan,
isFetchedPlan,
currentWorkspaceId: currentWorkspace?.id,
})
const handleCancelModerationSettingModal = () => {
setShowModerationSettingModal(null)
if (showModerationSettingModal?.onCancelCallback)
showModerationSettingModal.onCancelCallback()
}
const handleCancelExternalDataToolModal = () => {
setShowExternalDataToolModal(null)
if (showExternalDataToolModal?.onCancelCallback)
showExternalDataToolModal.onCancelCallback()
}
const handleCancelModelModal = useCallback(() => {
setShowModelModal(null)
if (showModelModal?.onCancelCallback)
showModelModal.onCancelCallback()
}, [showModelModal])
const handleSaveModelModal = useCallback((formValues?: Record<string, any>) => {
if (showModelModal?.onSaveCallback)
showModelModal.onSaveCallback(showModelModal.payload, formValues)
setShowModelModal(null)
}, [showModelModal])
const handleRemoveModelModal = useCallback((formValues?: Record<string, any>) => {
if (showModelModal?.onRemoveCallback)
showModelModal.onRemoveCallback(showModelModal.payload, formValues)
setShowModelModal(null)
}, [showModelModal])
const handleCancelExternalApiModal = useCallback(() => {
setShowExternalKnowledgeAPIModal(null)
if (showExternalKnowledgeAPIModal?.onCancelCallback)
showExternalKnowledgeAPIModal.onCancelCallback()
}, [showExternalKnowledgeAPIModal])
const handleSaveExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
if (showExternalKnowledgeAPIModal?.onSaveCallback)
showExternalKnowledgeAPIModal.onSaveCallback(updatedFormValue)
setShowExternalKnowledgeAPIModal(null)
}, [showExternalKnowledgeAPIModal])
const handleEditExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
if (showExternalKnowledgeAPIModal?.onEditCallback)
showExternalKnowledgeAPIModal.onEditCallback(updatedFormValue)
setShowExternalKnowledgeAPIModal(null)
}, [showExternalKnowledgeAPIModal])
const handleCancelOpeningModal = useCallback(() => {
setShowOpeningModal(null)
if (showOpeningModal?.onCancelCallback)
showOpeningModal.onCancelCallback()
}, [showOpeningModal])
const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => {
if (showApiBasedExtensionModal?.onSaveCallback)
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
setShowApiBasedExtensionModal(null)
}
const handleSaveModeration = (newModerationConfig: ModerationConfig) => {
if (showModerationSettingModal?.onSaveCallback)
showModerationSettingModal.onSaveCallback(newModerationConfig)
setShowModerationSettingModal(null)
}
const handleSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
if (showExternalDataToolModal?.onSaveCallback)
showExternalDataToolModal.onSaveCallback(newExternalDataTool)
setShowExternalDataToolModal(null)
}
const handleValidateBeforeSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
if (showExternalDataToolModal?.onValidateBeforeSaveCallback)
return showExternalDataToolModal?.onValidateBeforeSaveCallback(newExternalDataTool)
return true
}
const handleSaveOpeningModal = (newOpening: OpeningStatement) => {
if (showOpeningModal?.onSaveCallback)
showOpeningModal.onSaveCallback(newOpening)
setShowOpeningModal(null)
}
const handleShowPricingModal = useCallback(() => {
setShowPricingModal(true)
}, [])
const handleCancelPricingModal = useCallback(() => {
setShowPricingModal(false)
}, [])
return (
<ModalContext.Provider value={{
setShowAccountSettingModal,
setShowApiBasedExtensionModal,
setShowModerationSettingModal,
setShowExternalDataToolModal,
setShowPricingModal: handleShowPricingModal,
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
setShowModelModal,
setShowExternalKnowledgeAPIModal,
setShowModelLoadBalancingModal,
setShowOpeningModal,
setShowUpdatePluginModal,
setShowEducationExpireNoticeModal,
setShowTriggerEventsLimitModal,
}}>
<>
{children}
{
!!showAccountSettingModal && (
<AccountSetting
activeTab={showAccountSettingModal.payload}
onCancel={handleCancelAccountSettingModal}
onTabChange={handleAccountSettingTabChange}
/>
)
}
{
!!showApiBasedExtensionModal && (
<ApiBasedExtensionModal
data={showApiBasedExtensionModal.payload}
onCancel={() => setShowApiBasedExtensionModal(null)}
onSave={handleSaveApiBasedExtension}
/>
)
}
{
!!showModerationSettingModal && (
<ModerationSettingModal
data={showModerationSettingModal.payload}
onCancel={handleCancelModerationSettingModal}
onSave={handleSaveModeration}
/>
)
}
{
!!showExternalDataToolModal && (
<ExternalDataToolModal
data={showExternalDataToolModal.payload}
onCancel={handleCancelExternalDataToolModal}
onSave={handleSaveExternalDataTool}
onValidateBeforeSave={handleValidateBeforeSaveExternalDataTool}
/>
)
}
{
!!showPricingModal && (
<Pricing onCancel={handleCancelPricingModal} />
)
}
{
showAnnotationFullModal && (
<AnnotationFullModal
show={showAnnotationFullModal}
onHide={() => setShowAnnotationFullModal(false)} />
)
}
{
!!showModelModal && (
<ModelModal
provider={showModelModal.payload.currentProvider}
configurateMethod={showModelModal.payload.currentConfigurationMethod}
currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
isModelCredential={showModelModal.payload.isModelCredential}
credential={showModelModal.payload.credential}
model={showModelModal.payload.model}
mode={showModelModal.payload.mode}
onCancel={handleCancelModelModal}
onSave={handleSaveModelModal}
onRemove={handleRemoveModelModal}
/>
)
}
{
!!showExternalKnowledgeAPIModal && (
<ExternalAPIModal
data={showExternalKnowledgeAPIModal.payload}
datasetBindings={showExternalKnowledgeAPIModal.datasetBindings ?? []}
onSave={handleSaveExternalApiModal}
onCancel={handleCancelExternalApiModal}
onEdit={handleEditExternalApiModal}
isEditMode={showExternalKnowledgeAPIModal.isEditMode ?? false}
/>
)
}
{
Boolean(showModelLoadBalancingModal) && (
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
)
}
{showOpeningModal && (
<OpeningSettingModal
data={showOpeningModal.payload}
onSave={handleSaveOpeningModal}
onCancel={handleCancelOpeningModal}
promptVariables={showOpeningModal.payload.promptVariables}
workflowVariables={showOpeningModal.payload.workflowVariables}
onAutoAddPromptVariable={showOpeningModal.payload.onAutoAddPromptVariable}
/>
)}
{
!!showUpdatePluginModal && (
<UpdatePlugin
{...showUpdatePluginModal.payload}
onCancel={() => {
setShowUpdatePluginModal(null)
showUpdatePluginModal.onCancelCallback?.()
}}
onSave={() => {
setShowUpdatePluginModal(null)
showUpdatePluginModal.onSaveCallback?.({} as any)
}}
/>
)
}
{
!!showEducationExpireNoticeModal && (
<ExpireNoticeModal
{...showEducationExpireNoticeModal.payload}
onClose={() => setShowEducationExpireNoticeModal(null)}
/>
)}
{
!!showTriggerEventsLimitModal && (
<TriggerEventsLimitModal
show
usage={showTriggerEventsLimitModal.payload.usage}
total={showTriggerEventsLimitModal.payload.total}
planType={showTriggerEventsLimitModal.payload.planType}
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
onDismiss={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
}}
onUpgrade={() => {
persistTriggerEventsLimitModalDismiss()
setShowTriggerEventsLimitModal(null)
handleShowPricingModal()
}}
/>
)}
</>
</ModalContext.Provider>
)
}
export default ModalContext

View File

@@ -0,0 +1,253 @@
'use client'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import useSWR from 'swr'
import { useEffect, useState } from 'react'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import {
fetchModelList,
fetchModelProviders,
fetchSupportRetrievalMethods,
} from '@/service/common'
import {
CurrentSystemQuotaTypeEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { RETRIEVE_METHOD } from '@/types/app'
import type { Plan, UsageResetInfo } from '@/app/components/billing/type'
import type { UsagePlanInfo } from '@/app/components/billing/type'
import { fetchCurrentPlanInfo } from '@/service/billing'
import { parseCurrentPlan } from '@/app/components/billing/utils'
import { defaultPlan } from '@/app/components/billing/config'
import Toast from '@/app/components/base/toast'
import {
useEducationStatus,
} from '@/service/use-education'
import { noop } from 'lodash-es'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
type ProviderContextState = {
modelProviders: ModelProvider[]
refreshModelProviders: () => void
textGenerationModelList: Model[]
supportRetrievalMethods: RETRIEVE_METHOD[]
isAPIKeySet: boolean
plan: {
type: Plan
usage: UsagePlanInfo
total: UsagePlanInfo
reset: UsageResetInfo
}
isFetchedPlan: boolean
enableBilling: boolean
onPlanInfoChanged: () => void
enableReplaceWebAppLogo: boolean
modelLoadBalancingEnabled: boolean
datasetOperatorEnabled: boolean
enableEducationPlan: boolean
isEducationWorkspace: boolean
isEducationAccount: boolean
allowRefreshEducationVerify: boolean
educationAccountExpireAt: number | null
isLoadingEducationAccountInfo: boolean
isFetchingEducationAccountInfo: boolean
webappCopyrightEnabled: boolean
licenseLimit: {
workspace_members: {
size: number
limit: number
}
},
refreshLicenseLimit: () => void
isAllowTransferWorkspace: boolean
isAllowPublishAsCustomKnowledgePipelineTemplate: boolean
}
const ProviderContext = createContext<ProviderContextState>({
modelProviders: [],
refreshModelProviders: noop,
textGenerationModelList: [],
supportRetrievalMethods: [],
isAPIKeySet: true,
plan: defaultPlan,
isFetchedPlan: false,
enableBilling: false,
onPlanInfoChanged: noop,
enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false,
datasetOperatorEnabled: false,
enableEducationPlan: false,
isEducationWorkspace: false,
isEducationAccount: false,
allowRefreshEducationVerify: false,
educationAccountExpireAt: null,
isLoadingEducationAccountInfo: false,
isFetchingEducationAccountInfo: false,
webappCopyrightEnabled: false,
licenseLimit: {
workspace_members: {
size: 0,
limit: 0,
},
},
refreshLicenseLimit: noop,
isAllowTransferWorkspace: false,
isAllowPublishAsCustomKnowledgePipelineTemplate: false,
})
export const useProviderContext = () => useContext(ProviderContext)
// Adding a dangling comma to avoid the generic parsing issue in tsx, see:
// https://github.com/microsoft/TypeScript/issues/15713
export const useProviderContextSelector = <T,>(selector: (state: ProviderContextState) => T): T =>
useContextSelector(ProviderContext, selector)
type ProviderContextProviderProps = {
children: React.ReactNode
}
export const ProviderContextProvider = ({
children,
}: ProviderContextProviderProps) => {
const { data: providersData, mutate: refreshModelProviders } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
const fetchModelListUrlPrefix = '/workspaces/current/models/model-types/'
const { data: textGenerationModelList } = useSWR(`${fetchModelListUrlPrefix}${ModelTypeEnum.textGeneration}`, fetchModelList)
const { data: supportRetrievalMethods } = useSWR('/datasets/retrieval-setting', fetchSupportRetrievalMethods)
const [plan, setPlan] = useState(defaultPlan)
const [isFetchedPlan, setIsFetchedPlan] = useState(false)
const [enableBilling, setEnableBilling] = useState(true)
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false)
const [licenseLimit, setLicenseLimit] = useState({
workspace_members: {
size: 0,
limit: 0,
},
})
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan)
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
const [isAllowPublishAsCustomKnowledgePipelineTemplate, setIsAllowPublishAsCustomKnowledgePipelineTemplate] = useState(false)
const fetchPlan = async () => {
try {
const data = await fetchCurrentPlanInfo()
if (!data) {
console.error('Failed to fetch plan info: data is undefined')
return
}
// set default value to avoid undefined error
setEnableBilling(data.billing?.enabled ?? false)
setEnableEducationPlan(data.education?.enabled ?? false)
setIsEducationWorkspace(data.education?.activated ?? false)
setEnableReplaceWebAppLogo(data.can_replace_logo ?? false)
if (data.billing?.enabled) {
setPlan(parseCurrentPlan(data) as any)
setIsFetchedPlan(true)
}
if (data.model_load_balancing_enabled)
setModelLoadBalancingEnabled(true)
if (data.dataset_operator_enabled)
setDatasetOperatorEnabled(true)
if (data.webapp_copyright_enabled)
setWebappCopyrightEnabled(true)
if (data.workspace_members)
setLicenseLimit({ workspace_members: data.workspace_members })
if (data.is_allow_transfer_workspace)
setIsAllowTransferWorkspace(data.is_allow_transfer_workspace)
if (data.knowledge_pipeline?.publish_enabled)
setIsAllowPublishAsCustomKnowledgePipelineTemplate(data.knowledge_pipeline?.publish_enabled)
}
catch (error) {
console.error('Failed to fetch plan info:', error)
// set default value to avoid undefined error
setEnableBilling(false)
setEnableEducationPlan(false)
setIsEducationWorkspace(false)
setEnableReplaceWebAppLogo(false)
}
}
useEffect(() => {
fetchPlan()
}, [])
// #region Zendesk conversation fields
useEffect(() => {
if (ZENDESK_FIELD_IDS.PLAN && plan.type) {
setZendeskConversationFields([{
id: ZENDESK_FIELD_IDS.PLAN,
value: `${plan.type}-plan`,
}])
}
}, [plan.type])
// #endregion Zendesk conversation fields
const { t } = useTranslation()
useEffect(() => {
if (localStorage.getItem('anthropic_quota_notice') === 'true')
return
if (dayjs().isAfter(dayjs('2025-03-17')))
return
if (providersData?.data && providersData.data.length > 0) {
const anthropic = providersData.data.find(provider => provider.provider === 'anthropic')
if (anthropic && anthropic.system_configuration.current_quota_type === CurrentSystemQuotaTypeEnum.trial) {
const quota = anthropic.system_configuration.quota_configurations.find(item => item.quota_type === anthropic.system_configuration.current_quota_type)
if (quota && quota.is_valid && quota.quota_used < quota.quota_limit) {
Toast.notify({
type: 'info',
message: t('common.provider.anthropicHosted.trialQuotaTip'),
duration: 60000,
onClose: () => {
localStorage.setItem('anthropic_quota_notice', 'true')
},
})
}
}
}
}, [providersData, t])
return (
<ProviderContext.Provider value={{
modelProviders: providersData?.data || [],
refreshModelProviders,
textGenerationModelList: textGenerationModelList?.data || [],
isAPIKeySet: !!textGenerationModelList?.data.some(model => model.status === ModelStatusEnum.active),
supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [],
plan,
isFetchedPlan,
enableBilling,
onPlanInfoChanged: fetchPlan,
enableReplaceWebAppLogo,
modelLoadBalancingEnabled,
datasetOperatorEnabled,
enableEducationPlan,
isEducationWorkspace,
isEducationAccount: educationAccountInfo?.is_student || false,
allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false,
educationAccountExpireAt: educationAccountInfo?.expire_at || null,
isLoadingEducationAccountInfo,
isFetchingEducationAccountInfo,
webappCopyrightEnabled,
licenseLimit,
refreshLicenseLimit: fetchPlan,
isAllowTransferWorkspace,
isAllowPublishAsCustomKnowledgePipelineTemplate,
}}>
{children}
</ProviderContext.Provider>
)
}
export default ProviderContext

View File

@@ -0,0 +1,23 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const STALE_TIME = 1000 * 60 * 30 // 30 minutes
const client = new QueryClient({
defaultOptions: {
queries: {
staleTime: STALE_TIME,
},
},
})
export const TanstackQueryInitializer: FC<PropsWithChildren> = (props) => {
const { children } = props
return <QueryClientProvider client={client}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
}

View File

@@ -0,0 +1,125 @@
'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 { create } from 'zustand'
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
import { useGlobalPublicStore } from './global-public-context'
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
embeddedUserId: string | null
updateEmbeddedUserId: (userId: string | null) => void
embeddedConversationId: string | null
updateEmbeddedConversationId: (conversationId: string | null) => 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 })),
embeddedUserId: null,
updateEmbeddedUserId: (userId: string | null) => set(() => ({ embeddedUserId: userId })),
embeddedConversationId: null,
updateEmbeddedConversationId: (conversationId: string | null) =>
set(() => ({ embeddedConversationId: conversationId })),
}))
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 isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
const updateShareCode = useWebAppStore(state => state.updateShareCode)
const updateEmbeddedUserId = useWebAppStore(state => state.updateEmbeddedUserId)
const updateEmbeddedConversationId = useWebAppStore(state => state.updateEmbeddedConversationId)
const pathname = usePathname()
const searchParams = useSearchParams()
const redirectUrlParam = searchParams.get('redirect_url')
const searchParamsString = searchParams.toString()
// Compute shareCode directly
const shareCode = getShareCodeFromRedirectUrl(redirectUrlParam) || getShareCodeFromPathname(pathname)
useEffect(() => {
updateShareCode(shareCode)
}, [shareCode, updateShareCode])
useEffect(() => {
let cancelled = false
const syncEmbeddedUserId = async () => {
try {
const { user_id, conversation_id } = await getProcessedSystemVariablesFromUrlParams()
if (!cancelled) {
updateEmbeddedUserId(user_id || null)
updateEmbeddedConversationId(conversation_id || null)
}
}
catch {
if (!cancelled) {
updateEmbeddedUserId(null)
updateEmbeddedConversationId(null)
}
}
}
syncEmbeddedUserId()
return () => {
cancelled = true
}
}, [searchParamsString, updateEmbeddedUserId, updateEmbeddedConversationId])
const { isLoading, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
useEffect(() => {
if (accessModeResult?.accessMode)
updateWebAppAccessMode(accessModeResult.accessMode)
}, [accessModeResult, updateWebAppAccessMode, shareCode])
if (isGlobalPending || isLoading) {
return <div className='flex h-full w-full items-center justify-center'>
<Loading />
</div>
}
return (
<>
{children}
</>
)
}
export default WebAppStoreProvider

View File

@@ -0,0 +1,36 @@
'use client'
import { createContext, useContext } from 'use-context-selector'
import useSWR from 'swr'
import { fetchWorkspaces } from '@/service/common'
import type { IWorkspace } from '@/models/common'
export type WorkspacesContextValue = {
workspaces: IWorkspace[]
}
const WorkspacesContext = createContext<WorkspacesContextValue>({
workspaces: [],
})
type IWorkspaceProviderProps = {
children: React.ReactNode
}
export const WorkspaceProvider = ({
children,
}: IWorkspaceProviderProps) => {
const { data } = useSWR({ url: '/workspaces' }, fetchWorkspaces)
return (
<WorkspacesContext.Provider value={{
workspaces: data?.workspaces || [],
}}>
{children}
</WorkspacesContext.Provider>
)
}
export const useWorkspacesContext = () => useContext(WorkspacesContext)
export default WorkspacesContext