mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
fix(knowledge-fs): align new rag edge states
This commit is contained in:
parent
1648d8ac6b
commit
d040afebfb
@ -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' }))
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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) && (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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>
|
||||
))}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
@ -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 ||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -24,6 +24,8 @@ export type RetrievalTestRecord =
|
||||
kind: 'local'
|
||||
mode: Exclude<RetrievalTestMode, 'research'>
|
||||
query: string
|
||||
durationMs?: number
|
||||
resultCount?: number
|
||||
status: 'completed' | 'failed' | 'running'
|
||||
}
|
||||
| {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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()}
|
||||
>
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "حفظ التغييرات",
|
||||
"newKnowledge.settings.saveFailed": "تعذّر حفظ التغييرات. تحقق من اتصالك وحاول مرة أخرى — لا تزال تعديلاتك موجودة.",
|
||||
"newKnowledge.settings.scoreRange": "النطاق 0.00–1.00",
|
||||
"newKnowledge.settings.serverConflict": "تم تغيير هذه الإعدادات في مكان آخر. ألغِ تعديلاتك لتحميل أحدث إصدار قبل الحفظ مرة أخرى.",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "نموذج التفكير النظامي",
|
||||
"newKnowledge.settings.topKLabel": "أفضل K",
|
||||
"newKnowledge.settings.topKMinimum": "الحد الأدنى = 1",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -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": "Couldn’t save changes. Check your connection and try again — your edits are still here.",
|
||||
"newKnowledge.settings.scoreRange": "range 0.00–1.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",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -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": "حداقل = ۱",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "Enregistrer les modifications",
|
||||
"newKnowledge.settings.saveFailed": "Impossible d’enregistrer les modifications. Vérifiez votre connexion et réessayez — vos modifications sont toujours présentes.",
|
||||
"newKnowledge.settings.scoreRange": "plage 0,00–1,00",
|
||||
"newKnowledge.settings.serverConflict": "Ces paramètres ont été modifiés ailleurs. Annulez vos modifications pour charger la dernière version avant d’enregistrer à nouveau.",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "Modèle de Raisonnement du Système",
|
||||
"newKnowledge.settings.topKLabel": "Top K",
|
||||
"newKnowledge.settings.topKMinimum": "min. = 1",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "बदलाव सहेजें",
|
||||
"newKnowledge.settings.saveFailed": "बदलाव सहेजे नहीं जा सके। अपना कनेक्शन जाँचें और फिर कोशिश करें — आपके बदलाव अभी भी सुरक्षित हैं।",
|
||||
"newKnowledge.settings.scoreRange": "सीमा 0.00–1.00",
|
||||
"newKnowledge.settings.serverConflict": "इन सेटिंग्स को कहीं और बदला गया है। दोबारा सहेजने से पहले नवीनतम संस्करण लोड करने के लिए अपने बदलाव रद्द करें।",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "सिस्टम तर्क मॉडल",
|
||||
"newKnowledge.settings.topKLabel": "शीर्ष K",
|
||||
"newKnowledge.settings.topKMinimum": "न्यूनतम = 1",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "変更を保存",
|
||||
"newKnowledge.settings.saveFailed": "変更を保存できませんでした。接続を確認してもう一度お試しください。編集内容は保持されています。",
|
||||
"newKnowledge.settings.scoreRange": "範囲 0.00~1.00",
|
||||
"newKnowledge.settings.serverConflict": "これらの設定は別の場所で変更されました。もう一度保存する前に編集をキャンセルして最新バージョンを読み込んでください。",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "システム推論モデル",
|
||||
"newKnowledge.settings.topKLabel": "トップ K",
|
||||
"newKnowledge.settings.topKMinimum": "最小値 = 1",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "변경 사항 저장",
|
||||
"newKnowledge.settings.saveFailed": "변경 사항을 저장할 수 없습니다. 연결을 확인하고 다시 시도하세요. 편집 내용은 그대로 유지됩니다.",
|
||||
"newKnowledge.settings.scoreRange": "범위 0.00–1.00",
|
||||
"newKnowledge.settings.serverConflict": "이 설정이 다른 곳에서 변경되었습니다. 다시 저장하기 전에 편집을 취소하고 최신 버전을 불러오세요.",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "시스템 추론 모델",
|
||||
"newKnowledge.settings.topKLabel": "상위 K",
|
||||
"newKnowledge.settings.topKMinimum": "최소 = 1",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "Сохранить изменения",
|
||||
"newKnowledge.settings.saveFailed": "Не удалось сохранить изменения. Проверьте подключение и повторите попытку — внесённые изменения сохранены.",
|
||||
"newKnowledge.settings.scoreRange": "диапазон 0,00–1,00",
|
||||
"newKnowledge.settings.serverConflict": "Эти настройки были изменены в другом месте. Отмените свои правки, чтобы загрузить последнюю версию перед повторным сохранением.",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "Модель системного мышления",
|
||||
"newKnowledge.settings.topKLabel": "Top K",
|
||||
"newKnowledge.settings.topKMinimum": "мин. = 1",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "บันทึกการเปลี่ยนแปลง",
|
||||
"newKnowledge.settings.saveFailed": "ไม่สามารถบันทึกการเปลี่ยนแปลงได้ โปรดตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง — การแก้ไขของคุณยังคงอยู่",
|
||||
"newKnowledge.settings.scoreRange": "ช่วง 0.00–1.00",
|
||||
"newKnowledge.settings.serverConflict": "การตั้งค่าเหล่านี้ถูกเปลี่ยนจากที่อื่น โปรดยกเลิกการแก้ไขเพื่อโหลดเวอร์ชันล่าสุดก่อนบันทึกอีกครั้ง",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "แบบจําลองการให้เหตุผลของระบบ",
|
||||
"newKnowledge.settings.topKLabel": "ท็อป K",
|
||||
"newKnowledge.settings.topKMinimum": "ต่ำสุด = 1",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "Зберегти зміни",
|
||||
"newKnowledge.settings.saveFailed": "Не вдалося зберегти зміни. Перевірте з’єднання та повторіть спробу — ваші зміни залишилися.",
|
||||
"newKnowledge.settings.scoreRange": "діапазон 0,00–1,00",
|
||||
"newKnowledge.settings.serverConflict": "Ці налаштування було змінено в іншому місці. Скасуйте свої правки, щоб завантажити останню версію перед повторним збереженням.",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "Системна модель міркування",
|
||||
"newKnowledge.settings.topKLabel": "Найкращих K",
|
||||
"newKnowledge.settings.topKMinimum": "мін. = 1",
|
||||
|
||||
@ -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,00–1,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",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "保存更改",
|
||||
"newKnowledge.settings.saveFailed": "无法保存更改。请检查网络后重试——你的编辑仍然保留。",
|
||||
"newKnowledge.settings.scoreRange": "范围 0.00–1.00",
|
||||
"newKnowledge.settings.serverConflict": "这些设置已在其他位置更改。请取消当前编辑并加载最新版本,然后再保存。",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "系统推理模型",
|
||||
"newKnowledge.settings.topKLabel": "Top K",
|
||||
"newKnowledge.settings.topKMinimum": "最小值 = 1",
|
||||
|
||||
@ -620,6 +620,7 @@
|
||||
"newKnowledge.settings.saveChanges": "儲存變更",
|
||||
"newKnowledge.settings.saveFailed": "無法儲存變更。請檢查網路後重試——你的編輯仍然保留。",
|
||||
"newKnowledge.settings.scoreRange": "範圍 0.00–1.00",
|
||||
"newKnowledge.settings.serverConflict": "這些設定已在其他位置變更。請取消目前編輯並載入最新版本,然後再儲存。",
|
||||
"newKnowledge.settings.systemReasoningModelLabel": "系統推理模型",
|
||||
"newKnowledge.settings.topKLabel": "Top K",
|
||||
"newKnowledge.settings.topKMinimum": "最小值 = 1",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user