mirror of https://github.com/langgenius/dify.git
fix(web): page crash in knowledge retrieval node caused by dataset selection and score threshold (#33553)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
f886f11094
commit
49e0e1b939
|
|
@ -137,4 +137,31 @@ describe('SelectDataSet', () => {
|
||||||
expect(screen.getByRole('link', { name: 'appDebug.feature.dataSet.toCreate' })).toHaveAttribute('href', '/datasets/create')
|
expect(screen.getByRole('link', { name: 'appDebug.feature.dataSet.toCreate' })).toHaveAttribute('href', '/datasets/create')
|
||||||
expect(screen.getByRole('button', { name: 'common.operation.add' })).toBeDisabled()
|
expect(screen.getByRole('button', { name: 'common.operation.add' })).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses selectedIds as the initial modal selection', async () => {
|
||||||
|
const datasetOne = makeDataset({
|
||||||
|
id: 'set-1',
|
||||||
|
name: 'Dataset One',
|
||||||
|
})
|
||||||
|
mockUseInfiniteDatasets.mockReturnValue({
|
||||||
|
data: { pages: [{ data: [datasetOne] }] },
|
||||||
|
isLoading: false,
|
||||||
|
isFetchingNextPage: false,
|
||||||
|
fetchNextPage: vi.fn(),
|
||||||
|
hasNextPage: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSelect = vi.fn()
|
||||||
|
await act(async () => {
|
||||||
|
render(<SelectDataSet {...baseProps} onSelect={onSelect} selectedIds={['set-1']} />)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(screen.getByText('1 appDebug.feature.dataSet.selected')).toBeInTheDocument()
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'common.operation.add' }))
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(onSelect).toHaveBeenCalledWith([datasetOne])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import type { DataSet } from '@/models/datasets'
|
||||||
import { useInfiniteScroll } from 'ahooks'
|
import { useInfiniteScroll } from 'ahooks'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import Badge from '@/app/components/base/badge'
|
import Badge from '@/app/components/base/badge'
|
||||||
|
|
@ -31,17 +31,21 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [selected, setSelected] = useState<DataSet[]>([])
|
const [selectedIdsInModal, setSelectedIdsInModal] = useState(() => selectedIds)
|
||||||
const canSelectMulti = true
|
const canSelectMulti = true
|
||||||
const { formatIndexingTechniqueAndMethod } = useKnowledge()
|
const { formatIndexingTechniqueAndMethod } = useKnowledge()
|
||||||
const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteDatasets(
|
const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteDatasets(
|
||||||
{ page: 1 },
|
{ page: 1 },
|
||||||
{ enabled: isShow, staleTime: 0, refetchOnMount: 'always' },
|
{ enabled: isShow, staleTime: 0, refetchOnMount: 'always' },
|
||||||
)
|
)
|
||||||
const pages = data?.pages || []
|
|
||||||
const datasets = useMemo(() => {
|
const datasets = useMemo(() => {
|
||||||
|
const pages = data?.pages || []
|
||||||
return pages.flatMap(page => page.data.filter(item => item.indexing_technique || item.provider === 'external'))
|
return pages.flatMap(page => page.data.filter(item => item.indexing_technique || item.provider === 'external'))
|
||||||
}, [pages])
|
}, [data])
|
||||||
|
const datasetMap = useMemo(() => new Map(datasets.map(item => [item.id, item])), [datasets])
|
||||||
|
const selected = useMemo(() => {
|
||||||
|
return selectedIdsInModal.map(id => datasetMap.get(id) || ({ id } as DataSet))
|
||||||
|
}, [datasetMap, selectedIdsInModal])
|
||||||
const hasNoData = !isLoading && datasets.length === 0
|
const hasNoData = !isLoading && datasets.length === 0
|
||||||
|
|
||||||
const listRef = useRef<HTMLDivElement>(null)
|
const listRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -61,50 +65,14 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const prevSelectedIdsRef = useRef<string[]>([])
|
|
||||||
const hasUserModifiedSelectionRef = useRef(false)
|
|
||||||
useEffect(() => {
|
|
||||||
if (isShow)
|
|
||||||
hasUserModifiedSelectionRef.current = false
|
|
||||||
}, [isShow])
|
|
||||||
useEffect(() => {
|
|
||||||
const prevSelectedIds = prevSelectedIdsRef.current
|
|
||||||
const idsChanged = selectedIds.length !== prevSelectedIds.length
|
|
||||||
|| selectedIds.some((id, idx) => id !== prevSelectedIds[idx])
|
|
||||||
|
|
||||||
if (!selectedIds.length && (!hasUserModifiedSelectionRef.current || idsChanged)) {
|
|
||||||
setSelected([])
|
|
||||||
prevSelectedIdsRef.current = selectedIds
|
|
||||||
hasUserModifiedSelectionRef.current = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!idsChanged && hasUserModifiedSelectionRef.current)
|
|
||||||
return
|
|
||||||
|
|
||||||
setSelected((prev) => {
|
|
||||||
const prevMap = new Map(prev.map(item => [item.id, item]))
|
|
||||||
const nextSelected = selectedIds
|
|
||||||
.map(id => datasets.find(item => item.id === id) || prevMap.get(id))
|
|
||||||
.filter(Boolean) as DataSet[]
|
|
||||||
return nextSelected
|
|
||||||
})
|
|
||||||
prevSelectedIdsRef.current = selectedIds
|
|
||||||
hasUserModifiedSelectionRef.current = false
|
|
||||||
}, [datasets, selectedIds])
|
|
||||||
|
|
||||||
const toggleSelect = (dataSet: DataSet) => {
|
const toggleSelect = (dataSet: DataSet) => {
|
||||||
hasUserModifiedSelectionRef.current = true
|
setSelectedIdsInModal((prev) => {
|
||||||
const isSelected = selected.some(item => item.id === dataSet.id)
|
const isSelected = prev.includes(dataSet.id)
|
||||||
if (isSelected) {
|
if (isSelected)
|
||||||
setSelected(selected.filter(item => item.id !== dataSet.id))
|
return prev.filter(id => id !== dataSet.id)
|
||||||
}
|
|
||||||
else {
|
return canSelectMulti ? [...prev, dataSet.id] : [dataSet.id]
|
||||||
if (canSelectMulti)
|
})
|
||||||
setSelected([...selected, dataSet])
|
|
||||||
else
|
|
||||||
setSelected([dataSet])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
|
|
@ -126,7 +94,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||||
|
|
||||||
{hasNoData && (
|
{hasNoData && (
|
||||||
<div
|
<div
|
||||||
className="mt-6 flex h-[128px] items-center justify-center space-x-1 rounded-lg border text-[13px]"
|
className="mt-6 flex h-[128px] items-center justify-center space-x-1 rounded-lg border text-[13px]"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(0, 0, 0, 0.02)',
|
background: 'rgba(0, 0, 0, 0.02)',
|
||||||
borderColor: 'rgba(0, 0, 0, 0.02',
|
borderColor: 'rgba(0, 0, 0, 0.02',
|
||||||
|
|
@ -145,7 +113,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 shadow-xs hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
|
'flex h-10 cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 shadow-xs hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
|
||||||
selected.some(i => i.id === item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
|
selectedIdsInModal.includes(item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
|
||||||
!item.embedding_available && 'hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
|
!item.embedding_available && 'hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -195,7 +163,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||||
)}
|
)}
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<div className="mt-8 flex items-center justify-between">
|
<div className="mt-8 flex items-center justify-between">
|
||||||
<div className="text-sm font-medium text-text-secondary">
|
<div className="text-sm font-medium text-text-secondary">
|
||||||
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
|
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
|
|
|
||||||
|
|
@ -137,5 +137,11 @@ describe('ScoreThresholdItem', () => {
|
||||||
const input = screen.getByRole('textbox')
|
const input = screen.getByRole('textbox')
|
||||||
expect(input).toHaveValue('1')
|
expect(input).toHaveValue('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should fall back to default value when value is undefined', () => {
|
||||||
|
render(<ScoreThresholdItem {...defaultProps} value={undefined} />)
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
expect(input).toHaveValue('0.7')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import ParamItem from '.'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
className?: string
|
className?: string
|
||||||
value: number
|
value?: number
|
||||||
onChange: (key: string, value: number) => void
|
onChange: (key: string, value: number) => void
|
||||||
enable: boolean
|
enable: boolean
|
||||||
hasSwitch?: boolean
|
hasSwitch?: boolean
|
||||||
|
|
@ -20,6 +20,18 @@ const VALUE_LIMIT = {
|
||||||
max: 1,
|
max: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeScoreThreshold = (value?: number): number => {
|
||||||
|
const normalizedValue = typeof value === 'number' && Number.isFinite(value)
|
||||||
|
? value
|
||||||
|
: VALUE_LIMIT.default
|
||||||
|
const roundedValue = Number.parseFloat(normalizedValue.toFixed(2))
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
VALUE_LIMIT.max,
|
||||||
|
Math.max(VALUE_LIMIT.min, roundedValue),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const ScoreThresholdItem: FC<Props> = ({
|
const ScoreThresholdItem: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
|
|
@ -29,16 +41,10 @@ const ScoreThresholdItem: FC<Props> = ({
|
||||||
onSwitchChange,
|
onSwitchChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const handleParamChange = (key: string, value: number) => {
|
const handleParamChange = (key: string, nextValue: number) => {
|
||||||
let notOutRangeValue = Number.parseFloat(value.toFixed(2))
|
onChange(key, normalizeScoreThreshold(nextValue))
|
||||||
notOutRangeValue = Math.max(VALUE_LIMIT.min, notOutRangeValue)
|
|
||||||
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
|
|
||||||
onChange(key, notOutRangeValue)
|
|
||||||
}
|
}
|
||||||
const safeValue = Math.min(
|
const safeValue = normalizeScoreThreshold(value)
|
||||||
VALUE_LIMIT.max,
|
|
||||||
Math.max(VALUE_LIMIT.min, Number.parseFloat(value.toFixed(2))),
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ParamItem
|
<ParamItem
|
||||||
|
|
|
||||||
|
|
@ -928,12 +928,6 @@
|
||||||
"app/components/app/configuration/dataset-config/select-dataset/index.tsx": {
|
"app/components/app/configuration/dataset-config/select-dataset/index.tsx": {
|
||||||
"no-restricted-imports": {
|
"no-restricted-imports": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
|
||||||
"count": 2
|
|
||||||
},
|
|
||||||
"tailwindcss/no-unnecessary-whitespace": {
|
|
||||||
"count": 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app/components/app/configuration/dataset-config/settings-modal/index.tsx": {
|
"app/components/app/configuration/dataset-config/settings-modal/index.tsx": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue