fix(knowledge-fs): align new rag edge states

This commit is contained in:
Stephen Zhou 2026-08-03 11:06:21 +08:00
parent 1648d8ac6b
commit d040afebfb
No known key found for this signature in database
58 changed files with 967 additions and 203 deletions

View File

@ -352,7 +352,7 @@ describe('CrawlSelectionForm', () => {
const offDomainPage = screen.getByRole('checkbox', { name: 'Edit this page' })
expect(offDomainPage).toHaveAttribute('aria-disabled', 'true')
expect(offDomainPage).toHaveAccessibleDescription(
'https://github.com/langgenius/dify/edit/main/docs.md dataset.newKnowledge.skippedOffDomain',
'github.com/langgenius/dify/edit/main/docs.md dataset.newKnowledge.skippedOffDomain',
)
expect(screen.getByText('dataset.newKnowledge.skippedOffDomain')).toBeInTheDocument()
expect(screen.getByText(/dataset\.newKnowledge\.skippedFailed/)).toBeInTheDocument()
@ -429,7 +429,7 @@ describe('CrawlSelectionForm', () => {
const overflowPage = screen.getByRole('checkbox', { name: 'Page 201' })
expect(overflowPage).toHaveAttribute('aria-disabled', 'true')
expect(overflowPage).toHaveAccessibleDescription(
'https://docs.dify.ai/page-201 dataset.newKnowledge.maxPages: 200',
'docs.dify.ai/page-201 dataset.newKnowledge.maxPages: 200',
)
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.addSource' }))

View File

@ -562,7 +562,7 @@ describe('CreateKnowledgePage', () => {
expect(connectSource).toBeChecked()
expect(screen.getByRole('radio', { name: 'dataset.newKnowledge.websiteCrawl' })).toBeChecked()
expect(screen.getByRole('radio', { name: 'Firecrawl' })).toBeChecked()
for (const unavailableProvider of ['Jina Reader', 'WaterCrawl']) {
for (const unavailableProvider of ['Jina Reader', 'WaterCrawl', 'FakeCrawler']) {
await user.click(screen.getByRole('radio', { name: unavailableProvider }))
expect(screen.getByRole('radio', { name: unavailableProvider })).toBeChecked()
}
@ -598,6 +598,12 @@ describe('CreateKnowledgePage', () => {
expect(screen.getByText('Notion')).toBeInTheDocument()
expect(screen.getByText('dataset.newKnowledge.notionNotConnected')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'dataset.newKnowledge.connectNotion' })).toBeEnabled()
expect(
screen.queryByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }),
).not.toBeInTheDocument()
expect(
screen.queryByRole('combobox', { name: 'dataset.newKnowledge.syncPolicy' }),
).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.connectNotion' }))
expect(screen.getByRole('alert')).toHaveTextContent(
'dataset.newKnowledge.sourceSetupBackendDependency',
@ -784,6 +790,7 @@ describe('CreateKnowledgePage', () => {
)
await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' }))
await user.click(screen.getByRole('radio', { name: 'Google Docs' }))
expect(screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' })).toBeEnabled()
await user.type(
screen.getByRole('textbox', { name: 'dataset.newKnowledge.sourceName' }),
@ -836,7 +843,7 @@ describe('CreateKnowledgePage', () => {
expect(screen.getByText('oversized.pdf')).toBeInTheDocument()
expect(
screen.getByText('dataset.newKnowledge.documentUploadExclusion.fileSize'),
screen.getByText(/dataset\.newKnowledge\.documentUploadExclusion\.fileSize/),
).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'dataset.newKnowledge.createTitle' })).toBeDisabled()
expect(screen.queryByRole('button', { name: 'dataset.newKnowledge.preview' })).toBeNull()
@ -1002,6 +1009,7 @@ describe('CreateKnowledgePage', () => {
renderPage()
await user.click(screen.getByRole('radio', { name: 'dataset.newKnowledge.onlineDocuments' }))
await user.click(screen.getByRole('radio', { name: 'Google Docs' }))
await user.type(
screen.getByPlaceholderText('dataset.newKnowledge.sourceNamePlaceholder'),
'Release notes',

View File

@ -169,6 +169,7 @@ describe('document detail model', () => {
it('bounds tree labels to a single readable summary', () => {
expect(chunkTreeLabel('First line\nfull body', 3)).toBe('First line')
expect(chunkTreeLabel('## Markdown heading\nfull body', 3)).toBe('Markdown heading')
expect(chunkTreeLabel(' ', 3)).toBe('#3')
expect(chunkTreeLabel('x'.repeat(121), 3)).toBe(`${'x'.repeat(119)}`)
expect(chunkTreeLabel(`${'x'.repeat(118)}👨👩👧👦yz`, 3)).toBe(`${'x'.repeat(118)}👨‍👩‍👧‍👦…`)
@ -183,5 +184,9 @@ describe('document detail model', () => {
body: '',
heading: 'Standalone content',
})
expect(chunkContentParts('### Setup requirements\nWorkspace contract details')).toEqual({
body: 'Workspace contract details',
heading: 'Setup requirements',
})
})
})

View File

@ -1304,7 +1304,12 @@ describe('DocumentDetailPage', () => {
expect(screen.getByRole('alert')).toHaveTextContent(
'dataset.newKnowledge.documentReindexFailed',
)
expect(screen.getByText('dataset.newKnowledge.lastReadyRevisionHint')).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'dataset.newKnowledge.viewTask' }),
).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'dataset.newKnowledge.retryReindexDocument' }),
).toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'Parent content' })).toBeInTheDocument()
})
@ -1373,7 +1378,9 @@ describe('DocumentDetailPage', () => {
}),
)
render(<DocumentDetailPage documentId="document-1" knowledgeSpaceId="space-1" />)
const button = screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocument' })
const button = screen.getByRole('button', {
name: 'dataset.newKnowledge.reindexDocument',
})
await user.dblClick(button)
expect(reindexMutation.mutateAsync).toHaveBeenCalledOnce()
@ -1490,7 +1497,9 @@ describe('DocumentDetailPage', () => {
pages: [{ items: [task({ documentRevision: 4, id: 'old-failed', state: 'failed' })] }],
}
render(<DocumentDetailPage documentId="document-1" knowledgeSpaceId="space-1" />)
const button = screen.getByRole('button', { name: 'dataset.newKnowledge.reindexDocument' })
const button = screen.getByRole('button', {
name: 'dataset.newKnowledge.retryReindexDocument',
})
await user.click(button)
await waitFor(() => expect(toastState.success).toHaveBeenCalled())
@ -1551,7 +1560,7 @@ describe('DocumentDetailPage', () => {
try {
render(<DocumentDetailPage documentId="document-1" knowledgeSpaceId="space-1" />)
const reindexButton = screen.getByRole('button', {
name: 'dataset.newKnowledge.reindexDocument',
name: 'dataset.newKnowledge.retryReindexDocument',
})
await act(async () => {
fireEvent.click(reindexButton)

View File

@ -2,6 +2,7 @@ import type { DocumentProcessingTask, LogicalDocument } from '../document-models
import {
documentDisplayStatus,
newestTaskByDocument,
taskCanCancel,
taskCanRetry,
taskIsActive,
taskNeedsAttention,
@ -108,8 +109,11 @@ describe('new Knowledge document model', () => {
it('keeps task badge and actions aligned with the contract states', () => {
expect(taskIsActive(task({ state: 'running' }))).toBe(true)
expect(taskCanCancel(task({ canCancel: false, state: 'running' }))).toBe(false)
expect(taskCanCancel(task({ canCancel: true, state: 'queued' }))).toBe(true)
expect(taskNeedsAttention(task({ state: 'failed' }))).toBe(true)
expect(taskCanRetry(task({ state: 'canceled' }))).toBe(false)
expect(taskCanRetry(task({ canRetry: true, state: 'canceled' }))).toBe(true)
expect(taskNeedsAttention(task({ state: 'canceled' }))).toBe(false)
expect(taskNeedsAttention(task({ state: 'succeeded' }))).toBe(false)
expect(taskCanRetry(task({ state: 'superseded' }))).toBe(false)

View File

@ -555,7 +555,7 @@ describe('KnowledgeOverviewPage', () => {
).toBeInTheDocument()
expect(
screen.getByRole('combobox', { name: 'dataset.newKnowledge.overview.timeRange' }),
).toBeInTheDocument()
).toHaveTextContent('dataset.newKnowledge.overview.today')
expect(
screen.getByRole('combobox', { name: 'dataset.newKnowledge.overview.operator' }),
).toBeInTheDocument()
@ -846,7 +846,7 @@ describe('KnowledgeOverviewPage', () => {
it('shows first-indexing progress while the initial document task is running', () => {
queryData.stats.source_count = 0
queryData.stats.documents = 1
queryData.stats.documents = 0
queryData.inventory.index_coverage.indexed = 0
queryData.tasks[0]!.operation = 'document_processing'
queryData.tasks[0]!.progress_completed = 1
@ -865,6 +865,31 @@ describe('KnowledgeOverviewPage', () => {
expect(
screen.getByText('dataset.newKnowledge.overview.indexedDocuments:{"indexed":1,"total":2}'),
).toBeInTheDocument()
expect(
screen.getByRole('progressbar', { name: 'dataset.newKnowledge.overview.indexing' }),
).toHaveAttribute('aria-valuenow', '1')
expect(
screen.queryByRole('heading', { name: 'dataset.newKnowledge.overview.noSources' }),
).not.toBeInTheDocument()
})
it('exposes first-indexing progress as indeterminate until the total is known', () => {
queryData.stats.source_count = 0
queryData.stats.documents = 0
queryData.inventory.index_coverage.indexed = 0
queryData.tasks[0]!.operation = 'document_processing'
queryData.tasks[0]!.progress_completed = 0
queryData.tasks[0]!.progress_percent = 0
queryData.tasks[0]!.progress_total = 0
queryData.tasks[0]!.state = 'queued'
renderWithNuqs(<KnowledgeOverviewPage knowledgeSpaceId="space-1" />)
const progressbar = screen.getByRole('progressbar', {
name: 'dataset.newKnowledge.overview.indexing',
})
expect(progressbar).not.toHaveAttribute('aria-valuemax')
expect(progressbar).not.toHaveAttribute('aria-valuenow')
})
it('keeps the generic first-indexing title when the task has no source', () => {

View File

@ -201,11 +201,17 @@ const externalAccess = {
function renderForm({
externalAccess: externalAccessOverride = externalAccess,
members = [],
onDraftFinish,
onDraftStart,
serverConflict,
settings: settingsOverride = settings,
space: spaceOverride = space,
}: {
externalAccess?: typeof externalAccess
members?: Member[]
onDraftFinish?: () => void
onDraftStart?: () => void
serverConflict?: boolean
settings?: KnowledgeFsSettingsResponse
space?: KnowledgeFsSpaceDetailResponse
} = {}) {
@ -223,8 +229,11 @@ function renderForm({
externalAccess={externalAccessOverride}
members={members}
permissions={[]}
serverConflict={serverConflict}
settings={settingsOverride}
space={spaceOverride}
onDraftFinish={onDraftFinish}
onDraftStart={onDraftStart}
/>,
{ wrapper: Wrapper },
)
@ -548,6 +557,52 @@ describe('KnowledgeSettingsForm', () => {
).toBeDisabled()
})
it('blocks a stale draft after the server baseline changes and restores the latest value', async () => {
const user = userEvent.setup()
const onDraftFinish = vi.fn()
const onDraftStart = vi.fn()
const renderWithName = (name: string, serverConflict = false) => (
<KnowledgeSettingsForm
externalAccess={externalAccess}
members={[]}
permissions={[]}
serverConflict={serverConflict}
settings={settings}
space={{
...space,
technical_summary: {
...space.technical_summary,
name,
},
}}
onDraftFinish={onDraftFinish}
onDraftStart={onDraftStart}
/>
)
const view = renderForm({ onDraftFinish, onDraftStart })
const nameInput = screen.getByRole('textbox', { name: 'datasetSettings.form.name' })
await user.clear(nameInput)
await user.type(nameInput, 'Version B')
expect(onDraftStart).toHaveBeenCalled()
view.rerender(renderWithName('Version C', true))
expect(nameInput).toHaveValue('Version B')
expect(screen.getByRole('alert')).toHaveTextContent(
'dataset.newKnowledge.settings.serverConflict',
)
expect(
screen.getByRole('button', {
name: 'dataset.newKnowledge.settings.saveChanges',
}),
).toBeDisabled()
await user.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
expect(nameInput).toHaveValue('Version C')
expect(onDraftFinish).toHaveBeenCalledOnce()
expect(serviceMock.patchSpace).not.toHaveBeenCalled()
})
it('allows embedding and retrieval to be configured together during initial setup', async () => {
const user = userEvent.setup()
renderForm({

View File

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { screen, waitFor } from '@testing-library/react'
import { act, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render } from '@/test/console/render'
import { KnowledgeSettingsPage } from '../knowledge-settings-page'
@ -30,7 +30,26 @@ vi.mock('@/service/use-common', () => ({
}))
vi.mock('../knowledge-settings-form', () => ({
KnowledgeSettingsForm: () => <div>settings-form</div>,
KnowledgeSettingsForm: ({
onDraftFinish,
onDraftStart,
serverConflict,
}: {
onDraftFinish: () => void
onDraftStart: () => void
serverConflict: boolean
}) => (
<div>
settings-form
<span>{serverConflict ? 'server-conflict' : 'no-conflict'}</span>
<button type="button" onClick={onDraftStart}>
start-draft
</button>
<button type="button" onClick={onDraftFinish}>
finish-draft
</button>
</div>
),
}))
const queryData = vi.hoisted(() => ({
@ -96,7 +115,10 @@ function renderPage() {
const Wrapper = ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
return render(<KnowledgeSettingsPage knowledgeSpaceId="space-1" />, { wrapper: Wrapper })
return {
queryClient,
...render(<KnowledgeSettingsPage knowledgeSpaceId="space-1" />, { wrapper: Wrapper }),
}
}
describe('KnowledgeSettingsPage', () => {
@ -113,7 +135,15 @@ describe('KnowledgeSettingsPage', () => {
await waitFor(() => {
expect(screen.queryByText('settings-form')).not.toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
expect(screen.getByRole('status')).toHaveTextContent('common.loading')
expect(screen.getByRole('status')).not.toHaveTextContent(
'dataset.newKnowledge.settings.basicInfo',
)
expect(screen.getByRole('status')).not.toHaveTextContent(
'dataset.newKnowledge.settings.dangerZone',
)
expect(screen.getByText('dataset.newKnowledge.settings.basicInfo')).toBeInTheDocument()
expect(screen.getByText('dataset.newKnowledge.settings.dangerZone')).toBeInTheDocument()
})
})
@ -156,4 +186,24 @@ describe('KnowledgeSettingsPage', () => {
}),
).toBe(false)
})
it('keeps an active draft mounted and reports a conflict when the server version changes', async () => {
const user = userEvent.setup()
membersQueryMock.data = { accounts: [] }
membersQueryMock.isPending = false
const { queryClient } = renderPage()
expect(await screen.findByText('no-conflict')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'start-draft' }))
act(() => {
queryClient.setQueryData(['knowledge-fs', 'space'], {
...queryData.space,
resource_version: 2,
})
})
expect(await screen.findByText('server-conflict')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'finish-draft' }))
expect(await screen.findByText('no-conflict')).toBeInTheDocument()
})
})

View File

@ -349,6 +349,52 @@ describe('QualityPage', () => {
expect(serviceMock.createGolden).not.toHaveBeenCalled()
})
it('reveals the full annotation and submits edits through the update contract', async () => {
const user = userEvent.setup()
serviceMock.updateGolden.mockResolvedValue({})
renderPage()
await screen.findByText('What is the refund policy?')
const annotation = screen.getByRole('button', { name: 'Must cite the refund clause.' })
await user.hover(annotation)
await waitFor(() => expect(screen.getAllByText('Must cite the refund clause.')).toHaveLength(2))
await user.click(
screen.getByRole('button', {
name: /dataset\.newKnowledge\.qualityPage\.questionActions/,
}),
)
await user.click(
await screen.findByRole('menuitem', { name: 'dataset.newKnowledge.qualityPage.edit' }),
)
const annotationInput = screen.getByPlaceholderText(
'dataset.newKnowledge.qualityPage.annotationPlaceholder',
)
await user.clear(annotationInput)
await user.type(annotationInput, 'Updated expected answer')
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.qualityPage.save' }))
await waitFor(() =>
expect(serviceMock.updateGolden).toHaveBeenCalledWith(
{
body: {
annotation: 'Updated expected answer',
evidence_text: '',
expected_evidence_ids: [],
match_policy: 'all',
question: 'What is the refund policy?',
tags: ['billing'],
},
params: { control_space_id: 'space-1', question_id: 'golden-1' },
},
expect.anything(),
),
)
expect(
screen.queryByRole('dialog', { name: 'dataset.newKnowledge.qualityPage.editTitle' }),
).not.toBeInTheDocument()
})
it('resolves the protected trace reference before navigating', async () => {
const user = userEvent.setup()
navigationMock.tab = 'bad-cases'

View File

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { screen, waitFor } from '@testing-library/react'
import { act, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { render } from '@/test/console/render'
import { RetrievalTestPage } from '../retrieval-test-page'
@ -294,7 +294,21 @@ describe('RetrievalTestPage', () => {
onEvent({
createdAt: new Date(1_800_000_000_000 + seconds * 1000).toISOString(),
id: `event-${index + 1}`,
payload: {},
payload:
stage === 'retrieving'
? {
results: [
{
chunkCount: 3,
createdAt: '2027-01-15T08:00:02.000Z',
question: 'Refund policy',
sourceId: 'internal-source-id',
},
],
sourceCount: 2,
unknownTotal: 9,
}
: {},
researchTaskJobId: 'research-completed',
sequence: index + 1,
stage,
@ -308,11 +322,11 @@ describe('RetrievalTestPage', () => {
renderPage()
await user.click(screen.getByText('Compare the refund policies'))
expect(
screen.getByRole('button', {
name: 'dataset.newKnowledge.retrievalTest.processLog',
}),
).toHaveAttribute('aria-pressed', 'true')
const processLog = screen.getByRole('button', {
name: 'dataset.newKnowledge.retrievalTest.processLog',
})
expect(processLog).toHaveAttribute('aria-pressed', 'false')
await user.click(processLog)
await waitFor(() =>
expect(apiMock.streamCapability).toHaveBeenCalledWith({
@ -329,10 +343,48 @@ describe('RetrievalTestPage', () => {
expect(screen.getByText('5s')).toBeInTheDocument()
expect(screen.getByText('7s')).toBeInTheDocument()
expect(screen.getByText('11s')).toBeInTheDocument()
expect(screen.getByText('dataset.newKnowledge.sources: 2')).toBeInTheDocument()
expect(
screen.getByText('Refund policy · 3 dataset.newKnowledge.chunkCount'),
).toBeInTheDocument()
expect(screen.queryByText('internal-source-id')).not.toBeInTheDocument()
expect(screen.queryByText('2027-01-15T08:00:02.000Z')).not.toBeInTheDocument()
expect(screen.queryByText(/unknown.*9/i)).not.toBeInTheDocument()
expect(apiMock.refetchTasks).not.toHaveBeenCalled()
expect(apiMock.refetchPartials).not.toHaveBeenCalled()
})
it('replaces the just-now label after the first minute', () => {
vi.useFakeTimers()
try {
const now = Date.parse('2026-07-29T09:04:30Z')
vi.setSystemTime(now)
apiMock.traces = [
{
completed: true,
created_at: new Date(now - 30_000).toISOString(),
id: 'trace-recent',
mode: 'fast',
profile: {},
query: 'A recent retrieval run',
scores: {},
stages: [],
},
]
renderPage()
expect(screen.getByText('dataset.newKnowledge.retrievalTest.justNow')).toBeInTheDocument()
act(() => vi.advanceTimersByTime(30_001))
expect(
screen.queryByText('dataset.newKnowledge.retrievalTest.justNow'),
).not.toBeInTheDocument()
expect(screen.getByText('A recent retrieval run')).toBeInTheDocument()
} finally {
vi.useRealTimers()
}
})
it('renders generated Research answer deltas while the task is still active', async () => {
apiMock.researchTasks = [
{
@ -729,6 +781,39 @@ describe('RetrievalTestPage', () => {
expect(screen.getByText('provider timed out')).toBeInTheDocument()
})
it('cancels a stalled fast run and restores the composer', async () => {
let streamSignal: AbortSignal | undefined
apiMock.streamQuery.mockImplementationOnce(
({ signal }: { signal?: AbortSignal }) =>
new Promise<void>((_resolve, reject) => {
streamSignal = signal
signal?.addEventListener('abort', () => {
reject(new DOMException('Aborted', 'AbortError'))
})
}),
)
const user = userEvent.setup()
renderPage()
const queryInput = screen.getByLabelText('dataset.newKnowledge.retrievalTest.queryPlaceholder')
await user.type(queryInput, 'A query that never completes')
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.run' }))
expect(queryInput).toBeDisabled()
await user.click(
await screen.findByRole('button', {
name: 'dataset.newKnowledge.retrievalTest.cancel',
}),
)
expect(streamSignal?.aborted).toBe(true)
expect(queryInput).toBeEnabled()
expect(
screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.run' }),
).toBeEnabled()
expect(screen.getByText('dataset.newKnowledge.retrievalTest.emptyTitle')).toBeInTheDocument()
})
it('maps an empty unpublished knowledge space to the designed no-results state', async () => {
apiMock.streamQuery.mockRejectedValueOnce(
new Response('Published runtime snapshot unavailable', { status: 503 }),
@ -749,5 +834,40 @@ describe('RetrievalTestPage', () => {
screen.queryByText('dataset.newKnowledge.retrievalTest.failedTitle'),
).not.toBeInTheDocument()
expect(screen.getAllByText('Anything here?')).toHaveLength(2)
expect(
screen.queryByRole('button', {
name: 'dataset.newKnowledge.retrievalTest.makeBadCase',
}),
).not.toBeInTheDocument()
})
it('does not guess a trace id for a trace-less local run', async () => {
apiMock.traces = [
{
completed: true,
created_at: new Date().toISOString(),
id: 'different-trace',
mode: 'fast',
profile: {},
query: 'Repeated question',
scores: {},
stages: [],
},
]
const user = userEvent.setup()
renderPage()
await user.type(
screen.getByLabelText('dataset.newKnowledge.retrievalTest.queryPlaceholder'),
'Repeated question',
)
await user.click(screen.getByRole('button', { name: 'dataset.newKnowledge.retrievalTest.run' }))
expect(
screen.queryByRole('button', {
name: 'dataset.newKnowledge.retrievalTest.makeBadCase',
}),
).not.toBeInTheDocument()
expect(apiMock.createBadCase).not.toHaveBeenCalled()
})
})

View File

@ -488,7 +488,8 @@ describe('SourcesPage', () => {
expect(openSource).toHaveAttribute('rel', 'noopener noreferrer')
})
it('hides the row action menu when a read-only source has no openable URI', () => {
it('opens an Amazon S3 source in the AWS console for read-only users', async () => {
const user = userEvent.setup()
permissionState.workspacePermissionKeys = ['dataset.acl.readonly']
sourcesQuery.data = {
pages: [
@ -496,7 +497,7 @@ describe('SourcesPage', () => {
items: [
source({
type: 'object-storage',
uri: 's3://private-bucket/product-documentation',
uri: 's3://private-bucket/Product%20documentation/文档',
}),
],
},
@ -505,11 +506,17 @@ describe('SourcesPage', () => {
render(<SourcesPage knowledgeSpaceId="space-1" />)
expect(
screen.queryByRole('button', {
await user.click(
screen.getByRole('button', {
name: 'dataset.newKnowledge.sourceActions:{"name":"Product documentation"}',
}),
).not.toBeInTheDocument()
)
expect(
screen.getByRole('menuitem', { name: 'dataset.newKnowledge.editSource' }),
).toHaveAttribute(
'href',
'https://s3.console.aws.amazon.com/s3/buckets/private-bucket?prefix=Product+documentation%2F%E6%96%87%E6%A1%A3',
)
})
it('uses dataset.external.connect for every source mutation action', async () => {

View File

@ -800,7 +800,7 @@ describe('WebsiteCrawlPreview', () => {
expect(status).toHaveAttribute('role', 'status')
expect(status.querySelector('button')).not.toBeInTheDocument()
const skeletons = screen.getAllByTestId('crawl-page-skeleton')
expect(skeletons).toHaveLength(2)
expect(skeletons).toHaveLength(4)
expect(skeletons[0]?.closest('ul')).toBe(screen.getByRole('list'))
await act(async () => vi.advanceTimersByTime(1500))
expect(await screen.findByText('Three')).toBeInTheDocument()

View File

@ -1159,7 +1159,7 @@ export function AddSourcePage({
return (
<>
<main className="min-h-full px-4 py-6 sm:px-8 sm:py-8">
<main className="min-h-full w-full min-w-0 flex-1 px-4 py-6 sm:px-8 sm:py-8">
<header>
<h2 className="system-xl-semibold text-text-primary">
{t(($) => $['newKnowledge.addSource'])}
@ -1168,7 +1168,7 @@ export function AddSourcePage({
{t(($) => $['newKnowledge.addSourceDescription'])}
</p>
</header>
<div className="mt-4.5 flex w-full max-w-152 flex-col gap-4">
<div className="mt-4.5 flex w-full max-w-160 flex-col gap-4">
<SourceTypeSelector
value={sourceType}
onChange={(value) => {

View File

@ -13,7 +13,6 @@ import {
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectLabel,
SelectTrigger,
} from '@langgenius/dify-ui/select'
import { useState } from 'react'
@ -52,7 +51,6 @@ export function KnowledgeSettingsMembers({
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const owner = members.find((member) => member.id === ownerAccountId)
const selectedMembers = members.filter(
(member) => member.id !== ownerAccountId && selectedMemberIds.includes(member.id),
)
@ -86,8 +84,11 @@ export function KnowledgeSettingsMembers({
if (value) onVisibilityChange(value as KnowledgeFsControlSpaceVisibility)
}}
>
<SelectLabel>{tSettings(($) => $['form.permissions'])}</SelectLabel>
<SelectTrigger className="h-9 w-full" disabled={disabled}>
<SelectTrigger
aria-label={tSettings(($) => $['form.permissions'])}
className="h-9 w-full"
disabled={disabled}
>
{visibilityLabel(visibility)}
</SelectTrigger>
<SelectContent>
@ -107,12 +108,6 @@ export function KnowledgeSettingsMembers({
hasError && 'ring-1 ring-text-destructive',
)}
>
{owner && (
<span className="inline-flex h-6 items-center gap-1 rounded-md bg-components-badge-bg-dimm px-1.5 system-xs-medium text-text-secondary">
<Avatar avatar={owner.avatar_url} name={owner.name} size="xxs" />
<span className="max-w-28 truncate">{owner.name}</span>
</span>
)}
{selectedMembers.map((member) => (
<span
key={member.id}
@ -178,7 +173,7 @@ export function KnowledgeSettingsMembers({
<div className="max-h-72 overflow-y-auto p-1">
{filteredMembers.map((member) => {
const isOwner = member.id === ownerAccountId
const isSelected = isOwner || selectedMemberIds.includes(member.id)
const isSelected = selectedMemberIds.includes(member.id)
return (
<button
key={member.id}

View File

@ -1983,12 +1983,14 @@ export function ConnectedSourceSetup({
onDraftChange({
...draft,
provider: nextProvider as NewKnowledgeOnlineDocumentsProvider,
sourceName: '',
})
return
}
onDraftChange({
...draft,
provider: nextProvider as NewKnowledgeOnlineDriveProvider,
sourceName: '',
syncPolicy:
nextProvider === 'Amazon S3' && draft.syncPolicy === 'provider'
? 'daily'

View File

@ -485,10 +485,11 @@ function ReadyCrawlSelectionForm({
)}
<Button
type="button"
variant="tertiary"
variant="ghost-accent"
size="small"
disabled={submissionLocked}
loading={busy}
className="px-0"
onClick={onRecrawl}
>
{t(($) => $['newKnowledge.reCrawl'])}
@ -534,7 +535,7 @@ function ReadyCrawlSelectionForm({
id={urlId}
className="block truncate system-2xs-regular text-text-tertiary"
>
{page.sourceUrl}
{page.sourceUrl.replace(/^https?:\/\//, '')}
</span>
</span>
{(!selectable || selectionLimitReached) && (

View File

@ -400,7 +400,10 @@ export function CreateKnowledgePage() {
<FieldError match="customError" />
</Field>
<Field name="description" className="gap-1.5">
<FieldLabel>{t(($) => $['newKnowledge.description'])}</FieldLabel>
<FieldLabel>
{t(($) => $['newKnowledge.description'])}{' '}
{tCommon(($) => $['label.optional'])}
</FieldLabel>
<Textarea
autoComplete="off"
className="min-h-20"

View File

@ -62,6 +62,7 @@ const providers = {
{ icon: 'i-ri-fire-fill text-orange-500', label: 'Firecrawl', available: true },
{ icon: 'i-custom-public-llm-jina', label: 'Jina Reader' },
{ icon: 'i-ri-water-flash-line', label: 'WaterCrawl' },
{ icon: 'i-ri-bug-line', label: 'FakeCrawler' },
],
} as const
@ -148,7 +149,6 @@ export function CreateSourceSetup({
onSourceTypeChange: (sourceType: NewKnowledgeSourceDraft['sourceType']) => void
}) {
const { t } = useTranslation('dataset')
const { t: tCreation } = useTranslation('datasetCreation')
const [optionsExpanded, setOptionsExpanded] = useState(false)
const [backendBoundaryVisible, setBackendBoundaryVisible] = useState(false)
const sourceType = draft.sourceType
@ -208,11 +208,11 @@ export function CreateSourceSetup({
<Fieldset disabled={disabled}>
<FieldsetLegend className="sr-only">
{tCreation(($) => $['stepOne.website.chooseProvider'])}
{t(($) => $['newKnowledge.providerLabel'])}
</FieldsetLegend>
<div className="mb-1.5 flex items-center justify-between gap-3">
<span className="system-xs-medium text-text-secondary">
{tCreation(($) => $['stepOne.website.chooseProvider'])}
{t(($) => $['newKnowledge.providerLabel'])}
</span>
<Button
variant="ghost-accent"
@ -227,7 +227,10 @@ export function CreateSourceSetup({
<RadioGroup<string>
value={activeProvider}
disabled={disabled}
className="grid grid-cols-2 gap-2 sm:grid-cols-3"
className={cn(
'grid grid-cols-2 gap-2',
sourceType === 'websiteCrawl' ? 'sm:grid-cols-4' : 'sm:grid-cols-3',
)}
onValueChange={selectProvider}
>
{providers[sourceType].map((provider) => {
@ -254,7 +257,12 @@ export function CreateSourceSetup({
<div className="space-y-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field name="rootUrl" className="gap-1.5">
<FieldLabel>{t(($) => $['newKnowledge.rootUrl'])}</FieldLabel>
<FieldLabel>
{t(($) => $['newKnowledge.rootUrl'])}
<span aria-hidden className="ml-0.5 text-text-destructive">
*
</span>
</FieldLabel>
<FieldControl
type="url"
inputMode="url"
@ -273,7 +281,12 @@ export function CreateSourceSetup({
/>
</Field>
<Field name="sourceName" className="gap-1.5">
<FieldLabel>{t(($) => $['newKnowledge.sourceName'])}</FieldLabel>
<FieldLabel>
{t(($) => $['newKnowledge.sourceName'])}
<span aria-hidden className="ml-0.5 text-text-destructive">
*
</span>
</FieldLabel>
<FieldControl
type="text"
autoComplete="off"
@ -393,11 +406,11 @@ export function CreateSourceSetup({
<div className="space-y-3">
{draft.sourceType === 'onlineDocuments' && activeProvider === 'Notion' && (
<section className="rounded-lg border border-divider-subtle bg-background-default p-4">
<div className="flex items-start gap-3">
<div className="flex flex-col items-start gap-3">
<span
aria-hidden
className={cn(
'mt-0.5 size-5 shrink-0',
'flex size-10 shrink-0 items-center justify-center rounded-lg bg-background-section text-xl',
availableProviders.find((provider) => provider.label === activeProvider)?.icon,
)}
/>
@ -409,22 +422,24 @@ export function CreateSourceSetup({
{t(($) => $['newKnowledge.notionNotConnectedDescription'])}
</p>
</div>
<Button
variant="primary"
disabled={disabled}
className="shrink-0"
onClick={showBackendBoundary}
>
{t(($) => $['newKnowledge.connectNotion'])}
</Button>
</div>
<Button
variant="primary"
disabled={disabled}
className="mt-4"
onClick={showBackendBoundary}
>
{t(($) => $['newKnowledge.connectNotion'])}
</Button>
</section>
)}
<ConnectedSourceConfiguration
disabled={disabled}
draft={draft}
onDraftChange={updateDraft}
/>
{!(draft.sourceType === 'onlineDocuments' && activeProvider === 'Notion') && (
<ConnectedSourceConfiguration
disabled={disabled}
draft={draft}
onDraftChange={updateDraft}
/>
)}
</div>
)}

View File

@ -144,7 +144,8 @@ export function CreateUploadQueue({
key={upload.id}
className={cn(
'flex min-w-0 items-center gap-2 rounded-lg border border-divider-subtle bg-background-default px-3 py-2',
upload.issue && 'border-components-option-card-option-border',
upload.issue &&
'border-components-input-border-destructive bg-state-destructive-hover',
)}
>
<span
@ -173,7 +174,7 @@ export function CreateUploadQueue({
{uploading && !upload.issue
? t(($) => $['newKnowledge.uploadingFiles'])
: upload.issue === 'fileSize'
? t(($) => $['newKnowledge.documentUploadExclusion.fileSize'])
? `${formatFileSize(upload.file.size)} · ${t(($) => $['newKnowledge.documentUploadExclusion.fileSize'])}`
: upload.issue === 'fileType'
? t(($) => $['newKnowledge.documentUploadExclusion.fileType'])
: `${formatFileSize(upload.file.size)} · ${t(($) => $['newKnowledge.uploadCharactersUnavailable'])}`}
@ -199,7 +200,7 @@ export function CreateUploadQueue({
className="size-7 shrink-0 px-0"
onClick={() => onChange(uploads.filter((candidate) => candidate.id !== upload.id))}
>
<span aria-hidden className="i-ri-close-line size-4" />
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</Button>
</li>
))}

View File

@ -29,6 +29,7 @@ export function DocumentDetailHeader({
onRevisionChange,
reindexDisabled,
reindexDisabledReasonId,
reindexFailed,
reindexInProgress,
reindexing,
revisions,
@ -48,6 +49,7 @@ export function DocumentDetailHeader({
onRevisionChange: (revision: number) => void
reindexDisabled: boolean
reindexDisabledReasonId?: string
reindexFailed: boolean
reindexInProgress: boolean
reindexing: boolean
revisions: Array<Exclude<LogicalDocumentRevision, null>>
@ -141,7 +143,9 @@ export function DocumentDetailHeader({
{t(($) =>
reindexInProgress
? $['newKnowledge.cancelDocumentReindex']
: $['newKnowledge.reindexDocument'],
: reindexFailed
? $['newKnowledge.retryReindexDocument']
: $['newKnowledge.reindexDocument'],
)}
</Button>
</div>

View File

@ -132,7 +132,7 @@ export function initialDocumentRevision(
}
export function chunkTreeLabel(text: string, ordinal: number) {
const firstLine = text.split(/\r?\n/, 1)[0]?.trim()
const firstLine = cleanMarkdownHeading(text.split(/\r?\n/, 1)[0] ?? '')
if (!firstLine) return `#${ordinal}`
const graphemes: string[] = []
for (const { segment } of new Intl.Segmenter(undefined, {
@ -146,13 +146,17 @@ export function chunkTreeLabel(text: string, ordinal: number) {
export function chunkContentParts(text: string) {
const lineBreak = text.search(/\r?\n/)
if (lineBreak < 0) return { body: '', heading: text }
if (lineBreak < 0) return { body: '', heading: cleanMarkdownHeading(text) }
return {
body: text.slice(lineBreak).trimStart(),
heading: text.slice(0, lineBreak).trim(),
heading: cleanMarkdownHeading(text.slice(0, lineBreak)),
}
}
function cleanMarkdownHeading(text: string) {
return text.replace(/^\s{0,3}#{1,6}(?:\s+|$)/, '').trim()
}
export function chunkCharacterCount(text: string) {
let count = 0
for (const _segment of new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(text))

View File

@ -273,6 +273,7 @@ export function DocumentDetailPage({
Boolean(tasksError)
}
reindexDisabledReasonId={!hasEditPermission ? REINDEX_RESTRICTION_ID : undefined}
reindexFailed={latestTask?.state === 'failed'}
reindexInProgress={reindexInProgress}
reindexing={reindexBusy || submissionPending}
revisions={availableRevisions}

View File

@ -69,13 +69,16 @@ export function DocumentDetailStatus({
)}
{latestTask?.state === 'failed' && (
<div
className="mt-4 rounded-lg bg-state-destructive-hover px-3 py-2 system-xs-regular text-text-destructive"
className="mt-4 flex items-center gap-2 rounded-lg bg-state-destructive-hover px-3 py-2 system-xs-regular text-text-destructive"
role="alert"
>
<p>{t(($) => $['newKnowledge.documentReindexFailed'])}</p>
<p className="mt-1 text-text-secondary">
{t(($) => $['newKnowledge.lastReadyRevisionHint'])}
</p>
<span aria-hidden className="i-ri-error-warning-fill size-4 shrink-0" />
<span className="min-w-0 flex-1">
{t(($) => $['newKnowledge.documentReindexFailed'])}
</span>
<Button size="small" variant="ghost" onClick={onViewTasks}>
{t(($) => $['newKnowledge.viewTask'])}
</Button>
</div>
)}

View File

@ -96,6 +96,10 @@ export function taskIsActive(task: BackgroundTask) {
return ACTIVE_TASK_STATES.has(task.state)
}
export function taskCanRetry(task: BackgroundTask) {
return task.state === 'failed'
export function taskCanCancel(task: BackgroundTask) {
return taskIsActive(task) && task.canCancel !== false
}
export function taskCanRetry(task: BackgroundTask) {
return task.canRetry ?? task.state === 'failed'
}

View File

@ -204,6 +204,18 @@ function normalizedTaskSnapshot(task: DocumentProcessingTask): DocumentProcessin
}
}
function mergeTaskOverride(
task: DocumentProcessingTask,
override: Partial<DocumentProcessingTask>,
): DocumentProcessingTask {
const stateChanged = override.state !== undefined && override.state !== task.state
return {
...task,
...(stateChanged ? { canCancel: undefined, canRetry: undefined } : {}),
...override,
}
}
export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
@ -523,10 +535,10 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
taskIsActive(task) &&
!taskVersionIsAfter(task.updatedAt, terminalTaskPin.observedAt)
)
return { ...task, ...override }
if (!override?.updatedAt) return override ? { ...task, ...override } : task
return mergeTaskOverride(task, override)
if (!override?.updatedAt) return override ? mergeTaskOverride(task, override) : task
if (taskVersionIsAfter(task.updatedAt, override.updatedAt)) return task
const mergedTask = { ...task, ...override }
const mergedTask = mergeTaskOverride(task, override)
if (
!taskIsActive(task) &&
taskIsActive(mergedTask) &&
@ -1258,7 +1270,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
const trustedOverride = trustedActiveOverrideVersionsRef.current.get(task.id)
if (
!override?.updatedAt ||
!taskIsActive({ ...task, ...override }) ||
!taskIsActive(mergeTaskOverride(task, override)) ||
taskVersionIsAfter(override.updatedAt, task.updatedAt) ||
trustedOverride?.updatedAt !== override.updatedAt ||
taskListGeneration <= trustedOverride.taskListGeneration ||

View File

@ -59,8 +59,11 @@ type KnowledgeSettingsFormProps = {
externalAccess: KnowledgeFsExternalAccessResponse
members: Member[]
permissions: KnowledgeFsPermissionResponse[]
serverConflict?: boolean
settings: KnowledgeFsSettingsResponse
space: KnowledgeFsSpaceDetailResponse
onDraftFinish?: () => void
onDraftStart?: () => void
}
function pluginIdForModel(model: DefaultModel) {
@ -190,8 +193,11 @@ export function KnowledgeSettingsForm({
externalAccess,
members,
permissions,
serverConflict = false,
settings,
space,
onDraftFinish,
onDraftStart,
}: KnowledgeSettingsFormProps) {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
@ -347,7 +353,9 @@ export function KnowledgeSettingsForm({
const fieldsDisabled = !canEdit || isSaving
const retrievalFieldsDisabled = fieldsDisabled || (!initialModelSetup && embeddingDirty)
const scoreThresholdAvailable = retrievalMode === 'research' || rerankEnabled
const saveDisabled = !isDirty || nameInvalid || membersInvalid || settingsInvalid || isSaving
const saveDisabled =
!isDirty || nameInvalid || membersInvalid || settingsInvalid || isSaving || serverConflict
const startDraft = () => onDraftStart?.()
const resetDraft = () => {
setName(initialName)
@ -378,6 +386,7 @@ export function KnowledgeSettingsForm({
)
setNameTouched(false)
setSaveError(false)
onDraftFinish?.()
}
const invalidateSettingsQueries = useCallback(async () => {
@ -411,6 +420,8 @@ export function KnowledgeSettingsForm({
// oxlint-disable-next-line eslint-react/set-state-in-effect -- A terminal remote migration retires the local polling guard.
setPendingMigrationId(undefined)
void invalidateSettingsQueries().then(() => {
completedSaveFingerprintsRef.current = {}
onDraftFinish?.()
toast.success(tCommon(($) => $['api.saved']))
})
return
@ -435,6 +446,7 @@ export function KnowledgeSettingsForm({
invalidateSettingsQueries,
migrationQuery.data,
migrationQuery.isError,
onDraftFinish,
pendingMigrationId,
retrievalDirty,
tCommon,
@ -543,6 +555,8 @@ export function KnowledgeSettingsForm({
}
if (pendingMigrationId) return
await invalidateSettingsQueries()
completedSaveFingerprintsRef.current = {}
onDraftFinish?.()
toast.success(tCommon(($) => $['api.saved']))
} catch {
setSaveError(true)
@ -681,6 +695,18 @@ export function KnowledgeSettingsForm({
</div>
)}
{serverConflict && (
<div
className="mb-3 flex items-center gap-2 rounded-lg border border-text-warning/20 bg-state-warning-hover px-3 py-2"
role="alert"
>
<span aria-hidden className="i-ri-error-warning-line size-4 text-text-warning" />
<span className="min-w-0 flex-1 system-xs-regular text-text-warning">
{t(($) => $['newKnowledge.settings.serverConflict'])}
</span>
</div>
)}
{pendingMigrationId && (
<div
className="mb-3 flex items-center gap-2 rounded-lg border border-components-panel-border bg-background-section px-3 py-2 system-xs-regular text-text-tertiary"
@ -719,9 +745,10 @@ export function KnowledgeSettingsForm({
disabled={fieldsDisabled}
className={cn(nameTouched && nameInvalid && 'ring-1 ring-text-destructive')}
onBlur={() => setNameTouched(true)}
onChange={(event) =>
onChange={(event) => {
startDraft()
setName(event.target.value.slice(0, KNOWLEDGE_NAME_MAX_LENGTH))
}
}}
/>
{nameTouched && nameInvalid && (
<p
@ -755,7 +782,10 @@ export function KnowledgeSettingsForm({
disabled={fieldsDisabled}
placeholder={t(($) => $['newKnowledge.settings.descriptionPlaceholder'])}
className="min-h-20 resize-none"
onValueChange={setDescription}
onValueChange={(value) => {
startDraft()
setDescription(value)
}}
/>
</SettingsRow>
@ -767,14 +797,24 @@ export function KnowledgeSettingsForm({
ownerAccountId={space.owner_account_id}
selectedMemberIds={selectedMemberIds}
visibility={visibility}
onSelectedMemberIdsChange={setSelectedMemberIds}
onVisibilityChange={setVisibility}
onSelectedMemberIdsChange={(memberIds) => {
startDraft()
setSelectedMemberIds(memberIds)
}}
onVisibilityChange={(nextVisibility) => {
startDraft()
setVisibility(nextVisibility)
}}
/>
</SettingsRow>
{canEdit && (
<div className="flex justify-end gap-2 pt-1">
<Button type="button" disabled={!isDirty || isSaving} onClick={resetDraft}>
<Button
type="button"
disabled={(!isDirty && !serverConflict) || isSaving}
onClick={resetDraft}
>
{tCommon(($) => $['operation.cancel'])}
</Button>
<Button type="submit" variant="primary" disabled={saveDisabled} loading={isSaving}>
@ -794,7 +834,10 @@ export function KnowledgeSettingsForm({
aria-describedby={API_ACCESS_DESCRIPTION_ID}
checked={apiEnabled}
disabled={!canEdit || !canManageAccess || isSaving || !modelSetupReady}
onCheckedChange={setApiEnabled}
onCheckedChange={(checked) => {
startDraft()
setApiEnabled(checked)
}}
/>
<p
id={API_ACCESS_DESCRIPTION_ID}
@ -831,6 +874,7 @@ export function KnowledgeSettingsForm({
readonly={retrievalFieldsDisabled}
triggerClassName="w-full"
onSelect={(model) => {
startDraft()
setReasoningModel(model)
}}
/>
@ -849,7 +893,10 @@ export function KnowledgeSettingsForm({
modelList={embeddingModelList}
readonly={fieldsDisabled || (!initialModelSetup && retrievalDirty)}
triggerClassName="w-full"
onSelect={setEmbeddingModel}
onSelect={(model) => {
startDraft()
setEmbeddingModel(model)
}}
/>
{embeddingDirty && (space.technical_summary?.document_count ?? 0) > 0 && (
<p className="mt-1 flex items-start gap-1 system-xs-regular text-text-warning-secondary">
@ -866,6 +913,7 @@ export function KnowledgeSettingsForm({
checked={rerankEnabled}
disabled={retrievalFieldsDisabled}
onCheckedChange={(checked) => {
startDraft()
setRerankEnabled(checked)
if (!checked && retrievalMode !== 'research') setScoreThresholdEnabled(false)
}}
@ -881,6 +929,7 @@ export function KnowledgeSettingsForm({
readonly={retrievalFieldsDisabled || !rerankEnabled}
triggerClassName="w-full"
onSelect={(model) => {
startDraft()
setRerankModel(model)
}}
/>
@ -904,6 +953,7 @@ export function KnowledgeSettingsForm({
onValueChange={(values) => {
const mode = values[0]
if (mode === 'fast' || mode === 'deep' || mode === 'research') {
startDraft()
setRetrievalMode(mode)
if (mode !== 'research' && !rerankEnabled) setScoreThresholdEnabled(false)
}
@ -937,6 +987,7 @@ export function KnowledgeSettingsForm({
disabled={retrievalFieldsDisabled}
className="w-18 shrink-0"
onChange={(event) => {
startDraft()
setTopK(clamp(Number(event.target.value), TOP_K_MIN, TOP_K_MAX))
}}
/>
@ -947,6 +998,7 @@ export function KnowledgeSettingsForm({
value={topK}
disabled={retrievalFieldsDisabled}
onValueChange={(value) => {
startDraft()
setTopK(value)
}}
/>
@ -963,6 +1015,7 @@ export function KnowledgeSettingsForm({
checked={scoreThresholdEnabled}
disabled={retrievalFieldsDisabled || !scoreThresholdAvailable}
onCheckedChange={(checked) => {
startDraft()
setScoreThresholdEnabled(checked)
}}
/>
@ -985,12 +1038,13 @@ export function KnowledgeSettingsForm({
value={scoreThreshold}
disabled={retrievalFieldsDisabled || !scoreThresholdEnabled}
className="w-18 shrink-0"
onBlur={() =>
onBlur={() => {
setScoreThreshold(
clamp(scoreThreshold, SCORE_THRESHOLD_MIN, SCORE_THRESHOLD_MAX),
)
}
}}
onChange={(event) => {
startDraft()
setScoreThreshold(Number(event.target.value))
}}
/>
@ -1002,6 +1056,7 @@ export function KnowledgeSettingsForm({
value={scoreThreshold}
disabled={retrievalFieldsDisabled || !scoreThresholdEnabled}
onValueChange={(value) => {
startDraft()
setScoreThreshold(value)
}}
/>
@ -1050,7 +1105,10 @@ export function KnowledgeSettingsForm({
initialEmoji={{ icon }}
onOpenChange={setIconPickerOpen}
onSelect={(selection) => {
if (selection.type === 'emoji') setIcon(selection.icon)
if (selection.type === 'emoji') {
startDraft()
setIcon(selection.icon)
}
}}
/>

View File

@ -3,6 +3,7 @@
import type { KnowledgeFsExternalAccessResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
import { Button } from '@langgenius/dify-ui/button'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SkeletonRectangle } from '@/app/components/base/skeleton'
import { consoleQuery } from '@/service/client'
@ -18,19 +19,34 @@ const READ_ONLY_EXTERNAL_ACCESS: KnowledgeFsExternalAccessResponse = {
}
function KnowledgeSettingsSkeleton() {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const { t: tSettings } = useTranslation('datasetSettings')
return (
<div className="flex flex-col gap-4 pt-2" role="status">
<SkeletonRectangle className="h-5 w-28 rounded-md" />
<div className="flex flex-col gap-4 pt-2">
<span className="sr-only" role="status">
{tCommon(($) => $.loading)}
</span>
<h2 className="flex h-8 items-center system-sm-semibold text-text-secondary">
{t(($) => $['newKnowledge.settings.basicInfo'])}
</h2>
<div className="flex gap-1">
<SkeletonRectangle className="h-8 w-45 rounded-md" />
<div className="flex h-8 w-45 items-center system-sm-semibold text-text-secondary">
{tSettings(($) => $['form.nameAndIcon'])}
</div>
<SkeletonRectangle className="h-8 flex-1 rounded-lg" />
</div>
<div className="flex gap-1">
<SkeletonRectangle className="h-20 w-45 rounded-md" />
<div className="flex h-7 w-45 items-center system-sm-semibold text-text-secondary">
{tSettings(($) => $['form.desc'])}
</div>
<SkeletonRectangle className="h-20 flex-1 rounded-lg" />
</div>
<div className="flex gap-1">
<SkeletonRectangle className="h-9 w-45 rounded-md" />
<div className="flex h-7 w-45 items-center system-sm-semibold text-text-secondary">
{tSettings(($) => $['form.permissions'])}
</div>
<SkeletonRectangle className="h-9 flex-1 rounded-lg" />
</div>
<div className="flex justify-end gap-2 pt-1">
@ -39,16 +55,40 @@ function KnowledgeSettingsSkeleton() {
</div>
<div className="h-px bg-divider-subtle" />
<div className="flex gap-1">
<SkeletonRectangle className="h-7 w-45 rounded-md" />
<div className="flex h-7 w-45 items-center system-sm-semibold text-text-secondary">
{t(($) => $['newKnowledge.settings.apiAccessLabel'])}
</div>
<SkeletonRectangle className="h-7 flex-1 rounded-md" />
</div>
<div className="h-px bg-divider-subtle" />
<div className="flex gap-1">
<SkeletonRectangle className="h-20 w-45 rounded-md" />
<div className="w-45 shrink-0">
<h2 className="flex h-8 items-center system-sm-semibold text-text-secondary">
{t(($) => $['newKnowledge.settings.retrievalTitle'])}
</h2>
<p className="body-xs-regular text-text-tertiary">
{t(($) => $['newKnowledge.settings.retrievalDescription'])}
</p>
</div>
<div className="flex flex-1 flex-col gap-3">
<SkeletonRectangle className="h-16 w-full rounded-lg" />
<SkeletonRectangle className="h-16 w-full rounded-lg" />
<SkeletonRectangle className="h-16 w-full rounded-lg" />
<div>
<div className="flex h-7 items-center system-sm-medium text-text-secondary">
{t(($) => $['newKnowledge.settings.systemReasoningModelLabel'])}
</div>
<SkeletonRectangle className="h-9 w-full rounded-lg" />
</div>
<div>
<div className="flex h-7 items-center system-sm-medium text-text-secondary">
{t(($) => $['newKnowledge.settings.embeddingModelLabel'])}
</div>
<SkeletonRectangle className="h-9 w-full rounded-lg" />
</div>
<div>
<div className="flex h-7 items-center system-sm-medium text-text-secondary">
{tCommon(($) => $['modelProvider.rerankModel.key'])}
</div>
<SkeletonRectangle className="h-9 w-full rounded-lg" />
</div>
<SkeletonRectangle className="h-8 w-48 rounded-lg" />
<div className="flex gap-4">
<SkeletonRectangle className="h-16 flex-1 rounded-lg" />
@ -58,8 +98,22 @@ function KnowledgeSettingsSkeleton() {
</div>
<div className="h-px bg-divider-subtle" />
<div className="flex gap-1 pt-7">
<SkeletonRectangle className="h-8 w-45 rounded-md" />
<SkeletonRectangle className="h-[58px] flex-1 rounded-xl" />
<div className="w-45 shrink-0">
<h2 className="flex h-8 items-center system-sm-semibold text-text-destructive">
{t(($) => $['newKnowledge.settings.dangerZone'])}
</h2>
</div>
<div className="flex min-h-[58px] flex-1 items-center justify-between rounded-xl border border-components-panel-border bg-background-section px-4">
<div>
<p className="system-sm-medium text-text-secondary">
{t(($) => $['newKnowledge.settings.deleteTitle'])}
</p>
<p className="system-xs-regular text-text-tertiary">
{t(($) => $['newKnowledge.settings.deleteDescription'])}
</p>
</div>
<SkeletonRectangle className="h-8 w-16 rounded-lg" />
</div>
</div>
</div>
)
@ -96,6 +150,7 @@ export function KnowledgeSettingsPage({ knowledgeSpaceId }: { knowledgeSpaceId:
enabled: canManageAccess,
})
const membersQuery = useMembers()
const [activeDraftVersion, setActiveDraftVersion] = useState<string>()
const isPending =
spaceQuery.isPending ||
@ -107,6 +162,19 @@ export function KnowledgeSettingsPage({ knowledgeSpaceId }: { knowledgeSpaceId:
settingsQuery.isError ||
membersQuery.isError ||
(canManageAccess && (permissionsQuery.isError || externalAccessQuery.isError))
const settingsFormVersion = [
knowledgeSpaceId,
spaceQuery.data?.resource_version ?? 0,
settingsQuery.data?.revision ?? 0,
externalAccessQuery.data?.revision ?? 0,
permissionsQuery.data?.data
.map((permission) => `${permission.account_id}:${permission.revision}`)
.sort()
.join('|') ?? '',
].join(':')
const settingsFormKey = activeDraftVersion ?? settingsFormVersion
const serverConflict =
activeDraftVersion !== undefined && activeDraftVersion !== settingsFormVersion
const retry = () => {
const requests: Promise<unknown>[] = [
@ -147,17 +215,15 @@ export function KnowledgeSettingsPage({ knowledgeSpaceId }: { knowledgeSpaceId:
{!isPending && !hasError && spaceQuery.data && settingsQuery.data && (
<KnowledgeSettingsForm
key={[
spaceQuery.data.resource_version,
settingsQuery.data.revision,
externalAccessQuery.data?.revision ?? 0,
permissionsQuery.data?.data.map((permission) => permission.revision).join('-') ?? '',
].join(':')}
key={settingsFormKey}
externalAccess={externalAccessQuery.data ?? READ_ONLY_EXTERNAL_ACCESS}
members={membersQuery.data?.accounts ?? []}
permissions={permissionsQuery.data?.data ?? []}
serverConflict={serverConflict}
settings={settingsQuery.data}
space={spaceQuery.data}
onDraftFinish={() => setActiveDraftVersion(undefined)}
onDraftStart={() => setActiveDraftVersion((current) => current ?? settingsFormVersion)}
/>
)}
</div>

View File

@ -832,7 +832,7 @@ function RecentActivity({
<h2 className="text-[15px] leading-6 font-medium text-text-secondary">
{t(($) => $['newKnowledge.overview.recentActivity'])}
</h2>
<Button disabled={loading} size="small" variant="secondary" onClick={onOpenAll}>
<Button size="small" variant="secondary" onClick={onOpenAll}>
{t(($) => $['newKnowledge.overview.allActivity'])}
</Button>
</header>
@ -847,7 +847,9 @@ function RecentActivity({
role="row"
className="grid grid-cols-[100px_minmax(280px,1fr)_200px] items-center gap-3 pb-2 system-2xs-medium-uppercase text-text-tertiary"
>
<span role="columnheader">{t(($) => $['newKnowledge.overview.when'])}</span>
<span role="columnheader">
<span className="sr-only">{t(($) => $['newKnowledge.overview.when'])}</span>
</span>
<span role="columnheader">{t(($) => $['newKnowledge.overview.activity'])}</span>
<span role="columnheader">{t(($) => $['newKnowledge.overview.operator'])}</span>
</div>
@ -1062,7 +1064,7 @@ function ActivityDrawer({
: members.find((member) => `member:${member.id}` === operator)?.name || operator.slice(7)
const clearFilters = () => {
restoreFilterFocusRef.current = true
onRangeChange('7d')
onRangeChange('today')
onOperatorChange('all')
}
@ -1489,7 +1491,8 @@ function Onboarding({
}
}
if (indexingTask)
if (indexingTask) {
const progressKnown = indexingTask.progress_total > 0
return (
<section className="flex h-29.75 flex-col rounded-xl bg-background-section p-4">
<h2 className="text-[18px] leading-[1.2] font-semibold text-text-primary">
@ -1503,7 +1506,14 @@ function Onboarding({
{t(($) => $['newKnowledge.overview.indexingConnectedDescription'])}
</p>
<div className="mt-3">
<div className="h-2 overflow-hidden rounded-full bg-util-colors-gray-gray-200">
<div
role="progressbar"
aria-label={t(($) => $['newKnowledge.overview.indexing'])}
aria-valuemin={0}
aria-valuemax={progressKnown ? indexingTask.progress_total : undefined}
aria-valuenow={progressKnown ? indexingTask.progress_completed : undefined}
className="h-2 overflow-hidden rounded-full bg-util-colors-gray-gray-200"
>
<div
className="h-full rounded-full bg-components-progress-bar-progress-solid"
style={{ width: `${indexingTask.progress_percent}%` }}
@ -1518,6 +1528,7 @@ function Onboarding({
</div>
</section>
)
}
return (
<section
@ -1674,9 +1685,9 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
uploadAvailable && hasPermission(datasetDefaultPermissionKeys, DatasetACLPermission.Edit)
const [window, setWindow] = useQueryState('window', overviewWindowParser)
const [activityOpen, setActivityOpen] = useState(false)
const [activityRange, setActivityRange] = useState<ActivityRange>('7d')
const [activityRange, setActivityRange] = useState<ActivityRange>('today')
const [activityDates, setActivityDates] = useState<ActivityDateRange>(() =>
activityDatesForRange('7d'),
activityDatesForRange('today'),
)
const [activityOperator, setActivityOperator] = useState<ActivityOperator>('all')
const activityFrom = activityRange === 'all' ? undefined : activityDates.start.toISOString()
@ -1840,12 +1851,12 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
activityPreviewQuery.isError)
const hasContent =
(statsQuery.data?.source_count ?? 0) > 0 || (statsQuery.data?.documents ?? 0) > 0
const empty = !pageLoading && !statsQuery.isError && !hasContent
const showIndexing =
!pageLoading &&
!inventoryQuery.isError &&
(inventoryQuery.data?.index_coverage.indexed ?? 0) === 0 &&
indexingTask !== undefined
const empty = !pageLoading && !statsQuery.isError && !hasContent && !showIndexing
const showEmptyModules = empty || showIndexing
const retry = () =>
void Promise.all([
@ -1860,7 +1871,7 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
return (
<main className="min-h-0 min-w-0 flex-1 overflow-y-auto bg-components-panel-bg">
<div className="mx-auto w-full max-w-332 px-5 py-6 sm:px-8">
<header className="flex flex-wrap items-center justify-between gap-3">
<header className="flex flex-wrap items-center gap-3">
<h1 className="text-[18px] leading-[1.2] font-bold text-text-primary">
{t(($) => $['newKnowledge.overviewTitle'])}
</h1>
@ -1890,6 +1901,12 @@ export function KnowledgeOverviewPage({ knowledgeSpaceId }: { knowledgeSpaceId:
)}
</header>
{pageLoading && (
<p className="sr-only" role="status">
{tCommon(($) => $.loading)}
</p>
)}
{firstLoadFailed && (
<div
role="alert"

View File

@ -20,7 +20,7 @@ import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { consoleClient } from '@/service/client'
import { taskCanRetry, taskIsActive, taskVersionIsAfter } from './document-model'
import { taskCanCancel, taskCanRetry, taskIsActive, taskVersionIsAfter } from './document-model'
import { backgroundTaskFromApi } from './document-models'
type TaskAction = 'cancel' | 'retry'
@ -229,14 +229,16 @@ export function ProcessingTasksDrawer({
const progress = taskProgressStore.get(task.id)
if (!progress || !taskIsActive(task) || taskVersionIsAfter(task.updatedAt, progress.updatedAt))
return task
const stateChanged = progress.state !== task.state
return {
...task,
...(stateChanged ? { canCancel: undefined, canRetry: undefined } : {}),
errorCode: undefined,
errorMessage: undefined,
...progress,
}
})
const activeActionCount = orderedTasks.filter(taskIsActive).length
const activeActionCount = orderedTasks.filter(taskCanCancel).length
const retryActionCount = orderedTasks.filter(taskCanRetry).length
const taskLifecycles = useMemo(
() => new Map(tasks.map((task) => [task.id, taskLifecycle(task)])),
@ -529,11 +531,15 @@ export function ProcessingTasksDrawer({
className={
task.state === 'failed'
? 'i-ri-error-warning-fill size-4 shrink-0 text-text-destructive'
: taskIsActive(task)
? 'i-ri-loader-2-line size-4 shrink-0 animate-spin text-text-accent motion-reduce:animate-none'
: task.state === 'succeeded'
? 'i-ri-check-line size-4 shrink-0 text-text-success'
: 'i-ri-indeterminate-circle-line size-4 shrink-0 text-text-tertiary'
: task.state === 'dispatch_pending' ||
task.state === 'queued' ||
task.state === 'retry_wait'
? 'i-ri-time-line size-4 shrink-0 text-text-tertiary'
: taskIsActive(task)
? 'i-ri-loader-2-line size-4 shrink-0 animate-spin text-text-accent motion-reduce:animate-none'
: task.state === 'succeeded'
? 'i-ri-check-line size-4 shrink-0 text-text-success'
: 'i-ri-indeterminate-circle-line size-4 shrink-0 text-text-tertiary'
}
/>
<div className="min-w-0 flex-1">
@ -567,7 +573,7 @@ export function ProcessingTasksDrawer({
</p>
)}
</div>
{canEdit && taskIsActive(task) ? (
{canEdit && taskCanCancel(task) ? (
<Button
aria-label={
activeActionCount > 1

View File

@ -140,7 +140,7 @@ export function GoldenQuestionDialog({
</FieldLabel>
<Textarea
aria-invalid={annotationInvalid}
className="h-16 resize-y"
className={mode === 'edit' ? 'h-22 min-h-22 resize-y' : 'h-16 min-h-16 resize-y'}
placeholder={t(($) => $['newKnowledge.qualityPage.annotationPlaceholder'])}
value={annotation}
onValueChange={setAnnotation}

View File

@ -21,8 +21,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -544,12 +544,26 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
</span>
))}
</div>
<Tooltip>
<TooltipTrigger className="block min-w-0 truncate text-left system-xs-regular text-text-secondary">
{item.annotation}
</TooltipTrigger>
<TooltipContent className="max-w-67">{item.annotation}</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger
openOnHover
delay={300}
closeDelay={200}
render={
<button
type="button"
className="block min-w-0 truncate text-left system-xs-regular text-text-secondary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
>
{item.annotation}
</button>
}
/>
<PopoverContent placement="top" popupClassName="max-w-67 px-3 py-2">
<p className="system-xs-regular wrap-break-word text-text-tertiary">
{item.annotation}
</p>
</PopoverContent>
</Popover>
<span className="system-xs-regular text-text-secondary">
{updated(item.updated_at)}
</span>
@ -611,12 +625,12 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
)}
</div>
) : (
<div className="flex min-h-[calc(100vh-15rem)] flex-col items-center justify-center text-center">
<div className="flex min-h-[calc(100vh-22rem)] flex-col items-center justify-center text-center">
<span aria-hidden className="i-ri-thumb-up-line size-8 text-text-tertiary" />
<h2 className="mt-4 system-md-semibold text-text-primary">
{t(($) => $['newKnowledge.qualityPage.goldenEmptyTitle'])}
</h2>
<p className="mt-1 max-w-105 system-xs-regular text-text-tertiary">
<p className="mt-1 max-w-128 system-xs-regular text-text-tertiary">
{t(($) => $['newKnowledge.qualityPage.goldenEmptyDescription'])}
</p>
<div className="mt-4 flex gap-2">
@ -709,7 +723,7 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
disabled={pendingBadCaseId === item.id}
onClick={() => void openTrace(item.id)}
>
<span aria-hidden className="i-ri-node-tree size-4" />
<span aria-hidden className="i-ri-arrow-right-up-line size-4" />
{t(($) => $['newKnowledge.qualityPage.openTrace'])}
</DropdownMenuItem>
{item.status !== 'fixed' && (
@ -753,12 +767,12 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
)}
</div>
) : (
<div className="flex min-h-[calc(100vh-15rem)] flex-col items-center justify-center text-center">
<div className="flex min-h-[calc(100vh-22rem)] flex-col items-center justify-center text-center">
<span aria-hidden className="i-ri-check-line size-8 text-text-tertiary" />
<h2 className="mt-4 system-md-semibold text-text-primary">
{t(($) => $['newKnowledge.qualityPage.badCasesEmptyTitle'])}
</h2>
<p className="mt-1 max-w-120 system-xs-regular text-text-tertiary">
<p className="mt-1 max-w-136 system-xs-regular text-text-tertiary">
{t(($) => $['newKnowledge.qualityPage.badCasesEmptyDescription'])}
</p>
{badCaseQuery.hasNextPage && (
@ -775,7 +789,7 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
))}
{activeTab === 'golden' && selected.size > 0 && (
<div className="fixed bottom-6 left-[calc(50%+var(--new-rag-sidebar-width)/2)] flex h-12 -translate-x-1/2 items-center gap-3 rounded-xl border border-components-panel-border bg-components-panel-bg px-4 shadow-xl">
<div className="fixed bottom-6 left-[calc(50%+var(--new-rag-sidebar-width)/2)] flex h-12 -translate-x-1/2 items-center gap-2 rounded-xl border border-components-panel-border bg-components-panel-bg px-3 shadow-xl">
<span className="system-sm-medium text-text-primary">
{t(
($) =>
@ -788,8 +802,11 @@ export function QualityPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
)}
</span>
<span aria-hidden className="h-5 w-px bg-divider-regular" />
<Button variant="ghost" onClick={() => setDeleteIds(new Set(selected))}>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
<Button
variant="secondary"
tone="destructive"
onClick={() => setDeleteIds(new Set(selected))}
>
{t(($) => $['newKnowledge.qualityPage.deleteEllipsis'])}
</Button>
<button

View File

@ -24,6 +24,8 @@ export type RetrievalTestRecord =
kind: 'local'
mode: Exclude<RetrievalTestMode, 'research'>
query: string
durationMs?: number
resultCount?: number
status: 'completed' | 'failed' | 'running'
}
| {

View File

@ -75,6 +75,133 @@ function formatRecordTime(value: number) {
}).format(value)
}
function RecordTime({ value }: { value: number }) {
const { t } = useTranslation('dataset')
const [showJustNow, setShowJustNow] = useState(() => {
const age = Date.now() - value
return age >= 0 && age < 60_000
})
useEffect(() => {
if (!showJustNow) return
const timeout = globalThis.setTimeout(
() => setShowJustNow(false),
Math.max(0, value + 60_000 - Date.now()),
)
return () => globalThis.clearTimeout(timeout)
}, [showJustNow, value])
return showJustNow ? t(($) => $['newKnowledge.retrievalTest.justNow']) : formatRecordTime(value)
}
type ResearchPayloadLabels = {
chunks: string
documents: string
sources: string
}
const researchPayloadContainers = new Set([
'analysis',
'analyzing',
'candidates',
'chunks',
'coverage',
'data',
'details',
'documents',
'findings',
'generating',
'generation',
'items',
'plan',
'planning',
'questions',
'results',
'retrieval',
'retrieving',
'sources',
'topics',
'warnings',
])
const researchPayloadLabels = new Set(['name', 'query', 'question', 'title', 'topic'])
const researchPayloadText = new Set([
...researchPayloadLabels,
'coverage',
'coveragegap',
'coveragegapwarning',
'coveragewarning',
'finding',
'findings',
'mergedcandidatesummary',
'mergedsummary',
'message',
'questions',
'result',
'results',
'summary',
'topics',
'warning',
'warnings',
])
function normalizedPayloadKey(key: string) {
return key.replaceAll(/[^a-z0-9]/gi, '').toLocaleLowerCase()
}
function payloadCountLabel(key: string, labels: ResearchPayloadLabels) {
const normalizedKey = normalizedPayloadKey(key)
if (normalizedKey === 'chunkcount' || normalizedKey === 'chunks') return labels.chunks
if (normalizedKey === 'documentcount' || normalizedKey === 'documents') return labels.documents
if (normalizedKey === 'sourcecount' || normalizedKey === 'sources') return labels.sources
}
function researchPayloadLines(payload: Record<string, unknown>, labels: ResearchPayloadLabels) {
const lines: string[] = []
const visit = (value: unknown, key = '', depth = 0) => {
if (depth > 3) return
const normalizedKey = normalizedPayloadKey(key)
if (typeof value === 'string') {
if (value.trim() && researchPayloadText.has(normalizedKey)) lines.push(value.trim())
return
}
if (typeof value === 'number') {
const countLabel = payloadCountLabel(key, labels)
if (countLabel) lines.push(`${countLabel}: ${value}`)
return
}
if (Array.isArray(value)) {
if (!researchPayloadContainers.has(normalizedKey)) return
value.forEach((item) => visit(item, key, depth + 1))
return
}
if (!value || typeof value !== 'object') return
if (key && !researchPayloadContainers.has(normalizedKey)) return
const record = value as Record<string, unknown>
const entries = Object.entries(record)
const labelEntry = entries.find(
([candidate, nested]) =>
researchPayloadLabels.has(normalizedPayloadKey(candidate)) && typeof nested === 'string',
)
const countEntry = entries.find(
([candidate, nested]) =>
['chunkcount', 'chunks', 'count'].includes(normalizedPayloadKey(candidate)) &&
typeof nested === 'number',
)
if (labelEntry) {
const [, label] = labelEntry
lines.push(
`${String(label).trim()}${countEntry ? ` · ${countEntry[1]} ${labels.chunks}` : ''}`,
)
}
entries.forEach(([nestedKey, nested]) => {
if (nestedKey !== labelEntry?.[0] && nestedKey !== countEntry?.[0])
visit(nested, nestedKey, depth + 1)
})
}
Object.entries(payload).forEach(([key, value]) => visit(value, key))
return [...new Set(lines)].slice(0, 12)
}
function useClock(enabled: boolean) {
const [now, setNow] = useState(() => Date.now())
useEffect(() => {
@ -321,14 +448,13 @@ function FailedResult({ description, onRetry }: { description: string; onRetry:
return (
<div
role="alert"
className="flex items-start gap-3 rounded-xl border border-components-panel-border bg-background-section px-4 py-3"
className="flex min-h-10 items-center gap-2 rounded-lg bg-util-colors-red-red-50 px-3 py-2"
>
<span aria-hidden className="mt-0.5 i-ri-error-warning-fill size-4 text-text-destructive" />
<span className="min-w-0 flex-1">
<span className="block system-sm-semibold text-text-primary">
{t(($) => $['newKnowledge.retrievalTest.failedTitle'])}
</span>
<span className="mt-0.5 block body-xs-regular text-text-tertiary">{description}</span>
<span aria-hidden className="i-ri-alert-line size-4 text-text-destructive" />
<span className="min-w-0 flex-1 system-xs-regular break-words text-text-destructive">
{t(($) => $['newKnowledge.retrievalTest.failedTitle'])}
{' — '}
<span>{description}</span>
</span>
<Button size="small" variant="secondary" onClick={onRetry}>
{t(($) => $['newKnowledge.retrievalTest.retry'])}
@ -370,12 +496,14 @@ function ResearchAnswer({ answer, streaming }: { answer: string; streaming: bool
}
function QualityActions({
badCaseAvailable,
decision,
noResults,
onDecision,
pending,
qualityHref,
}: {
badCaseAvailable: boolean
decision?: QualityDecision
noResults?: boolean
onDecision: (decision: QualityDecision) => Promise<void>
@ -406,17 +534,21 @@ function QualityActions({
</div>
)
}
if (!badCaseAvailable && noResults) return null
return (
<div className="flex shrink-0 items-center justify-end gap-3 border-t border-divider-regular pt-4 pb-1">
<Button
disabled={pending}
loading={pending}
variant={noResults ? 'primary' : 'secondary'}
onClick={() => void onDecision('bad-case')}
>
<span aria-hidden className="mr-1 i-ri-thumb-down-line size-4" />
{t(($) => $['newKnowledge.retrievalTest.makeBadCase'])}
</Button>
{badCaseAvailable && (
<Button
disabled={pending}
loading={pending}
variant="secondary"
onClick={() => void onDecision('bad-case')}
>
<span aria-hidden className="mr-1 i-ri-thumb-down-line size-4" />
{t(($) => $['newKnowledge.retrievalTest.makeBadCase'])}
</Button>
)}
{!noResults && (
<Button
disabled={pending}
@ -491,6 +623,11 @@ function ResearchProcess({
planning: t(($) => $['newKnowledge.retrievalTest.planningActive']),
retrieving: t(($) => $['newKnowledge.retrievalTest.retrievingActive']),
}
const payloadLabels: ResearchPayloadLabels = {
chunks: t(($) => $['newKnowledge.chunkCount']),
documents: t(($) => $['newKnowledge.documents']),
sources: t(($) => $['newKnowledge.sources']),
}
return (
<section
@ -526,6 +663,7 @@ function ResearchProcess({
<Button
size="small"
variant="secondary"
className="ml-auto"
onClick={(event) => {
event.stopPropagation()
onCancel()
@ -551,6 +689,12 @@ function ResearchProcess({
const current = index === currentIndex && active
const stageDuration =
actualStageDuration(events, stage, task, now) ?? estimatedStageDuration(plan, stage)
const stagePayload = [...events]
.reverse()
.find((event) => event.stage === stage)?.payload
const payloadLines = stagePayload
? researchPayloadLines(stagePayload, payloadLabels)
: []
return (
<li key={stage} className="flex items-stretch gap-2.5 overflow-hidden">
<span
@ -599,6 +743,15 @@ function ResearchProcess({
})}
</span>
)}
{payloadLines.length > 0 && (completed || current) && (
<ul className="mt-1.5 space-y-1 system-xs-regular text-text-tertiary">
{payloadLines.map((line) => (
<li key={line} className="truncate">
{line}
</li>
))}
</ul>
)}
</span>
</li>
)
@ -650,12 +803,16 @@ function RecordButton({
{' · '}
{researchStageOrder.indexOf(activeResearchStage) + 1}/{researchStageOrder.length}
</>
) : record.kind === 'local' && record.status === 'failed' && record.durationMs ? (
`${t(($) => $['newKnowledge.retrievalTest.failedTitle'])} · ${formatDuration(record.durationMs)}`
) : record.kind === 'local' && record.resultCount !== undefined && record.durationMs ? (
`${record.resultCount} ${t(($) => $['newKnowledge.chunkCount']).toLocaleLowerCase()} · ${formatDuration(record.durationMs)}`
) : (
t(($) => $[`newKnowledge.settings.retrievalMode.${record.mode}`])
)}
</span>
<span className="shrink-0 text-[11px] leading-4 text-text-primary opacity-30">
{formatRecordTime(record.createdAt)}
<RecordTime value={record.createdAt} />
</span>
</span>
</span>
@ -714,7 +871,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
[researchTasksQuery.data?.data, tracesQuery.data?.data],
)
const displayRecords = useMemo<RetrievalTestRecord[]>(() => {
if (!localRun) return records
if (!localRun || localRun.status === 'running') return records
const traceAlreadyListed =
localRun.traceId &&
records.some((record) => record.kind === 'trace' && record.id === localRun.traceId)
@ -726,6 +883,8 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
kind: 'local',
mode: localRun.mode,
query: localRun.query,
durationMs: localRun.endedAt ? localRun.endedAt - localRun.startedAt : undefined,
resultCount: localRun.evidence.length,
status: localRun.status === 'no-results' ? 'completed' : localRun.status,
},
...records,
@ -743,8 +902,9 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
useEffect(() => {
selectedResearchActiveRef.current = selectedResearchActive
}, [selectedResearchActive])
const selectedResearchDefaultExpanded = researchTaskIsActive(selectedResearchTask)
const selectedResearchExpanded = selectedResearchTask
? (researchExpanded[selectedResearchTask.id] ?? true)
? (researchExpanded[selectedResearchTask.id] ?? selectedResearchDefaultExpanded)
: false
const selectedTraceId =
selected?.kind === 'trace'
@ -917,7 +1077,8 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
(selected?.kind === 'trace' && traceEvidenceQuery.isPending)
const selectedFailed = selected?.kind === 'local' && localRun?.status === 'failed'
const selectedHasNoResults = selected?.kind === 'local' && localRun?.status === 'no-results'
const visibleEvidence = showAll ? currentEvidence : currentEvidence.slice(0, 5)
const initialEvidenceCount = selectedMode === 'research' ? 5 : 3
const visibleEvidence = showAll ? currentEvidence : currentEvidence.slice(0, initialEvidenceCount)
const selectRecord = (record: RetrievalTestRecord) => {
setSelected({ id: record.id, kind: record.kind })
@ -1046,9 +1207,21 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
}
: current,
)
} finally {
if (queryAbortControllerRef.current === controller)
queryAbortControllerRef.current = undefined
}
}
const cancelFastQuery = () => {
if (localRun?.status !== 'running') return
queryAbortControllerRef.current?.abort()
queryAbortControllerRef.current = undefined
setLocalRun(undefined)
setSelected(undefined)
setShowAll(false)
}
const startResearch = async () => {
const cleanQuery = query.trim()
if (!cleanQuery) return
@ -1088,7 +1261,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
}
const run = () => {
if (selectedResearchActive) return
if (selectedResearchActive || localRun?.status === 'running') return
if (mode === 'research') void startResearch()
else void runFastQuery()
}
@ -1115,6 +1288,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
id="retrieval-test-query"
value={query}
maxLength={2000}
disabled={selectedResearchActive || localRun?.status === 'running'}
placeholder={t(($) => $['newKnowledge.retrievalTest.queryPlaceholder'])}
className="block h-36 w-full resize-none bg-transparent p-3.5 body-md-regular text-text-primary outline-hidden placeholder:text-text-quaternary"
onChange={(event) => setQuery(event.target.value)}
@ -1135,7 +1309,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
<button
key={item}
type="button"
disabled={selectedResearchActive}
disabled={selectedResearchActive || localRun?.status === 'running'}
aria-pressed={mode === item}
className={cn(
'rounded-md px-2.5 py-1 system-sm-regular text-text-tertiary capitalize outline-hidden hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:text-text-disabled',
@ -1148,18 +1322,24 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
</button>
))}
</div>
<Button
variant="primary"
disabled={!query.trim() || selectedResearchActive}
onClick={run}
>
<span aria-hidden className="mr-1 i-ri-play-circle-line size-4" />
{t(($) =>
mode === 'research'
? $['newKnowledge.retrievalTest.startResearch']
: $['newKnowledge.retrievalTest.run'],
)}
</Button>
{localRun?.status === 'running' ? (
<Button onClick={cancelFastQuery}>
{t(($) => $['newKnowledge.retrievalTest.cancel'])}
</Button>
) : (
<Button
variant="primary"
disabled={!query.trim() || selectedResearchActive}
onClick={run}
>
<span aria-hidden className="mr-1 i-ri-play-circle-line size-4" />
{t(($) =>
mode === 'research'
? $['newKnowledge.retrievalTest.startResearch']
: $['newKnowledge.retrievalTest.run'],
)}
</Button>
)}
</div>
</div>
</div>
@ -1214,7 +1394,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
</span>
{selectedCreatedAt && (
<span className="shrink-0 text-[11px] leading-4 text-text-tertiary">
{formatRecordTime(selectedCreatedAt)}
<RecordTime key={selectedCreatedAt} value={selectedCreatedAt} />
</span>
)}
<span className="min-w-0 flex-1" />
@ -1226,7 +1406,9 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
onClick={() =>
setResearchExpanded((current) => ({
...current,
[selectedResearchTask.id]: !(current[selectedResearchTask.id] ?? true),
[selectedResearchTask.id]: !(
current[selectedResearchTask.id] ?? selectedResearchDefaultExpanded
),
}))
}
>
@ -1254,7 +1436,9 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
onToggle={() =>
setResearchExpanded((current) => ({
...current,
[selectedResearchTask.id]: !(current[selectedResearchTask.id] ?? true),
[selectedResearchTask.id]: !(
current[selectedResearchTask.id] ?? selectedResearchDefaultExpanded
),
}))
}
onCancel={
@ -1332,7 +1516,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
)}
</div>
{!showAll && currentEvidence.length > 5 && (
{!showAll && currentEvidence.length > initialEvidenceCount && (
<div className="shrink-0 pl-1">
<button
type="button"
@ -1352,6 +1536,7 @@ export function RetrievalTestPage({ knowledgeSpaceId }: { knowledgeSpaceId: stri
!researchTaskIsActive(selectedResearchTask) &&
resultKey && (
<QualityActions
badCaseAvailable={Boolean(selectedTraceId)}
noResults={currentEvidence.length === 0}
decision={qualityDecisions[resultKey]}
onDecision={saveQualityDecision}

View File

@ -107,6 +107,12 @@ function sourceWorkflowStatus(state: string): SourceStatus {
function getOpenableSourceUri(uri: string) {
try {
const url = new URL(uri)
if (url.protocol === 's3:' && url.hostname) {
const prefix = decodeURIComponent(url.pathname.replace(/^\//, ''))
const consoleUrl = new URL(`https://s3.console.aws.amazon.com/s3/buckets/${url.hostname}`)
if (prefix) consoleUrl.searchParams.set('prefix', prefix)
return consoleUrl.toString()
}
return url.protocol === 'http:' || url.protocol === 'https:' ? uri : undefined
} catch {
return undefined
@ -309,7 +315,11 @@ function SourceRow({
const providerName = metadataString(source.metadata, 'providerName')
const syncPolicy = metadataString(source.metadata, 'syncPolicy')
const lastSync = metadataString(source.metadata, 'lastSyncedAt')
const typeLabel = t(($) => $[`newKnowledge.sourceType.${source.type}`])
const typeLabel =
source.type === 'connector' &&
(providerName === 'Notion' || providerName === 'Google Docs' || providerName === 'Confluence')
? t(($) => $['newKnowledge.onlineDocuments'])
: t(($) => $[`newKnowledge.sourceType.${source.type}`])
const sourceIcon =
source.type === 'web'
? 'i-ri-global-line'
@ -433,7 +443,12 @@ function SourceRow({
{providerName && <p className="system-2xs-regular text-text-tertiary">{typeLabel}</p>}
</td>
<td className="w-24 py-2 pr-3 sm:w-35">
<span className="inline-flex items-center gap-1.5 system-xs-medium text-text-primary">
<span
className={cn(
'inline-flex items-center gap-1.5 system-xs-medium text-text-primary',
visibleSource.status === 'syncing' && 'text-text-accent',
)}
>
<StatusDot
status={statusDotStatus[visibleSource.status]}
className={cn(
@ -784,14 +799,15 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
className="h-auto gap-1 rounded px-0"
>
{t(($) => $['newKnowledge.sourceColumn'])}
<span
aria-hidden
className={cn(
'size-3.5',
sort === 'name-desc' ? 'i-ri-arrow-down-line' : 'i-ri-arrow-up-line',
!sort && 'opacity-40',
)}
/>
{sort && (
<span
aria-hidden
className={cn(
'size-3.5',
sort === 'name-desc' ? 'i-ri-arrow-down-line' : 'i-ri-arrow-up-line',
)}
/>
)}
</Button>
</th>
<th className="hidden pb-2 font-medium sm:table-cell">

View File

@ -319,7 +319,7 @@ const CrawlPageList = memo(
</li>
))}
{loading &&
[0, 1].map((placeholder) => (
[0, 1, 2, 3].map((placeholder) => (
<li
key={`placeholder-${placeholder}`}
data-testid="crawl-page-skeleton"
@ -1114,7 +1114,7 @@ export function WebsiteCrawlPreview({
{t(($) => $['newKnowledge.providerConnected'], { provider: providerName })}
</p>
<Form onFormSubmit={handleSubmit}>
<Fieldset disabled={locked} className="space-y-4 disabled:opacity-70">
<Fieldset disabled={locked} className="space-y-4">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<Field name="rootUrl" invalid={urlTouched && !normalizedURL} className="gap-1.5">
<FieldLabel>
@ -1260,9 +1260,9 @@ export function WebsiteCrawlPreview({
</span>
<Button
type="button"
variant="tertiary"
variant="ghost-accent"
size="small"
className="ml-auto shrink-0"
className="ml-auto shrink-0 px-0"
disabled={stopping}
onClick={() => void stop()}
>

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "حفظ التغييرات",
"newKnowledge.settings.saveFailed": "تعذّر حفظ التغييرات. تحقق من اتصالك وحاول مرة أخرى — لا تزال تعديلاتك موجودة.",
"newKnowledge.settings.scoreRange": "النطاق 0.001.00",
"newKnowledge.settings.serverConflict": "تم تغيير هذه الإعدادات في مكان آخر. ألغِ تعديلاتك لتحميل أحدث إصدار قبل الحفظ مرة أخرى.",
"newKnowledge.settings.systemReasoningModelLabel": "نموذج التفكير النظامي",
"newKnowledge.settings.topKLabel": "أفضل K",
"newKnowledge.settings.topKMinimum": "الحد الأدنى = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Änderungen speichern",
"newKnowledge.settings.saveFailed": "Änderungen konnten nicht gespeichert werden. Prüfen Sie Ihre Verbindung und versuchen Sie es erneut — Ihre Änderungen sind weiterhin vorhanden.",
"newKnowledge.settings.scoreRange": "Bereich 0,001,00",
"newKnowledge.settings.serverConflict": "Diese Einstellungen wurden an anderer Stelle geändert. Verwerfen Sie Ihre Änderungen, um vor dem erneuten Speichern die neueste Version zu laden.",
"newKnowledge.settings.systemReasoningModelLabel": "System-Reasoning-Modell",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "Minimum = 1",

View File

@ -170,7 +170,7 @@
"newKnowledge.connectProvider": "Connect {{provider}}",
"newKnowledge.connectProviderGeneric": "Connect provider",
"newKnowledge.connectSource": "Connect a source",
"newKnowledge.connectSourceDescription": "Sync from Website, Notion, or Drive",
"newKnowledge.connectSourceDescription": "Content stays in sync with Notion, websites, and other services.",
"newKnowledge.connectingProvider": "Connecting…",
"newKnowledge.connectionFailed": "We couldn't connect {{provider}}. Re-enter the credential and try again.",
"newKnowledge.connectionNeedsAttention": "{{provider}} connection needs attention",
@ -249,7 +249,7 @@
"newKnowledge.documentTaskLookupIncomplete": "More task history is available. Continue checking to find this document's status.",
"newKnowledge.documentUploadExclusion.batchLimit": "batch size limit exceeded",
"newKnowledge.documentUploadExclusion.countLimit": "file count limit exceeded",
"newKnowledge.documentUploadExclusion.fileSize": "file is too large",
"newKnowledge.documentUploadExclusion.fileSize": "Exceeds 15 MB limit",
"newKnowledge.documentUploadExclusion.fileType": "unsupported or invalid file type",
"newKnowledge.documentUploadExclusion.more": "and {{count}} more",
"newKnowledge.documentUploadExclusion.processing": "processing failed",
@ -262,7 +262,7 @@
"newKnowledge.documentUploadStarted": "Document processing started.",
"newKnowledge.documents": "Documents",
"newKnowledge.documentsDescription": "Knowledge assets and their revisions — where each document came from and whether the latest revision is indexed.",
"newKnowledge.documentsDropHint": "or drop PDF, DOCX, Markdown, HTML, XLSX, TXT to upload",
"newKnowledge.documentsDropHint": "Drag and drop files, or browse · TXT, Markdown, PDF, HTML, XLSX, CSV, JSONL · 15 MB max",
"newKnowledge.documentsEmptyDescription": "Connect a source or upload files to start building this knowledge space.",
"newKnowledge.documentsEmptyTitle": "No documents yet",
"newKnowledge.documentsErrorDescription": "We couldn't load documents. Try again.",
@ -548,7 +548,7 @@
"newKnowledge.retrievalTest.justNow": "Just now",
"newKnowledge.retrievalTest.keepGoldenQuestion": "Keep golden question",
"newKnowledge.retrievalTest.makeBadCase": "Make bad case",
"newKnowledge.retrievalTest.noChunksDescription": "Try a more specific question, or save this run as a bad case for later improvement.",
"newKnowledge.retrievalTest.noChunksDescription": "Nothing in this knowledge base matched the query. Try different wording, or make it a bad case so the gap gets tracked.",
"newKnowledge.retrievalTest.noChunksTitle": "No chunks retrieved",
"newKnowledge.retrievalTest.open": "Open",
"newKnowledge.retrievalTest.page": "Page {{page}}",
@ -567,7 +567,7 @@
"newKnowledge.retrievalTest.running": "Researching…",
"newKnowledge.retrievalTest.savedBadCase": "Saved as bad case",
"newKnowledge.retrievalTest.savedGoldenQuestion": "Saved as golden question",
"newKnowledge.retrievalTest.showAllChunks": "Show all {{count}} sources",
"newKnowledge.retrievalTest.showAllChunks": "Show all {{count}} chunks",
"newKnowledge.retrievalTest.startResearch": "Start research",
"newKnowledge.retrievalTest.title": "Retrieval Test",
"newKnowledge.retrievalTest.viewInQuality": "View in Quality",
@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Save changes",
"newKnowledge.settings.saveFailed": "Couldnt save changes. Check your connection and try again — your edits are still here.",
"newKnowledge.settings.scoreRange": "range 0.001.00",
"newKnowledge.settings.serverConflict": "These settings changed elsewhere. Cancel your edits to load the latest version before saving again.",
"newKnowledge.settings.systemReasoningModelLabel": "System reasoning model",
"newKnowledge.settings.topKLabel": "TOP K",
"newKnowledge.settings.topKMinimum": "min = 1",
@ -685,7 +686,7 @@
"newKnowledge.uploadDocuments": "Upload documents",
"newKnowledge.uploadDropZoneTitle": "Drag & drop files here, or click to browse",
"newKnowledge.uploadFiles": "Upload files",
"newKnowledge.uploadFilesDescription": "Add documents from your device",
"newKnowledge.uploadFilesDescription": "Add files from your computer — they're indexed as soon as you create.",
"newKnowledge.uploadingFiles": "Uploading…",
"newKnowledge.usingDefaults": "Using defaults",
"newKnowledge.viewLabel": "Knowledge view",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Guardar cambios",
"newKnowledge.settings.saveFailed": "No se pudieron guardar los cambios. Comprueba tu conexión e inténtalo de nuevo; tus cambios siguen aquí.",
"newKnowledge.settings.scoreRange": "rango 0,001,00",
"newKnowledge.settings.serverConflict": "Esta configuración se modificó en otro lugar. Cancela tus cambios para cargar la versión más reciente antes de volver a guardar.",
"newKnowledge.settings.systemReasoningModelLabel": "Modelo de Razonamiento del Sistema",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "mín. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "ذخیره تغییرات",
"newKnowledge.settings.saveFailed": "تغییرات ذخیره نشد. اتصال خود را بررسی کنید و دوباره تلاش کنید — ویرایش‌های شما همچنان حفظ شده‌اند.",
"newKnowledge.settings.scoreRange": "محدوده ۰٫۰۰ تا ۱٫۰۰",
"newKnowledge.settings.serverConflict": "این تنظیمات در جای دیگری تغییر کرده‌اند. پیش از ذخیره دوباره، ویرایش‌های خود را لغو کنید تا آخرین نسخه بارگیری شود.",
"newKnowledge.settings.systemReasoningModelLabel": "مدل استدلال سیستم",
"newKnowledge.settings.topKLabel": "K بالا",
"newKnowledge.settings.topKMinimum": "حداقل = ۱",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Enregistrer les modifications",
"newKnowledge.settings.saveFailed": "Impossible denregistrer les modifications. Vérifiez votre connexion et réessayez — vos modifications sont toujours présentes.",
"newKnowledge.settings.scoreRange": "plage 0,001,00",
"newKnowledge.settings.serverConflict": "Ces paramètres ont été modifiés ailleurs. Annulez vos modifications pour charger la dernière version avant denregistrer à nouveau.",
"newKnowledge.settings.systemReasoningModelLabel": "Modèle de Raisonnement du Système",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "बदलाव सहेजें",
"newKnowledge.settings.saveFailed": "बदलाव सहेजे नहीं जा सके। अपना कनेक्शन जाँचें और फिर कोशिश करें — आपके बदलाव अभी भी सुरक्षित हैं।",
"newKnowledge.settings.scoreRange": "सीमा 0.001.00",
"newKnowledge.settings.serverConflict": "इन सेटिंग्स को कहीं और बदला गया है। दोबारा सहेजने से पहले नवीनतम संस्करण लोड करने के लिए अपने बदलाव रद्द करें।",
"newKnowledge.settings.systemReasoningModelLabel": "सिस्टम तर्क मॉडल",
"newKnowledge.settings.topKLabel": "शीर्ष K",
"newKnowledge.settings.topKMinimum": "न्यूनतम = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Simpan perubahan",
"newKnowledge.settings.saveFailed": "Perubahan tidak dapat disimpan. Periksa koneksi Anda dan coba lagi — perubahan Anda masih tersimpan di sini.",
"newKnowledge.settings.scoreRange": "rentang 0,001,00",
"newKnowledge.settings.serverConflict": "Pengaturan ini telah diubah di tempat lain. Batalkan perubahan Anda untuk memuat versi terbaru sebelum menyimpan lagi.",
"newKnowledge.settings.systemReasoningModelLabel": "Model Penalaran Sistem",
"newKnowledge.settings.topKLabel": "K Teratas",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Salva modifiche",
"newKnowledge.settings.saveFailed": "Impossibile salvare le modifiche. Controlla la connessione e riprova — le modifiche sono ancora presenti.",
"newKnowledge.settings.scoreRange": "intervallo 0,001,00",
"newKnowledge.settings.serverConflict": "Queste impostazioni sono state modificate altrove. Annulla le modifiche per caricare la versione più recente prima di salvare di nuovo.",
"newKnowledge.settings.systemReasoningModelLabel": "Modello di Ragionamento di Sistema",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "変更を保存",
"newKnowledge.settings.saveFailed": "変更を保存できませんでした。接続を確認してもう一度お試しください。編集内容は保持されています。",
"newKnowledge.settings.scoreRange": "範囲 0.001.00",
"newKnowledge.settings.serverConflict": "これらの設定は別の場所で変更されました。もう一度保存する前に編集をキャンセルして最新バージョンを読み込んでください。",
"newKnowledge.settings.systemReasoningModelLabel": "システム推論モデル",
"newKnowledge.settings.topKLabel": "トップ K",
"newKnowledge.settings.topKMinimum": "最小値 = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "변경 사항 저장",
"newKnowledge.settings.saveFailed": "변경 사항을 저장할 수 없습니다. 연결을 확인하고 다시 시도하세요. 편집 내용은 그대로 유지됩니다.",
"newKnowledge.settings.scoreRange": "범위 0.001.00",
"newKnowledge.settings.serverConflict": "이 설정이 다른 곳에서 변경되었습니다. 다시 저장하기 전에 편집을 취소하고 최신 버전을 불러오세요.",
"newKnowledge.settings.systemReasoningModelLabel": "시스템 추론 모델",
"newKnowledge.settings.topKLabel": "상위 K",
"newKnowledge.settings.topKMinimum": "최소 = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Wijzigingen opslaan",
"newKnowledge.settings.saveFailed": "De wijzigingen konden niet worden opgeslagen. Controleer je verbinding en probeer het opnieuw — je wijzigingen zijn nog steeds aanwezig.",
"newKnowledge.settings.scoreRange": "bereik 0,001,00",
"newKnowledge.settings.serverConflict": "Deze instellingen zijn elders gewijzigd. Annuleer je wijzigingen om de nieuwste versie te laden voordat je opnieuw opslaat.",
"newKnowledge.settings.systemReasoningModelLabel": "System Reasoning Model",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Zapisz zmiany",
"newKnowledge.settings.saveFailed": "Nie udało się zapisać zmian. Sprawdź połączenie i spróbuj ponownie — Twoje zmiany nadal są zachowane.",
"newKnowledge.settings.scoreRange": "zakres 0,001,00",
"newKnowledge.settings.serverConflict": "Te ustawienia zostały zmienione w innym miejscu. Anuluj swoje zmiany, aby wczytać najnowszą wersję przed ponownym zapisaniem.",
"newKnowledge.settings.systemReasoningModelLabel": "Model wnioskowania systemowego",
"newKnowledge.settings.topKLabel": "Najlepsze K",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Salvar alterações",
"newKnowledge.settings.saveFailed": "Não foi possível salvar as alterações. Verifique sua conexão e tente novamente — suas edições continuam aqui.",
"newKnowledge.settings.scoreRange": "intervalo 0,001,00",
"newKnowledge.settings.serverConflict": "Estas configurações foram alteradas em outro lugar. Cancele suas edições para carregar a versão mais recente antes de salvar novamente.",
"newKnowledge.settings.systemReasoningModelLabel": "Modelo de Raciocínio do Sistema",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "mín. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Salvați modificările",
"newKnowledge.settings.saveFailed": "Modificările nu au putut fi salvate. Verificați conexiunea și încercați din nou — editările sunt păstrate.",
"newKnowledge.settings.scoreRange": "interval 0,001,00",
"newKnowledge.settings.serverConflict": "Aceste setări au fost modificate în altă parte. Anulați editările pentru a încărca cea mai recentă versiune înainte de a salva din nou.",
"newKnowledge.settings.systemReasoningModelLabel": "Model de raționament de sistem",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Сохранить изменения",
"newKnowledge.settings.saveFailed": "Не удалось сохранить изменения. Проверьте подключение и повторите попытку — внесённые изменения сохранены.",
"newKnowledge.settings.scoreRange": "диапазон 0,001,00",
"newKnowledge.settings.serverConflict": "Эти настройки были изменены в другом месте. Отмените свои правки, чтобы загрузить последнюю версию перед повторным сохранением.",
"newKnowledge.settings.systemReasoningModelLabel": "Модель системного мышления",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "мин. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Shrani spremembe",
"newKnowledge.settings.saveFailed": "Sprememb ni bilo mogoče shraniti. Preverite povezavo in poskusite znova — vaše spremembe so še vedno ohranjene.",
"newKnowledge.settings.scoreRange": "razpon 0,001,00",
"newKnowledge.settings.serverConflict": "Te nastavitve so bile spremenjene drugje. Prekličite svoje spremembe, da pred ponovnim shranjevanjem naložite najnovejšo različico.",
"newKnowledge.settings.systemReasoningModelLabel": "Sistemski model za sklepanja",
"newKnowledge.settings.topKLabel": "Vrh K",
"newKnowledge.settings.topKMinimum": "najm. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "บันทึกการเปลี่ยนแปลง",
"newKnowledge.settings.saveFailed": "ไม่สามารถบันทึกการเปลี่ยนแปลงได้ โปรดตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง — การแก้ไขของคุณยังคงอยู่",
"newKnowledge.settings.scoreRange": "ช่วง 0.001.00",
"newKnowledge.settings.serverConflict": "การตั้งค่าเหล่านี้ถูกเปลี่ยนจากที่อื่น โปรดยกเลิกการแก้ไขเพื่อโหลดเวอร์ชันล่าสุดก่อนบันทึกอีกครั้ง",
"newKnowledge.settings.systemReasoningModelLabel": "แบบจําลองการให้เหตุผลของระบบ",
"newKnowledge.settings.topKLabel": "ท็อป K",
"newKnowledge.settings.topKMinimum": "ต่ำสุด = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Değişiklikleri kaydet",
"newKnowledge.settings.saveFailed": "Değişiklikler kaydedilemedi. Bağlantınızı kontrol edip tekrar deneyin — düzenlemeleriniz hâlâ burada.",
"newKnowledge.settings.scoreRange": "aralık 0,001,00",
"newKnowledge.settings.serverConflict": "Bu ayarlar başka bir yerde değiştirildi. Yeniden kaydetmeden önce en son sürümü yüklemek için düzenlemelerinizi iptal edin.",
"newKnowledge.settings.systemReasoningModelLabel": "Sistem Çıkarım Modeli",
"newKnowledge.settings.topKLabel": "En İyi K",
"newKnowledge.settings.topKMinimum": "min. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Зберегти зміни",
"newKnowledge.settings.saveFailed": "Не вдалося зберегти зміни. Перевірте з’єднання та повторіть спробу — ваші зміни залишилися.",
"newKnowledge.settings.scoreRange": "діапазон 0,001,00",
"newKnowledge.settings.serverConflict": "Ці налаштування було змінено в іншому місці. Скасуйте свої правки, щоб завантажити останню версію перед повторним збереженням.",
"newKnowledge.settings.systemReasoningModelLabel": "Системна модель міркування",
"newKnowledge.settings.topKLabel": "Найкращих K",
"newKnowledge.settings.topKMinimum": "мін. = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "Lưu thay đổi",
"newKnowledge.settings.saveFailed": "Không thể lưu thay đổi. Hãy kiểm tra kết nối và thử lại — nội dung chỉnh sửa của bạn vẫn được giữ nguyên.",
"newKnowledge.settings.scoreRange": "phạm vi 0,001,00",
"newKnowledge.settings.serverConflict": "Các cài đặt này đã được thay đổi ở nơi khác. Hãy hủy chỉnh sửa để tải phiên bản mới nhất trước khi lưu lại.",
"newKnowledge.settings.systemReasoningModelLabel": "Mô hình lập luận hệ thống",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "tối thiểu = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "保存更改",
"newKnowledge.settings.saveFailed": "无法保存更改。请检查网络后重试——你的编辑仍然保留。",
"newKnowledge.settings.scoreRange": "范围 0.001.00",
"newKnowledge.settings.serverConflict": "这些设置已在其他位置更改。请取消当前编辑并加载最新版本,然后再保存。",
"newKnowledge.settings.systemReasoningModelLabel": "系统推理模型",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "最小值 = 1",

View File

@ -620,6 +620,7 @@
"newKnowledge.settings.saveChanges": "儲存變更",
"newKnowledge.settings.saveFailed": "無法儲存變更。請檢查網路後重試——你的編輯仍然保留。",
"newKnowledge.settings.scoreRange": "範圍 0.001.00",
"newKnowledge.settings.serverConflict": "這些設定已在其他位置變更。請取消目前編輯並載入最新版本,然後再儲存。",
"newKnowledge.settings.systemReasoningModelLabel": "系統推理模型",
"newKnowledge.settings.topKLabel": "Top K",
"newKnowledge.settings.topKMinimum": "最小值 = 1",