mirror of
https://github.com/langgenius/dify.git
synced 2026-10-30 18:34:29 +08:00
fix(web): keep shared ref updates out of render (#40427)
This commit is contained in:
@@ -468,9 +468,6 @@
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/debug/hooks.tsx": {
|
||||
"no-restricted-globals": {
|
||||
"count": 2
|
||||
},
|
||||
"typescript/no-explicit-any": {
|
||||
"count": 3
|
||||
}
|
||||
|
||||
@@ -70,6 +70,46 @@ describe('configuration debug hooks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should restore persisted multiple-model debug settings', () => {
|
||||
localStorage.setItem(
|
||||
'app-debug-with-single-or-multiple-models',
|
||||
JSON.stringify({
|
||||
'app-1': {
|
||||
multiple: true,
|
||||
configs: [
|
||||
{
|
||||
id: 'model-1',
|
||||
model: 'gpt-4o',
|
||||
provider: 'langgenius/openai/openai',
|
||||
parameters: { temperature: 0.7 },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const { result } = renderHook(() => useDebugWithSingleOrMultipleModel('app-1'))
|
||||
|
||||
expect(result.current.debugWithMultipleModel).toBe(true)
|
||||
expect(result.current.multipleModelConfigs).toEqual([
|
||||
{
|
||||
id: 'model-1',
|
||||
model: 'gpt-4o',
|
||||
provider: 'langgenius/openai/openai',
|
||||
parameters: { temperature: 0.7 },
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['{', 'null'])('should fall back to default settings for stored value %s', (value) => {
|
||||
localStorage.setItem('app-debug-with-single-or-multiple-models', value)
|
||||
|
||||
const { result } = renderHook(() => useDebugWithSingleOrMultipleModel('app-1'))
|
||||
|
||||
expect(result.current.debugWithMultipleModel).toBe(false)
|
||||
expect(result.current.multipleModelConfigs).toEqual([])
|
||||
})
|
||||
|
||||
it('should derive chat config data from the debug context', () => {
|
||||
mockUseDebugConfigurationContext.mockReturnValue({
|
||||
isAdvancedMode: false,
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import type { DebugWithSingleOrMultipleModelConfigs, ModelAndParameter } from './types'
|
||||
import type { ModelAndParameter } from './types'
|
||||
import type { ChatConfig, ChatItem } from '@/app/components/base/chat/types'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { AgentStrategy } from '@/types/app'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { useDebugModelConfigsStorage } from './storage'
|
||||
import { ORCHESTRATE_CHANGED } from './types'
|
||||
|
||||
export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
||||
const localeDebugWithSingleOrMultipleModelConfigs = localStorage.getItem(
|
||||
'app-debug-with-single-or-multiple-models',
|
||||
)
|
||||
|
||||
const debugWithSingleOrMultipleModelConfigs = useRef<DebugWithSingleOrMultipleModelConfigs>({})
|
||||
|
||||
if (localeDebugWithSingleOrMultipleModelConfigs) {
|
||||
try {
|
||||
debugWithSingleOrMultipleModelConfigs.current =
|
||||
JSON.parse(localeDebugWithSingleOrMultipleModelConfigs) || {}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
const [debugWithSingleOrMultipleModelConfigs, setDebugWithSingleOrMultipleModelConfigs] =
|
||||
useDebugModelConfigsStorage()
|
||||
|
||||
const [debugWithMultipleModel, setDebugWithMultipleModel] = useState(
|
||||
debugWithSingleOrMultipleModelConfigs.current[appId]?.multiple || false,
|
||||
debugWithSingleOrMultipleModelConfigs[appId]?.multiple || false,
|
||||
)
|
||||
|
||||
const [multipleModelConfigs, setMultipleModelConfigs] = useState(
|
||||
debugWithSingleOrMultipleModelConfigs.current[appId]?.configs || [],
|
||||
debugWithSingleOrMultipleModelConfigs[appId]?.configs || [],
|
||||
)
|
||||
|
||||
const handleMultipleModelConfigsChange = useCallback(
|
||||
@@ -40,15 +29,15 @@ export const useDebugWithSingleOrMultipleModel = (appId: string) => {
|
||||
multiple,
|
||||
configs: modelConfigs,
|
||||
}
|
||||
debugWithSingleOrMultipleModelConfigs.current[appId] = value
|
||||
localStorage.setItem(
|
||||
'app-debug-with-single-or-multiple-models',
|
||||
JSON.stringify(debugWithSingleOrMultipleModelConfigs.current),
|
||||
)
|
||||
const nextConfigs = {
|
||||
...debugWithSingleOrMultipleModelConfigs,
|
||||
[appId]: value,
|
||||
}
|
||||
setDebugWithSingleOrMultipleModelConfigs(nextConfigs)
|
||||
setDebugWithMultipleModel(value.multiple)
|
||||
setMultipleModelConfigs(value.configs)
|
||||
},
|
||||
[appId],
|
||||
[appId, debugWithSingleOrMultipleModelConfigs, setDebugWithSingleOrMultipleModelConfigs],
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { DebugWithSingleOrMultipleModelConfigs } from './types'
|
||||
import { createLocalStorageState } from 'foxact/create-local-storage-state'
|
||||
|
||||
const deserializeDebugModelConfigs = (value: string): DebugWithSingleOrMultipleModelConfigs => {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value)
|
||||
return parsed && typeof parsed === 'object'
|
||||
? (parsed as DebugWithSingleOrMultipleModelConfigs)
|
||||
: {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const [useDebugModelConfigsStorage] =
|
||||
createLocalStorageState<DebugWithSingleOrMultipleModelConfigs>(
|
||||
'app-debug-with-single-or-multiple-models',
|
||||
{},
|
||||
{
|
||||
serializer: (value) => JSON.stringify(value),
|
||||
deserializer: deserializeDebugModelConfigs,
|
||||
},
|
||||
)
|
||||
|
||||
export { useDebugModelConfigsStorage }
|
||||
+1
-1
@@ -155,7 +155,6 @@ const MockContextProvider = ({
|
||||
const [newInputs, setNewInputs] = React.useState(value.newConversationInputs)
|
||||
|
||||
const newInputsRef = React.useRef(newInputs)
|
||||
newInputsRef.current = newInputs
|
||||
|
||||
const contextValue: ChatWithHistoryContextValue = {
|
||||
...value,
|
||||
@@ -167,6 +166,7 @@ const MockContextProvider = ({
|
||||
value.setCurrentConversationInputs(v)
|
||||
},
|
||||
handleNewConversationInputsChange: (v: Record<string, unknown>) => {
|
||||
newInputsRef.current = v
|
||||
setNewInputs(v)
|
||||
value.handleNewConversationInputsChange(v)
|
||||
},
|
||||
|
||||
@@ -462,7 +462,9 @@ export const useChat = (
|
||||
},
|
||||
[bindWorkflowEventsReadyWaiters, markWorkflowEventsPending, resolveWorkflowEventsReadyWaiters],
|
||||
)
|
||||
startWorkflowEventsSubscriptionRef.current = startWorkflowEventsSubscription
|
||||
useEffect(() => {
|
||||
startWorkflowEventsSubscriptionRef.current = startWorkflowEventsSubscription
|
||||
}, [startWorkflowEventsSubscription])
|
||||
|
||||
const ensureWorkflowEventsSubscription = useCallback(
|
||||
(workflowRunId: string, options: IOtherOptions) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FeaturesState, FeaturesStore } from './store'
|
||||
import { createContext, useRef } from 'react'
|
||||
import { createContext, useState } from 'react'
|
||||
import { createFeaturesStore } from './store'
|
||||
|
||||
export const FeaturesContext = createContext<FeaturesStore | null>(null)
|
||||
@@ -8,9 +8,7 @@ type FeaturesProviderProps = {
|
||||
children: React.ReactNode
|
||||
} & Partial<FeaturesState>
|
||||
export const FeaturesProvider = ({ children, ...props }: FeaturesProviderProps) => {
|
||||
const storeRef = useRef<FeaturesStore | undefined>(undefined)
|
||||
const [store] = useState(() => createFeaturesStore(props))
|
||||
|
||||
if (!storeRef.current) storeRef.current = createFeaturesStore(props)
|
||||
|
||||
return <FeaturesContext.Provider value={storeRef.current}>{children}</FeaturesContext.Provider>
|
||||
return <FeaturesContext.Provider value={store}>{children}</FeaturesContext.Provider>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FileEntity } from './types'
|
||||
import { createContext, use, useRef } from 'react'
|
||||
import { createContext, use, useState } from 'react'
|
||||
import { create, useStore as useZustandStore } from 'zustand'
|
||||
|
||||
type Shape = {
|
||||
@@ -40,9 +40,7 @@ type FileProviderProps = {
|
||||
onChange?: (files: FileEntity[]) => void
|
||||
}
|
||||
export const FileContextProvider = ({ children, value, onChange }: FileProviderProps) => {
|
||||
const storeRef = useRef<FileStore | undefined>(undefined)
|
||||
const [store] = useState(() => createFileStore(value, onChange))
|
||||
|
||||
if (!storeRef.current) storeRef.current = createFileStore(value, onChange)
|
||||
|
||||
return <FileContext.Provider value={storeRef.current}>{children}</FileContext.Provider>
|
||||
return <FileContext.Provider value={store}>{children}</FileContext.Provider>
|
||||
}
|
||||
|
||||
@@ -296,6 +296,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
const parsed = JSON.parse(trimmedContent)
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
setFinalChartOption(parsed)
|
||||
finishedEventCountRef.current = 0
|
||||
setChartState('success')
|
||||
processedRef.current = true
|
||||
return
|
||||
@@ -338,6 +339,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
}
|
||||
|
||||
if (isValidOption) {
|
||||
finishedEventCountRef.current = 0
|
||||
setChartState('success')
|
||||
processedRef.current = true
|
||||
}
|
||||
@@ -424,9 +426,6 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
|
||||
// Success state: show the chart
|
||||
if (chartState === 'success' && finalChartOption) {
|
||||
// Reset finished event counter
|
||||
finishedEventCountRef.current = 0
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -11,10 +11,13 @@ export const useElapsedTimer = (complete: boolean) => {
|
||||
const [elapsedTime, setElapsedTime] = useState(0)
|
||||
// Latch completion so a transient flip back to "not complete" never restarts the timer.
|
||||
const completedRef = useRef(complete)
|
||||
if (complete) completedRef.current = true
|
||||
const isComplete = completedRef.current
|
||||
const isComplete = complete || completedRef.current
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (complete) completedRef.current = true
|
||||
}, [complete])
|
||||
|
||||
useEffect(() => {
|
||||
if (isComplete) return
|
||||
|
||||
|
||||
@@ -78,6 +78,10 @@ const AgentOutputBlockComponent = ({
|
||||
const latestDraftNameRef = useRef(name)
|
||||
const skipNameFocusRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
latestDraftNameRef.current = name
|
||||
}, [name])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditing) return
|
||||
if (skipNameFocusRef.current) {
|
||||
@@ -96,7 +100,6 @@ const AgentOutputBlockComponent = ({
|
||||
if (name !== lastNodeName) {
|
||||
setLastNodeName(name)
|
||||
setDraftName(name)
|
||||
latestDraftNameRef.current = name
|
||||
}
|
||||
|
||||
const commitOutput = (
|
||||
|
||||
+17
-2
@@ -55,7 +55,22 @@ const HITLInputReplacementBlock = ({
|
||||
ragVariables,
|
||||
readonly,
|
||||
})
|
||||
latestConfigRef.current = {
|
||||
|
||||
useEffect(() => {
|
||||
latestConfigRef.current = {
|
||||
nodeId,
|
||||
formInputs,
|
||||
onFormInputsChange,
|
||||
onFormInputItemRename,
|
||||
onFormInputItemRemove,
|
||||
workflowNodesMap,
|
||||
getVarType,
|
||||
environmentVariables,
|
||||
conversationVariables,
|
||||
ragVariables,
|
||||
readonly,
|
||||
}
|
||||
}, [
|
||||
nodeId,
|
||||
formInputs,
|
||||
onFormInputsChange,
|
||||
@@ -67,7 +82,7 @@ const HITLInputReplacementBlock = ({
|
||||
conversationVariables,
|
||||
ragVariables,
|
||||
readonly,
|
||||
}
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.hasNodes([HITLInputNode]))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FileEntity } from './types'
|
||||
import { createContext, use, useRef } from 'react'
|
||||
import { createContext, use, useState } from 'react'
|
||||
import { create, useStore } from 'zustand'
|
||||
|
||||
type Shape = {
|
||||
@@ -40,9 +40,7 @@ type FileProviderProps = {
|
||||
onChange?: (files: FileEntity[]) => void
|
||||
}
|
||||
export const FileContextProvider = ({ children, value, onChange }: FileProviderProps) => {
|
||||
const storeRef = useRef<FileStore | undefined>(undefined)
|
||||
const [store] = useState(() => createFileStore(value, onChange))
|
||||
|
||||
if (!storeRef.current) storeRef.current = createFileStore(value, onChange)
|
||||
|
||||
return <FileContext.Provider value={storeRef.current}>{children}</FileContext.Provider>
|
||||
return <FileContext.Provider value={store}>{children}</FileContext.Provider>
|
||||
}
|
||||
|
||||
+1
-7
@@ -1,6 +1,6 @@
|
||||
import type { CustomFile as File, FileItem } from '@/models/datasets'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useFileUpload } from '@/app/components/datasets/create/file-uploader/hooks/use-file-upload'
|
||||
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../../store'
|
||||
|
||||
@@ -20,16 +20,11 @@ export const useLocalFileUpload = ({
|
||||
}: UseLocalFileUploadOptions) => {
|
||||
const localFileList = useDataSourceStoreWithSelector((state) => state.localFileList)
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
const fileListRef = useRef<FileItem[]>([])
|
||||
|
||||
// Sync fileListRef with localFileList for internal tracking
|
||||
fileListRef.current = localFileList
|
||||
|
||||
const prepareFileList = useCallback(
|
||||
(files: FileItem[]) => {
|
||||
const { setLocalFileList } = dataSourceStore.getState()
|
||||
setLocalFileList(files)
|
||||
fileListRef.current = files
|
||||
},
|
||||
[dataSourceStore],
|
||||
)
|
||||
@@ -56,7 +51,6 @@ export const useLocalFileUpload = ({
|
||||
(files: FileItem[]) => {
|
||||
const { setLocalFileList } = dataSourceStore.getState()
|
||||
setLocalFileList(files)
|
||||
fileListRef.current = files
|
||||
},
|
||||
[dataSourceStore],
|
||||
)
|
||||
|
||||
+3
-3
@@ -12,12 +12,12 @@ type DataSourceProviderProps = {
|
||||
}
|
||||
|
||||
const DataSourceProvider = ({ children }: DataSourceProviderProps) => {
|
||||
const storeRef = useRef<DataSourceStoreApi>(null)
|
||||
const storeRef = useRef<DataSourceStoreApi | null>(null)
|
||||
|
||||
if (!storeRef.current) storeRef.current = createDataSourceStore()
|
||||
if (storeRef.current === null) storeRef.current = createDataSourceStore()
|
||||
|
||||
return (
|
||||
<DataSourceContext.Provider value={storeRef.current!}>{children}</DataSourceContext.Provider>
|
||||
<DataSourceContext.Provider value={storeRef.current}>{children}</DataSourceContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -39,7 +39,10 @@ export const useEmbeddingStatus = ({
|
||||
const queryClient = useQueryClient()
|
||||
const isPolling = useRef(false)
|
||||
const onCompleteRef = useRef(onComplete)
|
||||
onCompleteRef.current = onComplete
|
||||
|
||||
useEffect(() => {
|
||||
onCompleteRef.current = onComplete
|
||||
}, [onComplete])
|
||||
|
||||
const queryKey = useMemo(
|
||||
() => [NAME_SPACE, 'indexing-status', datasetId, documentId] as const,
|
||||
|
||||
+6
-3
@@ -1,6 +1,6 @@
|
||||
import type { Credential, ModelProvider } from '../../declarations'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useActiveProviderCredential } from '@/service/use-models'
|
||||
import { useUpdateModelList, useUpdateModelProviders } from '../../hooks'
|
||||
@@ -14,9 +14,12 @@ export function useActivateCredential(provider: ModelProvider) {
|
||||
const currentId = provider.custom_configuration.current_credential_id
|
||||
const selectedCredentialId = optimisticId ?? currentId
|
||||
const selectedIdRef = useRef(selectedCredentialId)
|
||||
selectedIdRef.current = selectedCredentialId
|
||||
const supportedModelTypesRef = useRef(provider.supported_model_types)
|
||||
supportedModelTypesRef.current = provider.supported_model_types
|
||||
|
||||
useEffect(() => {
|
||||
selectedIdRef.current = selectedCredentialId
|
||||
supportedModelTypesRef.current = provider.supported_model_types
|
||||
}, [selectedCredentialId, provider.supported_model_types])
|
||||
const activate = useCallback(
|
||||
(credential: Credential) => {
|
||||
if (credential.credential_id === selectedIdRef.current) return
|
||||
|
||||
Reference in New Issue
Block a user