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'])}