mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(knowledge-fs): correct quality retest workflow
This commit is contained in:
parent
b20b6f3c18
commit
02151d0afa
@ -2141,6 +2141,11 @@ describe("database quality-control repository", () => {
|
||||
await expect(
|
||||
repositoryFor().repository.updateBadCase({ ...base, status: "fixed" }),
|
||||
).rejects.toThrow("Invalid bad-case transition open -> fixed");
|
||||
await expect(
|
||||
repositoryFor({ badCase: { ...badCaseRow(), status: "fixed" } }).repository.updateBadCase(
|
||||
base,
|
||||
),
|
||||
).resolves.toMatchObject({ revision: 2, status: "dismissed" });
|
||||
await expect(
|
||||
repositoryFor().repository.updateBadCase({ ...base, status: "replaying" }),
|
||||
).rejects.toThrow("requires a replay run");
|
||||
|
||||
@ -2058,7 +2058,7 @@ function mapHistoryEvent(row: DatabaseRow): QualityHistoryEvent {
|
||||
function validateBadCaseTransition(from: QualityBadCaseState, to: QualityBadCaseState) {
|
||||
const allowed: Readonly<Record<QualityBadCaseState, readonly QualityBadCaseState[]>> = {
|
||||
dismissed: ["open"],
|
||||
fixed: ["open", "replaying"],
|
||||
fixed: ["dismissed", "open", "replaying"],
|
||||
open: ["dismissed", "replaying"],
|
||||
replaying: ["dismissed", "fixed", "open"],
|
||||
};
|
||||
|
||||
@ -8,7 +8,6 @@ import { QualityPage } from '../quality/quality-page'
|
||||
const serviceMock = vi.hoisted(() => ({
|
||||
bulkImport: vi.fn(),
|
||||
createGolden: vi.fn(),
|
||||
createReplay: vi.fn(),
|
||||
deleteGolden: vi.fn(),
|
||||
getBadCase: vi.fn(),
|
||||
getBadCases: vi.fn(),
|
||||
@ -52,7 +51,6 @@ vi.mock('@/service/client', () => ({
|
||||
traceReference: { get: serviceMock.getTraceReference },
|
||||
},
|
||||
},
|
||||
replayRuns: { post: serviceMock.createReplay },
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -857,71 +855,33 @@ describe('QualityPage', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reuses the replay idempotency key after a partial failure', async () => {
|
||||
let linked = false
|
||||
let rejectReplayPatch = true
|
||||
serviceMock.getBadCase.mockImplementation(async () => ({
|
||||
created_at: '2026-07-28T00:00:00Z',
|
||||
id: 'bad-1',
|
||||
question: 'Refund after activation',
|
||||
reason: 'coverage gap',
|
||||
replay_run_id: null,
|
||||
revision: linked ? 2 : 1,
|
||||
status: 'open',
|
||||
tags: linked ? ['billing', 'golden-question:golden-2'] : ['billing'],
|
||||
updated_at: '2026-07-28T00:00:00Z',
|
||||
}))
|
||||
serviceMock.updateBadCase.mockImplementation(
|
||||
async (input: { body: { status: string; tags?: string[] } }) => {
|
||||
if (input.body.status === 'open') {
|
||||
linked = true
|
||||
return {
|
||||
...(await serviceMock.getBadCase()),
|
||||
revision: 2,
|
||||
tags: input.body.tags,
|
||||
}
|
||||
}
|
||||
if (rejectReplayPatch) {
|
||||
rejectReplayPatch = false
|
||||
throw new Error('response lost')
|
||||
}
|
||||
return {
|
||||
...(await serviceMock.getBadCase()),
|
||||
replay_run_id: 'replay-1',
|
||||
revision: 3,
|
||||
status: 'replaying',
|
||||
}
|
||||
},
|
||||
)
|
||||
serviceMock.createReplay.mockResolvedValue({ id: 'replay-1', revision: 1, state: 'queued' })
|
||||
it('opens the source trace in retrieval test and requests one retest', async () => {
|
||||
navigationMock.tab = 'bad-cases'
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
|
||||
await screen.findByText('Refund after activation')
|
||||
const replay = async () => {
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /dataset\.newKnowledge\.qualityPage\.questionActions/,
|
||||
}),
|
||||
)
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', {
|
||||
name: 'dataset.newKnowledge.qualityPage.replay',
|
||||
}),
|
||||
)
|
||||
}
|
||||
await replay()
|
||||
await waitFor(() => expect(serviceMock.createReplay).toHaveBeenCalledTimes(1))
|
||||
await replay()
|
||||
await waitFor(() => expect(serviceMock.createReplay).toHaveBeenCalledTimes(2))
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: /dataset\.newKnowledge\.qualityPage\.questionActions/,
|
||||
}),
|
||||
)
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', {
|
||||
name: 'dataset.newKnowledge.qualityPage.replay',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(serviceMock.createGolden).toHaveBeenCalledTimes(2)
|
||||
const firstHeaders = serviceMock.createReplay.mock.calls[0]?.[0].headers
|
||||
expect(serviceMock.createReplay.mock.calls[1]?.[0].headers).toEqual(firstHeaders)
|
||||
await waitFor(() =>
|
||||
expect(routerMock.push).toHaveBeenCalledWith(
|
||||
'/datasets/new/space-1/retrieval?retest=trace-42&trace=trace-42',
|
||||
),
|
||||
)
|
||||
expect(serviceMock.createGolden).not.toHaveBeenCalled()
|
||||
expect(serviceMock.updateBadCase).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('replaces a stale golden-question link before replaying a bad case', async () => {
|
||||
it('creates a golden question and dismisses its source bad case', async () => {
|
||||
serviceMock.getBadCase.mockResolvedValue({
|
||||
created_at: '2026-07-28T00:00:00Z',
|
||||
id: 'bad-1',
|
||||
@ -941,20 +901,12 @@ describe('QualityPage', () => {
|
||||
tags: ['billing'],
|
||||
updated_at: '2026-07-29T00:00:00Z',
|
||||
})
|
||||
serviceMock.updateBadCase
|
||||
.mockResolvedValueOnce({
|
||||
...(await serviceMock.getBadCase()),
|
||||
revision: 2,
|
||||
tags: ['billing', 'golden-question:replacement-golden'],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
...(await serviceMock.getBadCase()),
|
||||
replay_run_id: 'replay-1',
|
||||
revision: 3,
|
||||
status: 'replaying',
|
||||
tags: ['billing', 'golden-question:replacement-golden'],
|
||||
})
|
||||
serviceMock.createReplay.mockResolvedValue({ id: 'replay-1', revision: 1, state: 'queued' })
|
||||
serviceMock.updateBadCase.mockResolvedValue({
|
||||
...(await serviceMock.getBadCase()),
|
||||
revision: 2,
|
||||
status: 'dismissed',
|
||||
tags: ['billing'],
|
||||
})
|
||||
navigationMock.tab = 'bad-cases'
|
||||
const user = userEvent.setup()
|
||||
renderPage()
|
||||
@ -967,20 +919,21 @@ describe('QualityPage', () => {
|
||||
)
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', {
|
||||
name: 'dataset.newKnowledge.qualityPage.replay',
|
||||
name: 'dataset.newKnowledge.qualityPage.toGolden',
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(serviceMock.createReplay).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
body: { golden_question_ids: ['replacement-golden'] },
|
||||
}),
|
||||
),
|
||||
await user.type(
|
||||
screen.getByPlaceholderText('dataset.newKnowledge.qualityPage.annotationPlaceholder'),
|
||||
'Expected answer',
|
||||
)
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'dataset.newKnowledge.qualityPage.promote' }),
|
||||
)
|
||||
|
||||
await waitFor(() => expect(serviceMock.createGolden).toHaveBeenCalledTimes(1))
|
||||
expect(serviceMock.createGolden.mock.calls[0]?.[0]).toEqual({
|
||||
body: {
|
||||
annotation: 'coverage gap',
|
||||
annotation: 'Expected answer',
|
||||
expected_evidence_ids: [],
|
||||
match_policy: 'all',
|
||||
question: 'Refund after activation',
|
||||
@ -992,8 +945,8 @@ describe('QualityPage', () => {
|
||||
expect(serviceMock.updateBadCase.mock.calls[0]?.[0]).toEqual({
|
||||
body: {
|
||||
expected_revision: 1,
|
||||
status: 'open',
|
||||
tags: ['billing', 'golden-question:replacement-golden'],
|
||||
status: 'dismissed',
|
||||
tags: ['billing'],
|
||||
},
|
||||
params: { bad_case_id: 'bad-1', control_space_id: 'space-1' },
|
||||
})
|
||||
|
||||
@ -1488,11 +1488,17 @@ describe('RetrievalTestPage', () => {
|
||||
})
|
||||
expect(makeBadCaseButton).toHaveClass('bg-components-button-secondary-bg')
|
||||
await user.click(makeBadCaseButton)
|
||||
expect(apiMock.createBadCase).not.toHaveBeenCalled()
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', {
|
||||
name: 'dataset.newKnowledge.qualityPage.reasonValues.lowScore',
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(apiMock.createBadCase).toHaveBeenCalledWith({
|
||||
body: {
|
||||
reason: 'retrieval-miss',
|
||||
reason: 'low-score',
|
||||
tags: ['retrieval-test'],
|
||||
trace_id: 'trace-1',
|
||||
},
|
||||
@ -1609,6 +1615,11 @@ describe('RetrievalTestPage', () => {
|
||||
name: 'dataset.newKnowledge.retrievalTest.makeBadCase',
|
||||
}),
|
||||
)
|
||||
await user.click(
|
||||
await screen.findByRole('menuitem', {
|
||||
name: 'dataset.newKnowledge.qualityPage.reasonValues.retrievalMiss',
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(apiMock.createBadCase).toHaveBeenCalledWith({
|
||||
@ -1622,6 +1633,31 @@ describe('RetrievalTestPage', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('runs a one-shot retest from a linked production trace', async () => {
|
||||
apiMock.traceDetail = {
|
||||
completed: true,
|
||||
created_at: '2026-07-01T00:00:00.000Z',
|
||||
id: 'trace-old',
|
||||
mode: 'deep',
|
||||
profile: {},
|
||||
query: 'Retest the refund exception',
|
||||
scores: {},
|
||||
stages: [],
|
||||
}
|
||||
const { onUrlUpdate } = renderPage({
|
||||
searchParams: '?trace=trace-old&retest=trace-old',
|
||||
})
|
||||
|
||||
await waitFor(() =>
|
||||
expect(apiMock.queryAdmission).toHaveBeenCalledWith({
|
||||
body: { mode: 'deep', query: 'Retest the refund exception' },
|
||||
params: { control_space_id: 'space-1' },
|
||||
}),
|
||||
)
|
||||
expect(apiMock.queryAdmission).toHaveBeenCalledTimes(1)
|
||||
expect(onUrlUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens retrieval evidence through its logical document instead of its asset', async () => {
|
||||
apiMock.traces = [
|
||||
{
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
@ -44,23 +44,15 @@ const emptyDraft: GoldenQuestionDraft = {
|
||||
const goldenLinkPrefix = 'golden-question:'
|
||||
const pageSize = 50
|
||||
|
||||
function createIdempotencyKey() {
|
||||
return `quality-replay-${
|
||||
globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||
}`
|
||||
}
|
||||
|
||||
function visibleTags(tags: string[]) {
|
||||
return tags.filter((tag) => !tag.startsWith(goldenLinkPrefix))
|
||||
}
|
||||
|
||||
function linkedGoldenQuestionId(tags: string[]) {
|
||||
return tags.find((tag) => tag.startsWith(goldenLinkPrefix))?.slice(goldenLinkPrefix.length)
|
||||
}
|
||||
|
||||
function Reason({ question, reason, tags }: { question?: string; reason: string; tags: string[] }) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const normalized = reason.toLowerCase()
|
||||
if (normalized === 'low-score' || (normalized.includes('low') && normalized.includes('score')))
|
||||
return t(($) => $['newKnowledge.qualityPage.reasonValues.lowScore'])
|
||||
if (normalized.includes('outdated'))
|
||||
return t(($) => $['newKnowledge.qualityPage.reasonValues.outdatedContent'])
|
||||
if (
|
||||
@ -148,8 +140,6 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
|
||||
const [dialogSubmitting, setDialogSubmitting] = useState(false)
|
||||
const [importOpen, setImportOpen] = useState(false)
|
||||
const [pendingBadCaseId, setPendingBadCaseId] = useState<string>()
|
||||
const replayIdempotencyKeysRef = useRef(new Map<string, string>())
|
||||
const pendingGoldenQuestionIdsRef = useRef(new Map<string, string>())
|
||||
const [dialog, setDialog] = useState<
|
||||
| { key: string; mode: 'create'; value: GoldenQuestionDraft }
|
||||
| { id: string; key: string; mode: 'edit'; value: GoldenQuestionDraft }
|
||||
@ -227,52 +217,6 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
|
||||
params: { bad_case_id: badCaseId, control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
|
||||
const ensureLinkedGoldenQuestion = async (
|
||||
item: KnowledgeFsBadCaseResponse,
|
||||
draft: GoldenQuestionDraft,
|
||||
) => {
|
||||
const current = await getBadCase(item.id)
|
||||
const linkedId = linkedGoldenQuestionId(current.tags)
|
||||
|
||||
let goldenQuestionId = pendingGoldenQuestionIdsRef.current.get(item.id)
|
||||
if (!goldenQuestionId) {
|
||||
const created = await createGoldenMutation.mutateAsync({
|
||||
body: {
|
||||
...goldenQuestionPayload(draft),
|
||||
source_bad_case_id: item.id,
|
||||
},
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
goldenQuestionId = created.id
|
||||
pendingGoldenQuestionIdsRef.current.set(item.id, goldenQuestionId)
|
||||
}
|
||||
if (linkedId === goldenQuestionId) {
|
||||
pendingGoldenQuestionIdsRef.current.delete(item.id)
|
||||
return { badCase: current, goldenQuestionId }
|
||||
}
|
||||
|
||||
try {
|
||||
const badCase =
|
||||
await consoleClient.knowledgeFs.spaces.byControlSpaceId.quality.badCases.byBadCaseId.patch({
|
||||
body: {
|
||||
expected_revision: current.revision,
|
||||
status: current.status,
|
||||
tags: [...visibleTags(current.tags), `${goldenLinkPrefix}${goldenQuestionId}`],
|
||||
},
|
||||
params: { bad_case_id: current.id, control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
pendingGoldenQuestionIdsRef.current.delete(item.id)
|
||||
return { badCase, goldenQuestionId }
|
||||
} catch (error) {
|
||||
const refreshed = await getBadCase(item.id).catch(() => undefined)
|
||||
if (refreshed && linkedGoldenQuestionId(refreshed.tags) === goldenQuestionId) {
|
||||
pendingGoldenQuestionIdsRef.current.delete(item.id)
|
||||
return { badCase: refreshed, goldenQuestionId }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const submitDialog = async (draft: GoldenQuestionDraft) => {
|
||||
if (!dialog) return
|
||||
setDialogError(undefined)
|
||||
@ -291,9 +235,29 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
|
||||
})
|
||||
toast.success(t(($) => $['newKnowledge.qualityPage.createdToast']))
|
||||
} else {
|
||||
const badCase = badCases.find((item) => item.id === dialog.id)
|
||||
if (!badCase) throw new Error('Bad case is unavailable')
|
||||
await ensureLinkedGoldenQuestion(badCase, draft)
|
||||
const badCase = await getBadCase(dialog.id)
|
||||
await createGoldenMutation.mutateAsync({
|
||||
body: {
|
||||
...goldenQuestionPayload(draft),
|
||||
source_bad_case_id: badCase.id,
|
||||
},
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
try {
|
||||
await consoleClient.knowledgeFs.spaces.byControlSpaceId.quality.badCases.byBadCaseId.patch(
|
||||
{
|
||||
body: {
|
||||
expected_revision: badCase.revision,
|
||||
status: 'dismissed',
|
||||
tags: visibleTags(badCase.tags),
|
||||
},
|
||||
params: { bad_case_id: badCase.id, control_space_id: knowledgeSpaceId },
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
const refreshed = await getBadCase(dialog.id).catch(() => undefined)
|
||||
if (refreshed?.status !== 'dismissed') throw error
|
||||
}
|
||||
toast.success(t(($) => $['newKnowledge.qualityPage.promotedToast']))
|
||||
}
|
||||
await invalidateQuality()
|
||||
@ -341,43 +305,18 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
|
||||
const replayBadCase = async (item: KnowledgeFsBadCaseResponse) => {
|
||||
setPendingBadCaseId(item.id)
|
||||
try {
|
||||
const { badCase, goldenQuestionId } = await ensureLinkedGoldenQuestion(item, {
|
||||
annotation: item.reason,
|
||||
expectedEvidenceIds: [],
|
||||
matchPolicy: 'all',
|
||||
question: item.question ?? '',
|
||||
tags: visibleTags(item.tags),
|
||||
})
|
||||
let idempotencyKey = replayIdempotencyKeysRef.current.get(item.id)
|
||||
if (!idempotencyKey) {
|
||||
idempotencyKey = createIdempotencyKey()
|
||||
replayIdempotencyKeysRef.current.set(item.id, idempotencyKey)
|
||||
}
|
||||
const replay =
|
||||
await consoleClient.knowledgeFs.spaces.byControlSpaceId.quality.replayRuns.post({
|
||||
body: { golden_question_ids: [goldenQuestionId] },
|
||||
headers: { 'Idempotency-Key': idempotencyKey },
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
try {
|
||||
await consoleClient.knowledgeFs.spaces.byControlSpaceId.quality.badCases.byBadCaseId.patch({
|
||||
body: {
|
||||
expected_revision: badCase.revision,
|
||||
replay_run_id: replay.id,
|
||||
status: 'replaying',
|
||||
tags: badCase.tags,
|
||||
const reference =
|
||||
await consoleClient.knowledgeFs.spaces.byControlSpaceId.quality.badCases.byBadCaseId.traceReference.get(
|
||||
{
|
||||
params: { bad_case_id: item.id, control_space_id: knowledgeSpaceId },
|
||||
},
|
||||
params: { bad_case_id: badCase.id, control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
} catch (error) {
|
||||
const current = await getBadCase(item.id).catch(() => undefined)
|
||||
if (current?.replay_run_id !== replay.id) throw error
|
||||
}
|
||||
replayIdempotencyKeysRef.current.delete(item.id)
|
||||
await invalidateQuality()
|
||||
toast.success(t(($) => $['newKnowledge.qualityPage.replayStartedToast']))
|
||||
)
|
||||
const search = new URLSearchParams({
|
||||
retest: reference.trace_id,
|
||||
trace: reference.trace_id,
|
||||
})
|
||||
router.push(`${newKnowledgeRetrievalTestPath(knowledgeSpaceId)}?${search.toString()}`)
|
||||
} catch {
|
||||
await invalidateQuality()
|
||||
toast.error(t(($) => $.unknownError))
|
||||
} finally {
|
||||
setPendingBadCaseId(undefined)
|
||||
|
||||
@ -17,11 +17,17 @@ import type { ResearchTaskProgressEvent } from './services/research-task-events'
|
||||
import type { MarkdownProps } from '@/app/components/base/markdown'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { matchesKeyboardEvent } from '@tanstack/react-hotkeys'
|
||||
import { skipToken, useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { parseAsString, useQueryStates } from 'nuqs'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { Link as MarkdownLink } from '@/app/components/base/markdown-blocks'
|
||||
@ -75,6 +81,8 @@ type ComposerDraft = {
|
||||
|
||||
type QualityDecision = 'bad-case' | 'golden'
|
||||
|
||||
type BadCaseReason = 'low-score' | 'retrieval-miss'
|
||||
|
||||
type ResearchExpansionState = Partial<Record<'active' | 'terminal', boolean>>
|
||||
|
||||
type GoldenQuestionPromotion = {
|
||||
@ -83,8 +91,6 @@ type GoldenQuestionPromotion = {
|
||||
value: GoldenQuestionDraft
|
||||
}
|
||||
|
||||
const retrievalTestBadCaseReason = 'retrieval-miss'
|
||||
|
||||
const researchStageOrder = ['planning', 'retrieving', 'analyzing', 'generating'] as const
|
||||
type ResearchStage = (typeof researchStageOrder)[number]
|
||||
const runRetrievalHotkey = 'Mod+Enter' satisfies Hotkey
|
||||
@ -114,6 +120,11 @@ function timeValue(value: number) {
|
||||
return value < 10_000_000_000 ? value * 1000 : value
|
||||
}
|
||||
|
||||
function normalizedRetrievalTestMode(mode?: string): RetrievalTestMode {
|
||||
if (mode === 'deep' || mode === 'research') return mode
|
||||
return 'fast'
|
||||
}
|
||||
|
||||
function formatRecordTime(value: number) {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
@ -736,14 +747,16 @@ function QualityActions({
|
||||
badCaseAvailable,
|
||||
decision,
|
||||
noResults,
|
||||
onDecision,
|
||||
onBadCase,
|
||||
onGolden,
|
||||
pending,
|
||||
qualityHref,
|
||||
}: {
|
||||
badCaseAvailable: boolean
|
||||
decision?: QualityDecision
|
||||
noResults?: boolean
|
||||
onDecision: (decision: QualityDecision) => Promise<void>
|
||||
onBadCase: (reason: BadCaseReason) => Promise<void>
|
||||
onGolden: () => void
|
||||
pending?: boolean
|
||||
qualityHref: string
|
||||
}) {
|
||||
@ -776,22 +789,30 @@ function QualityActions({
|
||||
return (
|
||||
<div className="flex shrink-0 items-center justify-end gap-3 border-t border-divider-regular pt-4 pb-1">
|
||||
{badCaseAvailable && (
|
||||
<Button
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
variant={noResults ? 'secondary' : 'ghost'}
|
||||
onClick={() => void onDecision('bad-case')}
|
||||
>
|
||||
<span aria-hidden className="i-ri-thumb-down-line size-4" />
|
||||
{t(($) => $['newKnowledge.retrievalTest.makeBadCase'])}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
disabled={pending}
|
||||
render={<Button loading={pending} variant={noResults ? 'secondary' : 'ghost'} />}
|
||||
>
|
||||
<span aria-hidden className="i-ri-thumb-down-line size-4" />
|
||||
{t(($) => $['newKnowledge.retrievalTest.makeBadCase'])}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent placement="top-end" sideOffset={4} popupClassName="w-44">
|
||||
<DropdownMenuItem onClick={() => void onBadCase('low-score')}>
|
||||
{t(($) => $['newKnowledge.qualityPage.reasonValues.lowScore'])}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => void onBadCase('retrieval-miss')}>
|
||||
{t(($) => $['newKnowledge.qualityPage.reasonValues.retrievalMiss'])}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{!noResults && (
|
||||
<Button
|
||||
disabled={pending}
|
||||
loading={pending}
|
||||
variant="secondary"
|
||||
onClick={() => void onDecision('golden')}
|
||||
onClick={() => void onGolden()}
|
||||
>
|
||||
<span aria-hidden className="i-ri-thumb-up-line size-4" />
|
||||
{t(($) => $['newKnowledge.retrievalTest.keepGoldenQuestion'])}
|
||||
@ -1089,9 +1110,14 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
} = useKnowledgeModelSetupGuard(knowledgeSpaceId)
|
||||
const [linkedSelection, setLinkedSelection] = useQueryStates({
|
||||
research: parseAsString,
|
||||
retest: parseAsString,
|
||||
trace: parseAsString,
|
||||
})
|
||||
const { research: linkedResearchId, trace: linkedTraceId } = linkedSelection
|
||||
const {
|
||||
research: linkedResearchId,
|
||||
retest: linkedRetestTraceId,
|
||||
trace: linkedTraceId,
|
||||
} = linkedSelection
|
||||
const [composerDraft, setComposerDraft] = useState<ComposerDraft>({ mode: 'fast', query: '' })
|
||||
const [localRun, setLocalRun] = useState<LocalQueryRun>()
|
||||
const [localSelected, setLocalSelected] = useState<SelectedRun>()
|
||||
@ -1118,6 +1144,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
taskId: string
|
||||
}>()
|
||||
const queryAbortControllerRef = useRef<AbortController>(undefined)
|
||||
const consumedRetestTraceIdRef = useRef<string | undefined>(undefined)
|
||||
const runInFlightRef = useRef(false)
|
||||
|
||||
useEffect(
|
||||
@ -1237,37 +1264,12 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
? (selectedResearchTaskFromHistory ?? researchDetailQuery.data)
|
||||
: undefined
|
||||
const selectedHistoryRecord = selected?.kind === 'local' ? undefined : selectedRecord
|
||||
const query =
|
||||
composerDraft.selectionKey === selectedHistoryKey
|
||||
? composerDraft.query
|
||||
: (selectedHistoryRecord?.query ?? selectedResearchTask?.query ?? '')
|
||||
const mode =
|
||||
composerDraft.selectionKey === selectedHistoryKey
|
||||
? composerDraft.mode
|
||||
: (selectedHistoryRecord?.mode ??
|
||||
(selectedResearchTask?.mode === 'research'
|
||||
? 'research'
|
||||
: selectedResearchTask?.mode === 'deep'
|
||||
? 'deep'
|
||||
: 'fast'))
|
||||
const selectedResearchActive = researchTaskIsActive(selectedResearchTask)
|
||||
const selectedResearchActiveRef = useRef(selectedResearchActive)
|
||||
useEffect(() => {
|
||||
selectedResearchActiveRef.current = selectedResearchActive
|
||||
}, [selectedResearchActive])
|
||||
const selectedResearchDefaultExpanded = researchTaskIsActive(selectedResearchTask)
|
||||
const selectedResearchExpansionPhase = selectedResearchDefaultExpanded ? 'active' : 'terminal'
|
||||
const selectedResearchExpanded = selectedResearchTask
|
||||
? (researchExpanded[selectedResearchTask.id]?.[selectedResearchExpansionPhase] ??
|
||||
selectedResearchDefaultExpanded)
|
||||
: false
|
||||
const selectedTraceId =
|
||||
selected?.kind === 'trace'
|
||||
? selected.id
|
||||
: selected?.kind === 'local'
|
||||
? localRun?.traceId
|
||||
: undefined
|
||||
|
||||
const traceDetailQuery = useQuery({
|
||||
...consoleQuery.knowledgeFs.spaces.byControlSpaceId.traces.byTraceId.get.queryOptions({
|
||||
input: {
|
||||
@ -1279,6 +1281,30 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
}),
|
||||
enabled: Boolean(selectedTraceId) && !selectedFailed,
|
||||
})
|
||||
const query =
|
||||
composerDraft.selectionKey === selectedHistoryKey
|
||||
? composerDraft.query
|
||||
: (selectedHistoryRecord?.query ??
|
||||
selectedResearchTask?.query ??
|
||||
traceDetailQuery.data?.query ??
|
||||
'')
|
||||
const mode: RetrievalTestMode =
|
||||
composerDraft.selectionKey === selectedHistoryKey
|
||||
? composerDraft.mode
|
||||
: normalizedRetrievalTestMode(
|
||||
selectedHistoryRecord?.mode ?? selectedResearchTask?.mode ?? traceDetailQuery.data?.mode,
|
||||
)
|
||||
const selectedResearchActive = researchTaskIsActive(selectedResearchTask)
|
||||
const selectedResearchActiveRef = useRef(selectedResearchActive)
|
||||
useEffect(() => {
|
||||
selectedResearchActiveRef.current = selectedResearchActive
|
||||
}, [selectedResearchActive])
|
||||
const selectedResearchDefaultExpanded = researchTaskIsActive(selectedResearchTask)
|
||||
const selectedResearchExpansionPhase = selectedResearchDefaultExpanded ? 'active' : 'terminal'
|
||||
const selectedResearchExpanded = selectedResearchTask
|
||||
? (researchExpanded[selectedResearchTask.id]?.[selectedResearchExpansionPhase] ??
|
||||
selectedResearchDefaultExpanded)
|
||||
: false
|
||||
const traceEvidenceQuery = useInfiniteQuery({
|
||||
...consoleQuery.knowledgeFs.spaces.byControlSpaceId.traces.byTraceId.evidence.get.infiniteOptions(
|
||||
{
|
||||
@ -1573,12 +1599,13 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
const selectRecord = (record: RetrievalTestRecord) => {
|
||||
if (record.kind === 'local') {
|
||||
setLocalSelected({ id: record.id, kind: record.kind })
|
||||
void setLinkedSelection({ research: null, trace: null }, { history: 'push' })
|
||||
void setLinkedSelection({ research: null, retest: null, trace: null }, { history: 'push' })
|
||||
} else {
|
||||
setLocalSelected(undefined)
|
||||
void setLinkedSelection(
|
||||
{
|
||||
research: record.kind === 'research' ? record.id : null,
|
||||
retest: null,
|
||||
trace: record.kind === 'trace' ? record.id : null,
|
||||
},
|
||||
{ history: 'push', shallow: false },
|
||||
@ -1592,23 +1619,24 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
setShowAll(false)
|
||||
}
|
||||
|
||||
const saveQualityDecision = async (decision: QualityDecision) => {
|
||||
const startGoldenPromotion = () => {
|
||||
if (!resultKey || !selectedQuery) return
|
||||
setGoldenPromotionError(undefined)
|
||||
setGoldenPromotion({
|
||||
evidenceOptions: goldenQuestionEvidenceOptions(currentEvidence),
|
||||
resultKey,
|
||||
value: {
|
||||
annotation: '',
|
||||
expectedEvidenceIds: [],
|
||||
matchPolicy: 'all',
|
||||
question: selectedQuery,
|
||||
tags: ['retrieval-test'],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const saveBadCase = async (reason: BadCaseReason) => {
|
||||
if (!resultKey || !selectedQuery) return
|
||||
if (decision === 'golden') {
|
||||
setGoldenPromotionError(undefined)
|
||||
setGoldenPromotion({
|
||||
evidenceOptions: goldenQuestionEvidenceOptions(currentEvidence),
|
||||
resultKey,
|
||||
value: {
|
||||
annotation: '',
|
||||
expectedEvidenceIds: [],
|
||||
matchPolicy: 'all',
|
||||
question: selectedQuery,
|
||||
tags: ['retrieval-test'],
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
setQualityPendingKey(resultKey)
|
||||
try {
|
||||
if (!selectedTraceId) {
|
||||
@ -1617,13 +1645,13 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
}
|
||||
await consoleClient.knowledgeFs.spaces.byControlSpaceId.quality.badCases.post({
|
||||
body: {
|
||||
reason: retrievalTestBadCaseReason,
|
||||
reason,
|
||||
tags: ['retrieval-test'],
|
||||
trace_id: selectedTraceId,
|
||||
},
|
||||
params: { control_space_id: knowledgeSpaceId },
|
||||
})
|
||||
setQualityDecisions((current) => ({ ...current, [resultKey]: decision }))
|
||||
setQualityDecisions((current) => ({ ...current, [resultKey]: 'bad-case' }))
|
||||
} catch {
|
||||
toast.error(t(($) => $.unknownError))
|
||||
} finally {
|
||||
@ -1662,11 +1690,12 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
}
|
||||
}
|
||||
|
||||
const runFastQuery = async () => {
|
||||
const cleanQuery = query.trim()
|
||||
const runFastQuery = async (input?: { mode: RetrievalTestMode; query: string }) => {
|
||||
const cleanQuery = (input?.query ?? query).trim()
|
||||
if (!cleanQuery || runInFlightRef.current) return
|
||||
runInFlightRef.current = true
|
||||
const runMode = mode === 'deep' ? 'deep' : 'fast'
|
||||
const requestedMode = input?.mode ?? mode
|
||||
const runMode = requestedMode === 'deep' ? 'deep' : 'fast'
|
||||
if (
|
||||
(
|
||||
await ensureModelReady({
|
||||
@ -1693,7 +1722,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
status: 'running',
|
||||
})
|
||||
setLocalSelected({ id, kind: 'local' })
|
||||
void setLinkedSelection({ research: null, trace: null }, { history: 'replace' })
|
||||
void setLinkedSelection({ research: null, retest: null, trace: null }, { history: 'replace' })
|
||||
setShowAll(false)
|
||||
const events: KnowledgeQueryEvent[] = []
|
||||
try {
|
||||
@ -1776,8 +1805,8 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
}
|
||||
}
|
||||
|
||||
const startResearch = async () => {
|
||||
const cleanQuery = query.trim()
|
||||
const startResearch = async (input?: { query: string }) => {
|
||||
const cleanQuery = (input?.query ?? query).trim()
|
||||
if (!cleanQuery || runInFlightRef.current) return
|
||||
runInFlightRef.current = true
|
||||
try {
|
||||
@ -1808,7 +1837,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
})
|
||||
setLocalSelected(undefined)
|
||||
void setLinkedSelection(
|
||||
{ research: task.id, trace: null },
|
||||
{ research: task.id, retest: null, trace: null },
|
||||
{ history: 'push', shallow: false },
|
||||
)
|
||||
setShowAll(false)
|
||||
@ -1837,6 +1866,25 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
else void runFastQuery()
|
||||
}
|
||||
|
||||
const runRetest = useEffectEvent((command: { mode: RetrievalTestMode; query: string }) => {
|
||||
if (command.mode === 'research') void startResearch({ query: command.query })
|
||||
else void runFastQuery(command)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!linkedRetestTraceId ||
|
||||
linkedTraceId !== linkedRetestTraceId ||
|
||||
selected?.kind !== 'trace' ||
|
||||
selected.id !== linkedRetestTraceId ||
|
||||
!query.trim() ||
|
||||
consumedRetestTraceIdRef.current === linkedRetestTraceId
|
||||
)
|
||||
return
|
||||
consumedRetestTraceIdRef.current = linkedRetestTraceId
|
||||
runRetest({ mode, query })
|
||||
}, [linkedRetestTraceId, linkedTraceId, mode, query, selected?.id, selected?.kind])
|
||||
|
||||
const toggleSelectedResearchProcess = () => {
|
||||
if (!selectedResearchTask) return
|
||||
setResearchExpanded((current) => ({
|
||||
@ -2145,7 +2193,8 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
|
||||
badCaseAvailable={Boolean(selectedTraceId)}
|
||||
noResults={currentEvidence.length === 0}
|
||||
decision={qualityDecisions[resultKey]}
|
||||
onDecision={saveQualityDecision}
|
||||
onBadCase={saveBadCase}
|
||||
onGolden={startGoldenPromotion}
|
||||
pending={qualityPendingKey === resultKey}
|
||||
qualityHref={newKnowledgeQualityPath(knowledgeSpaceId)}
|
||||
/>
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "سؤال مطلوب .",
|
||||
"newKnowledge.qualityPage.reason": "السبب",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "فجوة التغطية",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "محتوى قديم",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "ملكة جمال الاسترجاع",
|
||||
"newKnowledge.qualityPage.replay": "إعادة الاختبار",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Die Frage ist erforderlich.",
|
||||
"newKnowledge.qualityPage.reason": "Grund",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Deckungslücke",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Veralteter Inhalt",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Abholfehler",
|
||||
"newKnowledge.qualityPage.replay": "Erneut testen",
|
||||
|
||||
@ -526,6 +526,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Question is required.",
|
||||
"newKnowledge.qualityPage.reason": "Reason",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Coverage gap",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Outdated content",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Retrieval miss",
|
||||
"newKnowledge.qualityPage.replay": "Replay",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Se requiere pregunta.",
|
||||
"newKnowledge.qualityPage.reason": "Razón",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Brecha de cobertura",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Contenido obsoleto",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Error de recuperación",
|
||||
"newKnowledge.qualityPage.replay": "Volver a probar",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "سوال مورد نیاز است.",
|
||||
"newKnowledge.qualityPage.reason": "دلیل",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "شکاف پوشش",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "محتوای قدیمی",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "از دست دادن بازیابی",
|
||||
"newKnowledge.qualityPage.replay": "آزمایش دوباره",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Une question est requise.",
|
||||
"newKnowledge.qualityPage.reason": "Raison",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Lacune de couverture",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Contenu obsolète",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Échec de récupération",
|
||||
"newKnowledge.qualityPage.replay": "Tester à nouveau",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "प्रश्न आवश्यक है।",
|
||||
"newKnowledge.qualityPage.reason": "कारण",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "कवरेज गैप",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "पुरानी सामग्री",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "पुनर्प्राप्ति मिस",
|
||||
"newKnowledge.qualityPage.replay": "फिर से जाँचें",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Pertanyaan wajib diisi.",
|
||||
"newKnowledge.qualityPage.reason": "Alasan",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Kesenjangan cakupan",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Konten ketinggalan jaman",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Pengambilan gagal",
|
||||
"newKnowledge.qualityPage.replay": "Uji kembali",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "La domanda è obbligatoria.",
|
||||
"newKnowledge.qualityPage.reason": "Motivo",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Divario di copertura",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Contenuti obsoleti",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Recupero mancato",
|
||||
"newKnowledge.qualityPage.replay": "Verifica di nuovo",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "質問は必須です。",
|
||||
"newKnowledge.qualityPage.reason": "理由",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "カバレッジギャップ",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "内容が古いです",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "取得ミス",
|
||||
"newKnowledge.qualityPage.replay": "再テスト",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "질문이 필요합니다.",
|
||||
"newKnowledge.qualityPage.reason": "이유",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "보장 공백",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "오래된 콘텐츠",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "검색 미스",
|
||||
"newKnowledge.qualityPage.replay": "다시 테스트",
|
||||
|
||||
@ -367,6 +367,7 @@
|
||||
"newKnowledge.providerNotConfiguredDescription": "ເພີ່ມລະຫັດ API ຫຼື URL ຈຸດສິ້ນສຸດເພື່ອລວບລວມຂໍ້ມູນເວັບໄຊທ໌ດ້ວຍ {{provider}}.",
|
||||
"newKnowledge.providerUnavailable": "ຜູ້ໃຫ້ບໍລິການນີ້ບໍ່ສາມາດໃຊ້ໄດ້.",
|
||||
"newKnowledge.quality": "ຄຸນະພາບ",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "ຄະແນນຕໍ່າ",
|
||||
"newKnowledge.reCrawl": "ກວາດຄືນ",
|
||||
"newKnowledge.readOnlyEmpty": "ຂໍໃຫ້ຜູ້ແກ້ໄຂພື້ນທີ່ເຮັດວຽກສ້າງຖານຄວາມຮູ້ທຳອິດ.",
|
||||
"newKnowledge.refreshConnectionStatus": "ໂຫຼດສະຖານະຄືນໃໝ່",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Vraag is verplicht.",
|
||||
"newKnowledge.qualityPage.reason": "Reden",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Dekkingstekort",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Verouderde inhoud",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Ophaalfout",
|
||||
"newKnowledge.qualityPage.replay": "Opnieuw testen",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Pytanie jest wymagane.",
|
||||
"newKnowledge.qualityPage.reason": "Powód",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Luka w pokryciu",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Nieaktualna treść",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Panna odzyskania",
|
||||
"newKnowledge.qualityPage.replay": "Przetestuj ponownie",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "A pergunta é obrigatória.",
|
||||
"newKnowledge.qualityPage.reason": "Razão",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Lacuna de cobertura",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Conteúdo desatualizado",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Falha de recuperação",
|
||||
"newKnowledge.qualityPage.replay": "Testar novamente",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Este necesară întrebarea.",
|
||||
"newKnowledge.qualityPage.reason": "Motivul",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Decalaj de acoperire",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Conținut învechit",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Rata de recuperare",
|
||||
"newKnowledge.qualityPage.replay": "Testează din nou",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Требуется вопрос.",
|
||||
"newKnowledge.qualityPage.reason": "Причина",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Пробел в покрытии",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Устаревший контент",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Промах при поиске",
|
||||
"newKnowledge.qualityPage.replay": "Проверить повторно",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Vprašanje je obvezno.",
|
||||
"newKnowledge.qualityPage.reason": "Razlog",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Vrzel v kritju",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Zastarela vsebina",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Pogrešana vrnitev",
|
||||
"newKnowledge.qualityPage.replay": "Znova preizkusi",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "ต้องมีคำถาม",
|
||||
"newKnowledge.qualityPage.reason": "เหตุผล",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "ช่องว่างความครอบคลุม",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "เนื้อหาที่ล้าสมัย",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "การเรียกค้นที่พลาด",
|
||||
"newKnowledge.qualityPage.replay": "ทดสอบอีกครั้ง",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Soru zorunludur.",
|
||||
"newKnowledge.qualityPage.reason": "Sebep",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Kapsama açığı",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Güncel olmayan içerik",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Geri alma hatası",
|
||||
"newKnowledge.qualityPage.replay": "Yeniden test et",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Питання обов’язкове.",
|
||||
"newKnowledge.qualityPage.reason": "Причина",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Розрив покриття",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Застарілий вміст",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Промах повернення",
|
||||
"newKnowledge.qualityPage.replay": "Перевірити повторно",
|
||||
|
||||
@ -522,6 +522,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "Câu hỏi là bắt buộc.",
|
||||
"newKnowledge.qualityPage.reason": "Lý do",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "Khoảng cách phủ sóng",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "Low score",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "Nội dung đã lỗi thời",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "Cuộc truy tìm thất bại",
|
||||
"newKnowledge.qualityPage.replay": "Kiểm tra lại",
|
||||
|
||||
@ -526,6 +526,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "必须填写问题。",
|
||||
"newKnowledge.qualityPage.reason": "原因",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "内容覆盖不足",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "分数值低",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "内容已过时",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "检索未命中",
|
||||
"newKnowledge.qualityPage.replay": "重新测试",
|
||||
|
||||
@ -525,6 +525,7 @@
|
||||
"newKnowledge.qualityPage.questionRequired": "必問問題。",
|
||||
"newKnowledge.qualityPage.reason": "原因",
|
||||
"newKnowledge.qualityPage.reasonValues.coverageGap": "覆蓋差距",
|
||||
"newKnowledge.qualityPage.reasonValues.lowScore": "分數值低",
|
||||
"newKnowledge.qualityPage.reasonValues.outdatedContent": "過時的內容",
|
||||
"newKnowledge.qualityPage.reasonValues.retrievalMiss": "檢索失敗",
|
||||
"newKnowledge.qualityPage.replay": "重新測試",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user