fix(knowledge-fs): refresh quality evaluation progress

This commit is contained in:
Stephen Zhou 2026-09-02 20:48:26 +08:00
parent 2f8dd177d3
commit bc6ff536df
No known key found for this signature in database
2 changed files with 183 additions and 7 deletions

View File

@ -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 }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
@ -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 })

View File

@ -31,6 +31,7 @@ type ReplayItem = KnowledgeFsQualityReplayResponse['items'][number]
const pageSize = 20
const activeQuestionCountLimit = 100
const replayRefreshInterval = 1000
const activeReplayStates = new Set<ReplayState>(['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 (
<>
<section className="min-w-0">
@ -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'])}
</Button>