From bc6ff536dfc5f2e8edb8eba825cce205a90f14c2 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:48:26 +0800 Subject: [PATCH] fix(knowledge-fs): refresh quality evaluation progress --- .../__tests__/evaluation-panel.spec.tsx | 152 +++++++++++++++++- .../quality/quality-evaluation-panel.tsx | 38 ++++- 2 files changed, 183 insertions(+), 7 deletions(-) diff --git a/web/features/new-rag/quality/__tests__/evaluation-panel.spec.tsx b/web/features/new-rag/quality/__tests__/evaluation-panel.spec.tsx index f2069a5334b..2350abdb7c3 100644 --- a/web/features/new-rag/quality/__tests__/evaluation-panel.spec.tsx +++ b/web/features/new-rag/quality/__tests__/evaluation-panel.spec.tsx @@ -110,6 +110,19 @@ const completedRun = { updated_at: '2026-08-19T12:00:02.000Z', } as const +const queuedRun = { + ...completedRun, + items: completedRun.items.map((item) => ({ + ...item, + result: null, + state: 'queued' as const, + })), + revision: 1, + state: 'queued' as const, + summary: { completed: 0, failed: 0, hit_rate: 0, passed: 0, total: 1 }, + updated_at: completedRun.created_at, +} + const completedRunWithEvidence = { ...completedRun, items: [ @@ -145,13 +158,14 @@ const completedRunWithEvidence = { ], } as const -function renderPanel() { - const queryClient = new QueryClient({ +function renderPanel( + queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false }, queries: { retry: false }, }, - }) + }), +) { const Wrapper = ({ children }: { children: ReactNode }) => ( {children} ) @@ -355,6 +369,138 @@ describe('QualityEvaluationPanel', () => { ).toBeVisible() }) + it('shows the queued run without waiting for the evaluation list to refresh', async () => { + const user = userEvent.setup() + const queryClient = new QueryClient({ + defaultOptions: { + mutations: { retry: false }, + queries: { retry: false, staleTime: Number.POSITIVE_INFINITY }, + }, + }) + let listRequestCount = 0 + serviceMock.createReplay.mockResolvedValue(queuedRun) + serviceMock.getReplay.mockImplementation(() => new Promise(() => {})) + serviceMock.listReplays.mockImplementation(() => { + listRequestCount += 1 + return listRequestCount === 1 + ? Promise.resolve({ data: [], next_cursor: null }) + : new Promise(() => {}) + }) + + renderPanel(queryClient) + + await screen.findByText('dataset.newKnowledge.qualityPage.evaluation.emptyTitle') + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluation.run', + }), + ) + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluation.start', + }), + ) + + expect( + await screen.findByRole('heading', { + name: 'dataset.newKnowledge.qualityPage.evaluation.reportTitle', + }), + ).toBeVisible() + expect( + screen.getAllByText('dataset.newKnowledge.qualityPage.evaluation.state.queued'), + ).not.toHaveLength(0) + }) + + it('opens a completed evaluation with the latest progress from the list', async () => { + const user = userEvent.setup() + const queryClient = new QueryClient({ + defaultOptions: { + mutations: { retry: false }, + queries: { retry: false, staleTime: Number.POSITIVE_INFINITY }, + }, + }) + let detailRequestCount = 0 + serviceMock.listReplays + .mockResolvedValueOnce({ data: [queuedRun], next_cursor: null }) + .mockResolvedValueOnce({ data: [completedRun], next_cursor: null }) + serviceMock.getReplay.mockImplementation(() => { + detailRequestCount += 1 + return detailRequestCount === 1 ? Promise.resolve(queuedRun) : new Promise(() => {}) + }) + + renderPanel(queryClient) + + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluation.viewReport', + }), + ) + await screen.findAllByText('dataset.newKnowledge.qualityPage.evaluation.state.queued') + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluationTab', + }), + ) + + expect( + await screen.findAllByText('dataset.newKnowledge.qualityPage.evaluation.state.passed'), + ).not.toHaveLength(0) + expect(serviceMock.listReplays).toHaveBeenCalledTimes(2) + + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluation.viewReport', + }), + ) + + expect( + await screen.findAllByText('dataset.newKnowledge.qualityPage.evaluation.state.passed'), + ).not.toHaveLength(0) + expect(screen.getByText('1/1')).toBeVisible() + }) + + it('keeps newer report progress when the evaluation list is stale', async () => { + const user = userEvent.setup() + const queryClient = new QueryClient({ + defaultOptions: { + mutations: { retry: false }, + queries: { retry: false, staleTime: Number.POSITIVE_INFINITY }, + }, + }) + let detailRequestCount = 0 + serviceMock.listReplays.mockResolvedValue({ data: [queuedRun], next_cursor: null }) + serviceMock.getReplay.mockImplementation(() => { + detailRequestCount += 1 + return detailRequestCount === 1 ? Promise.resolve(completedRun) : new Promise(() => {}) + }) + + renderPanel(queryClient) + + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluation.viewReport', + }), + ) + expect( + await screen.findAllByText('dataset.newKnowledge.qualityPage.evaluation.state.passed'), + ).not.toHaveLength(0) + await user.click( + screen.getByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluationTab', + }), + ) + await user.click( + await screen.findByRole('button', { + name: 'dataset.newKnowledge.qualityPage.evaluation.viewReport', + }), + ) + + expect( + screen.getAllByText('dataset.newKnowledge.qualityPage.evaluation.state.passed'), + ).not.toHaveLength(0) + expect(screen.getByText('1/1')).toBeVisible() + }) + it('opens evidence hit details and identifies matched and missing passages', async () => { const user = userEvent.setup() serviceMock.listReplays.mockResolvedValue({ data: [completedRun], next_cursor: null }) diff --git a/web/features/new-rag/quality/quality-evaluation-panel.tsx b/web/features/new-rag/quality/quality-evaluation-panel.tsx index 0f171fa29a5..448cfbee31b 100644 --- a/web/features/new-rag/quality/quality-evaluation-panel.tsx +++ b/web/features/new-rag/quality/quality-evaluation-panel.tsx @@ -31,6 +31,7 @@ type ReplayItem = KnowledgeFsQualityReplayResponse['items'][number] const pageSize = 20 const activeQuestionCountLimit = 100 +const replayRefreshInterval = 1000 const activeReplayStates = new Set(['queued', 'running']) function evaluationStateClassName(state: ReplayState) { @@ -50,6 +51,13 @@ function formatDuration(milliseconds?: number | null) { return `${(milliseconds / 1000).toFixed(milliseconds < 10_000 ? 1 : 0)} s` } +function keepLatestReplay( + current: KnowledgeFsQualityReplayResponse | undefined, + candidate: KnowledgeFsQualityReplayResponse, +) { + return current && current.revision >= candidate.revision ? current : candidate +} + function EvaluationState({ state }: { state: ReplayState }) { const { t } = useTranslation('dataset') return ( @@ -85,8 +93,11 @@ export function EvaluationReport({ }) const detailQuery = useQuery({ ...detailOptions, + refetchOnMount: 'always', refetchInterval: (query) => - query.state.data && activeReplayStates.has(query.state.data.state) ? 1500 : false, + query.state.data && activeReplayStates.has(query.state.data.state) + ? replayRefreshInterval + : false, }) if (detailQuery.isLoading) @@ -475,11 +486,19 @@ function RunEvaluationDialogContent({ headers: { 'Idempotency-Key': crypto.randomUUID() }, params: { control_space_id: knowledgeSpaceId }, }) - await queryClient.invalidateQueries({ + const detailOptions = + consoleQuery.knowledgeFs.spaces.byControlSpaceId.quality.replayRuns.byRunId.get.queryOptions( + { + input: { params: { control_space_id: knowledgeSpaceId, run_id: run.id } }, + }, + ) + queryClient.setQueryData(detailOptions.queryKey, (current) => keepLatestReplay(current, run)) + void queryClient.invalidateQueries({ queryKey: consoleQuery.knowledgeFs.spaces.byControlSpaceId.quality.replayRuns.get.key({ input: { params: { control_space_id: knowledgeSpaceId } }, type: 'infinite', }), + refetchType: 'none', }) onClose() onRunStarted(run.id) @@ -560,6 +579,7 @@ export function QualityEvaluationPanel({ const { space } = useKnowledgeSpace() const canEdit = useKnowledgeSpacePermission('knowledge_space_edit') const knowledgeSpaceId = space.control_space_id + const queryClient = useQueryClient() const [dialogOpen, setDialogOpen] = useState(false) const listOptions = consoleQuery.knowledgeFs.spaces.byControlSpaceId.quality.replayRuns.get.infiniteOptions({ @@ -575,15 +595,25 @@ export function QualityEvaluationPanel({ }) const listQuery = useInfiniteQuery({ ...listOptions, + refetchOnMount: 'always', refetchInterval: (query) => query.state.data?.pages.some((page) => page.data.some((run) => activeReplayStates.has(run.state)), ) - ? 1500 + ? replayRefreshInterval : false, }) const runs = listQuery.data?.pages.flatMap((page) => page.data) ?? [] + const openReport = (run: KnowledgeFsQualityReplayResponse) => { + const detailOptions = + consoleQuery.knowledgeFs.spaces.byControlSpaceId.quality.replayRuns.byRunId.get.queryOptions({ + input: { params: { control_space_id: knowledgeSpaceId, run_id: run.id } }, + }) + queryClient.setQueryData(detailOptions.queryKey, (current) => keepLatestReplay(current, run)) + onOpenReport(run.id) + } + return ( <>
@@ -657,7 +687,7 @@ export function QualityEvaluationPanel({ variant="secondary" size="small" className="ml-auto" - onClick={() => onOpenReport(run.id)} + onClick={() => openReport(run)} > {t(($) => $['newKnowledge.qualityPage.evaluation.viewReport'])}