diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx index b07f48d4bb..39c1244980 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/config-param-modal.spec.tsx @@ -40,7 +40,7 @@ vi.mock('../score-slider', () => ({ onChange(Number((e.target as HTMLInputElement).value))} @@ -272,7 +272,7 @@ describe('ConfigParamModal', () => { ) const slider = screen.getByRole('slider') - expect(slider).toHaveAttribute('min', '80') + expect(slider).toHaveAttribute('min', '0') expect(slider).toHaveAttribute('max', '100') expect(slider).toHaveValue('90') }) @@ -375,7 +375,7 @@ describe('ConfigParamModal', () => { it('should use ANNOTATION_DEFAULT score_threshold when config has no score_threshold', () => { const configWithoutThreshold = { ...defaultAnnotationConfig, - score_threshold: 0, + score_threshold: undefined as unknown as number, } render( { expect(screen.getByRole('slider')).toHaveValue('90') }) + it('should preserve zero score threshold instead of falling back to default', async () => { + const onSave = vi.fn().mockResolvedValue(undefined) + render( + , + ) + + expect(screen.getByRole('slider')).toHaveValue('0') + + const buttons = screen.getAllByRole('button') + const saveBtn = buttons.find(b => b.textContent?.includes('initSetup')) + fireEvent.click(saveBtn!) + + await waitFor(() => { + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ embedding_provider_name: 'openai' }), + 0, + ) + }) + }) + it('should set loading state while saving', async () => { let resolveOnSave: () => void const onSave = vi.fn().mockImplementation(() => new Promise((resolve) => { diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx index 03ddbc6322..4aae34387e 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/index.spec.tsx @@ -175,6 +175,22 @@ describe('AnnotationReply', () => { expect(screen.getByText('text-embedding-ada-002')).toBeInTheDocument() }) + it('should show zero score threshold when enabled', () => { + renderWithProvider({}, { + annotationReply: { + enabled: true, + score_threshold: 0, + embedding_model: { + embedding_provider_name: 'openai', + embedding_model_name: 'text-embedding-ada-002', + }, + }, + }) + + expect(screen.getByText('0')).toBeInTheDocument() + expect(screen.getByText('text-embedding-ada-002')).toBeInTheDocument() + }) + it('should show dash when score threshold is not set', () => { renderWithProvider({}, { annotationReply: { diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts index 14f6d68bda..6f6deb7ae1 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/__tests__/use-annotation-config.spec.ts @@ -1,6 +1,6 @@ import type { AnnotationReplyConfig } from '@/models/debug' import { act, renderHook } from '@testing-library/react' -import { queryAnnotationJobStatus } from '@/service/annotation' +import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation' import { sleep } from '@/utils' import useAnnotationConfig from '../use-annotation-config' @@ -162,6 +162,35 @@ describe('useAnnotationConfig', () => { expect(updatedConfig.score_threshold).toBe(0.85) }) + it('should preserve zero score threshold when enabling annotation', async () => { + const zeroScoreConfig = { ...defaultConfig, score_threshold: 0 } + const setAnnotationConfig = vi.fn() + const { result } = renderHook(() => useAnnotationConfig({ + appId: 'test-app', + annotationConfig: zeroScoreConfig, + setAnnotationConfig, + })) + + await act(async () => { + await result.current.handleEnableAnnotation({ + embedding_provider_name: 'openai', + embedding_model_name: 'text-embedding-3-small', + }, 0) + }) + + expect(updateAnnotationStatus).toHaveBeenCalledWith( + 'test-app', + 'enable', + { + embedding_provider_name: 'openai', + embedding_model_name: 'text-embedding-3-small', + }, + 0, + ) + const updatedConfig = setAnnotationConfig.mock.calls[0]![0] + expect(updatedConfig.score_threshold).toBe(0) + }) + it('should set score and embedding model together', () => { const setAnnotationConfig = vi.fn() const { result } = renderHook(() => useAnnotationConfig({ diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx index e8932979a6..9c341d4ec1 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal.tsx @@ -75,7 +75,7 @@ const ConfigParamModal: FC = ({ isShow, onHide: doHide, onSave, isInit, a { setAnnotationConfig({ ...annotationConfig, diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx index 80ebaf9614..b73699359b 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/index.tsx @@ -100,7 +100,7 @@ const AnnotationReply = ({
{t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })}
-
{annotationReply.score_threshold || '-'}
+
{annotationReply.score_threshold ?? '-'}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx index ffa9c33043..97493ab441 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/__tests__/index.spec.tsx @@ -17,7 +17,7 @@ describe('ScoreSlider', () => { it('should display easy match and accurate match labels', () => { render() - expect(screen.getByText('0.8')).toBeInTheDocument() + expect(screen.getByText('0.0')).toBeInTheDocument() expect(screen.getByText('1.0')).toBeInTheDocument() expect(screen.getByText(/feature\.annotation\.scoreThreshold\.easyMatch/)).toBeInTheDocument() expect(screen.getByText(/feature\.annotation\.scoreThreshold\.accurateMatch/)).toBeInTheDocument() @@ -36,4 +36,11 @@ describe('ScoreSlider', () => { expect(getSliderInput()).toHaveValue('95') expect(screen.getByText('0.95')).toBeInTheDocument() }) + + it('should allow zero as the minimum score threshold', () => { + render() + + expect(getSliderInput()).toHaveValue('0') + expect(screen.getByText('0.00')).toBeInTheDocument() + }) }) diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx index 8a2dc18fc0..d3b6340c89 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/score-slider/index.tsx @@ -17,13 +17,16 @@ const clamp = (value: number, min: number, max: number) => { return Math.min(Math.max(value, min), max) } +const SCORE_MIN = 0 +const SCORE_MAX = 100 + const ScoreSlider: FC = ({ className, value, onChange, }) => { const { t } = useTranslation() - const safeValue = clamp(value, 80, 100) + const safeValue = clamp(value, SCORE_MIN, SCORE_MAX) return (
@@ -31,8 +34,8 @@ const ScoreSlider: FC = ({ = ({
@@ -49,7 +52,7 @@ const ScoreSlider: FC = ({
-
0.8
+
0.0
ยท
{t('feature.annotation.scoreThreshold.easyMatch', { ns: 'appDebug' })}
diff --git a/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts b/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts index c74175846d..64c714df4e 100644 --- a/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts +++ b/web/app/components/base/features/new-feature-panel/annotation-reply/use-annotation-config.ts @@ -53,7 +53,7 @@ const useAnnotationConfig = ({ setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => { draft.enabled = true draft.embedding_model = embeddingModel - if (!draft.score_threshold) + if (draft.score_threshold === undefined || draft.score_threshold === null) draft.score_threshold = ANNOTATION_DEFAULT.score_threshold })) } diff --git a/web/service/annotation.spec.ts b/web/service/annotation.spec.ts new file mode 100644 index 0000000000..60af578108 --- /dev/null +++ b/web/service/annotation.spec.ts @@ -0,0 +1,28 @@ +import { AnnotationEnableStatus } from '@/app/components/app/annotation/type' +import { updateAnnotationStatus } from './annotation' +import { post } from './base' + +vi.mock('./base', () => ({ + post: vi.fn(), +})) + +describe('annotation service', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should preserve zero score threshold when updating annotation status', () => { + updateAnnotationStatus('app-1', AnnotationEnableStatus.enable, { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + }, 0) + + expect(post).toHaveBeenCalledWith('apps/app-1/annotation-reply/enable', { + body: { + embedding_model_name: 'model', + embedding_provider_name: 'provider', + score_threshold: 0, + }, + }) + }) +}) diff --git a/web/service/annotation.ts b/web/service/annotation.ts index 8a19425044..ba8c560b1f 100644 --- a/web/service/annotation.ts +++ b/web/service/annotation.ts @@ -7,7 +7,7 @@ export const fetchAnnotationConfig = (appId: string) => { } export const updateAnnotationStatus = (appId: string, action: AnnotationEnableStatus, embeddingModel?: EmbeddingModelConfig, score?: number) => { let body: any = { - score_threshold: score || ANNOTATION_DEFAULT.score_threshold, + score_threshold: score ?? ANNOTATION_DEFAULT.score_threshold, } if (embeddingModel) { body = {