-
+
{t('form.numberOfKeywords', { ns: 'datasetSettings' })}
-
+
-
+ onValueChange={handleInputChange}
+ >
+
+
+
+
+
+
+
+
)
}
diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx
new file mode 100644
index 0000000000..72e2032d75
--- /dev/null
+++ b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy.spec.tsx
@@ -0,0 +1,186 @@
+import type { ReactNode } from 'react'
+import type {
+ CredentialFormSchema,
+ CredentialFormSchemaNumberInput,
+ CredentialFormSchemaTextInput,
+} from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { render, screen } from '@testing-library/react'
+import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { AgentStrategy } from '../agent-strategy'
+
+const createI18nLabel = (text: string) => ({ en_US: text, zh_Hans: text })
+
+vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
+ useDefaultModel: () => ({ data: null }),
+}))
+
+vi.mock('@/context/i18n', () => ({
+ useDocLink: () => () => '/docs',
+}))
+
+vi.mock('@/hooks/use-i18n', () => ({
+ useRenderI18nObject: () => (value: unknown) => {
+ if (typeof value === 'string')
+ return value
+ if (value && typeof value === 'object' && 'en_US' in value)
+ return value.en_US
+ return 'label'
+ },
+}))
+
+vi.mock('../../../../store', () => ({
+ useWorkflowStore: () => ({
+ getState: () => ({
+ setControlPromptEditorRerenderKey: vi.fn(),
+ }),
+ }),
+}))
+
+vi.mock('../agent-strategy-selector', () => ({
+ AgentStrategySelector: () =>
,
+}))
+
+vi.mock('../field', () => ({
+ default: ({ children }: { children: React.ReactNode }) =>
{children}
,
+}))
+
+vi.mock('../prompt/editor', () => ({
+ default: ({ value }: { value: string }) =>
{value}
,
+}))
+
+type MockFormRenderProps = {
+ value: Record
+ onChange: (value: Record) => void
+ nodeId?: string
+ nodeOutputVars?: unknown[]
+ availableNodes?: unknown[]
+}
+
+type MockFormProps = {
+ formSchemas: Array<{ variable: string }>
+ value: Record
+ onChange: (value: Record) => void
+ override?: [unknown, (schema: unknown, props: MockFormRenderProps) => ReactNode]
+ nodeId?: string
+ nodeOutputVars?: unknown[]
+ availableNodes?: unknown[]
+}
+
+vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => ({
+ default: ({ formSchemas, value, onChange, override, nodeId, nodeOutputVars, availableNodes }: MockFormProps) => {
+ const renderOverride = override?.[1]
+
+ return (
+
+ {formSchemas.map(schema => (
+
+ {renderOverride?.(schema, {
+ value,
+ onChange,
+ nodeId,
+ nodeOutputVars,
+ availableNodes,
+ })}
+
+ ))}
+
+ )
+ },
+}))
+
+describe('AgentStrategy', () => {
+ const defaultProps = {
+ strategy: {
+ agent_strategy_provider_name: 'provider',
+ agent_strategy_name: 'strategy',
+ agent_strategy_label: 'Strategy',
+ agent_output_schema: {},
+ plugin_unique_identifier: 'plugin',
+ },
+ onStrategyChange: vi.fn(),
+ formValue: {},
+ onFormValueChange: vi.fn(),
+ nodeOutputVars: [],
+ availableNodes: [],
+ nodeId: 'node-1',
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ const createTextNumberSchema = (overrides: Partial = {}): CredentialFormSchema => ({
+ name: 'count',
+ variable: 'count',
+ label: createI18nLabel('Count'),
+ type: FormTypeEnum.textNumber,
+ required: false,
+ show_on: [],
+ default: '1',
+ ...overrides,
+ } as unknown as CredentialFormSchema)
+
+ const createTextInputSchema = (overrides: Partial = {}): CredentialFormSchema => ({
+ name: 'prompt',
+ variable: 'prompt',
+ label: createI18nLabel('Prompt'),
+ type: FormTypeEnum.textInput,
+ required: false,
+ show_on: [],
+ default: 'hello',
+ ...overrides,
+ })
+
+ it('should render text-number schemas when min and max are zero', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByRole('slider')).toBeInTheDocument()
+ expect(screen.getByRole('textbox')).toBeInTheDocument()
+ })
+
+ it('should skip text-number schemas when min is missing', () => {
+ render(
+ ,
+ )
+
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
+ })
+
+ it('should skip text-number schemas when max is missing', () => {
+ render(
+ ,
+ )
+
+ expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
+ })
+
+ it('should render text-input schemas through the editor override', () => {
+ render(
+ ,
+ )
+
+ expect(screen.getByTestId('agent-strategy-editor')).toHaveTextContent('hello')
+ })
+})
diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx
index 42be3d46e4..ba30053b77 100644
--- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx
+++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx
@@ -9,9 +9,16 @@ import Link from 'next/link'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { Agent } from '@/app/components/base/icons/src/vender/workflow'
-import { InputNumber } from '@/app/components/base/input-number'
import ListEmpty from '@/app/components/base/list-empty'
import Slider from '@/app/components/base/slider'
+import {
+ NumberField,
+ NumberFieldControls,
+ NumberFieldDecrement,
+ NumberFieldGroup,
+ NumberFieldIncrement,
+ NumberFieldInput,
+} from '@/app/components/base/ui/number-field'
import { FormTypeEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
@@ -116,11 +123,11 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
}
case FormTypeEnum.textNumber: {
const def = schema as CredentialFormSchemaNumberInput
- if (!def.max || !def.min)
+ if (def.max == null || def.min == null)
return false
const defaultValue = schema.default ? Number.parseInt(schema.default) : 1
- const value = props.value[schema.variable] || defaultValue
+ const value = props.value[schema.variable] ?? defaultValue
const onChange = (value: number) => {
props.onChange({ ...props.value, [schema.variable]: value })
}
@@ -145,16 +152,20 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => {
min={def.min}
max={def.max}
/>
-
+ onValueChange={nextValue => onChange(nextValue ?? defaultValue)}
+ >
+
+
+
+
+
+
+
+
)
diff --git a/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/__tests__/top-k-and-score-threshold.spec.tsx b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/__tests__/top-k-and-score-threshold.spec.tsx
new file mode 100644
index 0000000000..762c4c4c05
--- /dev/null
+++ b/web/app/components/workflow/nodes/knowledge-base/components/retrieval-setting/__tests__/top-k-and-score-threshold.spec.tsx
@@ -0,0 +1,35 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import TopKAndScoreThreshold from '../top-k-and-score-threshold'
+
+describe('TopKAndScoreThreshold', () => {
+ const defaultProps = {
+ topK: 3,
+ onTopKChange: vi.fn(),
+ scoreThreshold: 0.4,
+ onScoreThresholdChange: vi.fn(),
+ isScoreThresholdEnabled: true,
+ onScoreThresholdEnabledChange: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should round top-k input values before notifying the parent', () => {
+ render(