mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
fix(web): improve document detail layout (WTA-2026)
This commit is contained in:
parent
426b5e9fee
commit
ebc7322e00
@ -16,6 +16,13 @@ import copy from 'copy-to-clipboard'
|
||||
import { renderWithNuqs as render } from '@/test/nuqs-testing'
|
||||
import { DocumentDetailPage } from '../document-detail-page'
|
||||
|
||||
const multimodalAssetGet = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/service/base', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/base')>()),
|
||||
get: multimodalAssetGet,
|
||||
}))
|
||||
|
||||
vi.mock('../components/knowledge-model-readiness-banner', () => ({
|
||||
KnowledgeModelReadinessBanner: () => null,
|
||||
}))
|
||||
@ -690,6 +697,9 @@ describe('DocumentDetailPage', () => {
|
||||
multimodalQuery.data = undefined
|
||||
multimodalQuery.error = null
|
||||
multimodalQuery.isPending = false
|
||||
multimodalAssetGet.mockImplementation(
|
||||
async () => new Response(new Blob(['image-bytes'], { type: 'image/png' })),
|
||||
)
|
||||
tasksQuery.data = { pages: [{ items: [] }] }
|
||||
tasksQuery.error = null
|
||||
tasksQuery.hasNextPage = false
|
||||
@ -887,7 +897,8 @@ describe('DocumentDetailPage', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the persisted outline tree and summaries while hiding a legacy title chunk', () => {
|
||||
it('renders collapsible outline summaries and semantic heading levels', async () => {
|
||||
const user = userEvent.setup()
|
||||
chunksQuery.data = {
|
||||
pages: [
|
||||
{
|
||||
@ -969,11 +980,29 @@ describe('DocumentDetailPage', () => {
|
||||
expect(within(tree).queryByRole('treeitem', { name: '#0' })).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Generated guide summary.')).toBeInTheDocument()
|
||||
expect(screen.getByText('Generated setup summary.')).toBeInTheDocument()
|
||||
const summaryButtons = screen.getAllByRole('button', {
|
||||
name: 'dataset.newKnowledge.documentSummary',
|
||||
})
|
||||
expect(summaryButtons[0]).toHaveAttribute('aria-expanded', 'true')
|
||||
await user.click(summaryButtons[0]!)
|
||||
expect(summaryButtons[0]).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText('Generated guide summary.')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Generated setup summary.')).toBeInTheDocument()
|
||||
expect(screen.getByText('Guide body')).toBeInTheDocument()
|
||||
expect(screen.getByText('Setup body')).toBeInTheDocument()
|
||||
const article = screen.getByRole('article')
|
||||
expect(
|
||||
within(article).getByRole('heading', { level: 2, name: 'Guide Operating safely' }),
|
||||
).toBeInTheDocument()
|
||||
expect(within(article).getByRole('heading', { level: 3, name: 'Setup' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders extracted document images next to the chunk selected by canonical offsets', () => {
|
||||
it('loads protected document images next to the chunk selected by canonical offsets', async () => {
|
||||
const createObjectUrl = vi
|
||||
.spyOn(URL, 'createObjectURL')
|
||||
.mockReturnValueOnce('blob:asset')
|
||||
.mockReturnValueOnce('blob:thumbnail')
|
||||
const revokeObjectUrl = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {})
|
||||
chunksQuery.data = {
|
||||
pages: [
|
||||
{
|
||||
@ -1012,14 +1041,77 @@ describe('DocumentDetailPage', () => {
|
||||
|
||||
render(<DocumentDetailPage documentId="document-1" knowledgeSpaceId="space-1" />)
|
||||
|
||||
const image = screen.getByRole('img', { name: 'Screenshot of the source configuration' })
|
||||
expect(image).toHaveAttribute('src', '/console/api/knowledge-fs/image-1?variant=thumbnail')
|
||||
const image = await screen.findByRole('img', {
|
||||
name: 'Screenshot of the source configuration',
|
||||
})
|
||||
expect(image).toHaveAttribute('src', 'blob:asset')
|
||||
expect(multimodalAssetGet).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/knowledge-fs/image-1',
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
{ needAllResponseContent: true, silent: true },
|
||||
)
|
||||
fireEvent.error(image)
|
||||
expect(image).toHaveAttribute('src', '/console/api/knowledge-fs/image-1')
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole('img', { name: 'Screenshot of the source configuration' }),
|
||||
).toHaveAttribute('src', 'blob:thumbnail'),
|
||||
)
|
||||
expect(multimodalAssetGet).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/knowledge-fs/image-1?variant=thumbnail',
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
{ needAllResponseContent: true, silent: true },
|
||||
)
|
||||
expect(revokeObjectUrl).toHaveBeenCalledWith('blob:asset')
|
||||
expect(createObjectUrl).toHaveBeenCalledTimes(2)
|
||||
expect(screen.getByText('Screenshot of the source configuration')).toBeInTheDocument()
|
||||
expect(screen.getByText('The image caption follows.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows images without location metadata after the document chunks', () => {
|
||||
chunksQuery.data = {
|
||||
pages: [
|
||||
{
|
||||
items: [
|
||||
chunk({
|
||||
id: 'chapter',
|
||||
ordinal: 1,
|
||||
sectionPath: ['Chapter'],
|
||||
text: 'Chapter\n\nChapter body',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
multimodalQuery.data = {
|
||||
artifact_hash: 'artifact-hash',
|
||||
created_at: '2026-08-07T10:00:00Z',
|
||||
document_asset_id: 'asset-1',
|
||||
id: 'manifest-1',
|
||||
items: [
|
||||
{
|
||||
asset_url: '/image-without-location',
|
||||
caption: 'Screenshot without location metadata',
|
||||
id: 'image-without-location',
|
||||
modality: 'image',
|
||||
section_path: [],
|
||||
},
|
||||
],
|
||||
manifest_version: 'document-multimodal-manifest-v1',
|
||||
version: 1,
|
||||
}
|
||||
|
||||
render(<DocumentDetailPage documentId="document-1" knowledgeSpaceId="space-1" />)
|
||||
|
||||
const article = screen.getByRole('article')
|
||||
const chunkHeading = within(article).getByRole('heading', { level: 2, name: 'Chapter' })
|
||||
const image = screen.getByRole('img', { name: 'Screenshot without location metadata' })
|
||||
expect(chunkHeading.compareDocumentPosition(image) & Node.DOCUMENT_POSITION_FOLLOWING).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
)
|
||||
})
|
||||
|
||||
it('labels flat document chunks by their visible order', () => {
|
||||
chunksQuery.data = {
|
||||
pages: [
|
||||
@ -1491,6 +1583,12 @@ describe('DocumentDetailPage', () => {
|
||||
text: 'Child content',
|
||||
}),
|
||||
chunk({ id: 'second', ordinal: 3, sectionPath: ['Second root'] }),
|
||||
chunk({
|
||||
id: 'second-child',
|
||||
ordinal: 4,
|
||||
parentChunkId: 'second',
|
||||
sectionPath: ['Second root', 'Hidden child'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
@ -1501,6 +1599,8 @@ describe('DocumentDetailPage', () => {
|
||||
const parent = screen.getByRole('treeitem', { name: /Parent node/ })
|
||||
const child = screen.getByRole('treeitem', { name: /Child node/ })
|
||||
const second = screen.getByRole('treeitem', { name: /Second root/ })
|
||||
expect(second).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByRole('treeitem', { name: /Hidden child/ })).not.toBeInTheDocument()
|
||||
tree.focus()
|
||||
fireEvent.keyDown(tree, { key: 'ArrowRight' })
|
||||
expect(tree).toHaveAttribute('aria-activedescendant', child.id)
|
||||
|
||||
@ -8,9 +8,10 @@ import type {
|
||||
LogicalDocumentRevision,
|
||||
} from './document-models'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import {
|
||||
@ -61,6 +62,54 @@ function ChunkMarker({ label }: { label: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function DocumentSectionHeading({ children, level }: { children: React.ReactNode; level: number }) {
|
||||
const headingLevel = Math.min(6, Math.max(2, Math.trunc(level) + 1))
|
||||
const Heading = `h${headingLevel}` as 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
||||
return (
|
||||
<Heading
|
||||
className={cn(
|
||||
'wrap-break-word text-text-primary',
|
||||
headingLevel === 2 && 'system-xl-semibold',
|
||||
headingLevel === 3 && 'system-sm-semibold',
|
||||
headingLevel >= 4 && 'system-sm-semibold',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Heading>
|
||||
)
|
||||
}
|
||||
|
||||
function DocumentSectionSummary({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="mt-3 overflow-hidden rounded-lg bg-background-section">
|
||||
<button
|
||||
aria-expanded={expanded}
|
||||
className="flex w-full items-center gap-1.5 px-3.5 pt-3 text-left system-xs-regular text-text-secondary outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset"
|
||||
type="button"
|
||||
onClick={() => setExpanded((current) => !current)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-file-list-3-line size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">{t(($) => $['newKnowledge.documentSummary'])}</span>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'i-ri-arrow-down-s-line size-4 shrink-0 transition-transform motion-reduce:transition-none',
|
||||
!expanded && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{expanded && (
|
||||
<p className="px-3.5 pt-1 pb-3 system-xs-regular wrap-break-word text-text-secondary">
|
||||
{children}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DocumentChunkDetail({
|
||||
canEdit,
|
||||
controlSpaceId,
|
||||
@ -162,16 +211,6 @@ export function DocumentChunkDetail({
|
||||
className="flex max-h-[70vh] flex-col gap-3 overflow-auto px-2 pt-1 xl:h-full xl:max-h-none xl:px-0"
|
||||
data-testid="chunk-content-scroll"
|
||||
>
|
||||
{multimodalPlacement.unplaced.length > 0 && (
|
||||
<section className="space-y-3 rounded-lg px-3 pt-2 first:pt-3 xl:px-0">
|
||||
<h3 className="system-sm-semibold text-text-primary">
|
||||
{t(($) => $['newKnowledge.documentImages'])}
|
||||
</h3>
|
||||
{multimodalPlacement.unplaced.map((item) => (
|
||||
<DocumentMultimodalAsset item={item} key={item.id} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
{chunks.map((chunk) => {
|
||||
const content = chunkContentParts(chunk)
|
||||
const markerLabel = chunkMarkerLabels.get(chunk.id)
|
||||
@ -179,6 +218,8 @@ export function DocumentChunkDetail({
|
||||
const outlineSummary = outlineSummaryChunkIds.has(chunk.id)
|
||||
? outlineNode?.summary?.trim()
|
||||
: undefined
|
||||
const sectionLevel =
|
||||
outlineNode?.level ?? (chunk.sectionPath.length > 0 ? chunk.sectionPath.length : 2)
|
||||
const chunkMultimodalItems = multimodalPlacement.byChunkId.get(chunk.id) ?? []
|
||||
return (
|
||||
<section
|
||||
@ -190,18 +231,16 @@ export function DocumentChunkDetail({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start gap-1">
|
||||
{!content.body && markerLabel && <ChunkMarker label={markerLabel} />}
|
||||
<h3 className="system-sm-semibold wrap-break-word text-text-primary">
|
||||
<DocumentSectionHeading level={sectionLevel}>
|
||||
{outlineNode?.title.trim() ||
|
||||
content.heading ||
|
||||
t(($) => $['newKnowledge.chunkHeading'], {
|
||||
position: chunk.ordinal + 1,
|
||||
})}
|
||||
</h3>
|
||||
</DocumentSectionHeading>
|
||||
</div>
|
||||
{outlineSummary && (
|
||||
<p className="mt-1 text-[13px] leading-5.5 wrap-break-word text-text-tertiary">
|
||||
{outlineSummary}
|
||||
</p>
|
||||
<DocumentSectionSummary>{outlineSummary}</DocumentSectionSummary>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
@ -241,6 +280,16 @@ export function DocumentChunkDetail({
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
{multimodalPlacement.unplaced.length > 0 && (
|
||||
<section className="space-y-3 rounded-lg px-3 pt-2 first:pt-3 xl:px-0">
|
||||
<h3 className="system-sm-semibold text-text-primary">
|
||||
{t(($) => $['newKnowledge.documentImages'])}
|
||||
</h3>
|
||||
{multimodalPlacement.unplaced.map((item) => (
|
||||
<DocumentMultimodalAsset item={item} key={item.id} />
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-72 items-center justify-center px-6 text-center body-sm-regular text-text-tertiary">
|
||||
|
||||
@ -76,15 +76,31 @@ export function DocumentChunkTreePanel({
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [collapsedNodeIds, setCollapsedNodeIds] = useState<Set<string>>(() => new Set())
|
||||
const [expansionOverrides, setExpansionOverrides] = useState<{
|
||||
collapsed: Set<string>
|
||||
expanded: Set<string>
|
||||
}>(() => ({ collapsed: new Set(), expanded: new Set() }))
|
||||
const [focusedNodeId, setFocusedNodeId] = useState<string>()
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string>()
|
||||
const [treeHasFocus, setTreeHasFocus] = useState(false)
|
||||
const treeScrollRef = useRef<HTMLDivElement>(null)
|
||||
const expandedNodeIds = useMemo(
|
||||
() => new Set([...tree.byId.keys()].filter((id) => !collapsedNodeIds.has(id))),
|
||||
[collapsedNodeIds, tree.byId],
|
||||
)
|
||||
const selectedBranchNodeIds = useMemo(() => {
|
||||
const selectedNode =
|
||||
(selectedNodeId ? tree.byId.get(selectedNodeId) : undefined) ??
|
||||
[...tree.byId.values()].find((node) => node.targetChunkId === selectedChunkId)
|
||||
const branch = new Set<string>()
|
||||
let node = selectedNode
|
||||
while (node) {
|
||||
branch.add(node.id)
|
||||
node = node.parentId ? tree.byId.get(node.parentId) : undefined
|
||||
}
|
||||
return branch
|
||||
}, [selectedChunkId, selectedNodeId, tree.byId])
|
||||
const expandedNodeIds = useMemo(() => {
|
||||
const expanded = new Set([...selectedBranchNodeIds, ...expansionOverrides.expanded])
|
||||
for (const nodeId of expansionOverrides.collapsed) expanded.delete(nodeId)
|
||||
return expanded
|
||||
}, [expansionOverrides, selectedBranchNodeIds])
|
||||
const visibleNodes = useMemo(
|
||||
() => visibleDocumentChunkNodes(tree.roots, expandedNodeIds),
|
||||
[expandedNodeIds, tree.roots],
|
||||
@ -111,10 +127,19 @@ export function DocumentChunkTreePanel({
|
||||
const virtualRows = rowVirtualizer.getVirtualItems()
|
||||
|
||||
const toggleExpanded = (nodeId: string) => {
|
||||
setCollapsedNodeIds((current) => {
|
||||
const next = new Set(current)
|
||||
if (next.has(nodeId)) next.delete(nodeId)
|
||||
else next.add(nodeId)
|
||||
const expanded = expandedNodeIds.has(nodeId)
|
||||
setExpansionOverrides((current) => {
|
||||
const next = {
|
||||
collapsed: new Set(current.collapsed),
|
||||
expanded: new Set(current.expanded),
|
||||
}
|
||||
if (expanded) {
|
||||
next.expanded.delete(nodeId)
|
||||
next.collapsed.add(nodeId)
|
||||
} else {
|
||||
next.collapsed.delete(nodeId)
|
||||
next.expanded.add(nodeId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
@ -145,10 +170,10 @@ export function DocumentChunkTreePanel({
|
||||
else if (event.key === 'Home') nextId = visibleNodes[0]?.node.id
|
||||
else if (event.key === 'End') nextId = visibleNodes.at(-1)?.node.id
|
||||
else if (event.key === 'ArrowRight' && current.node.children.length) {
|
||||
if (collapsedNodeIds.has(nodeId)) toggleExpanded(nodeId)
|
||||
if (!expandedNodeIds.has(nodeId)) toggleExpanded(nodeId)
|
||||
else nextId = current.node.children[0]?.id
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
if (current.node.children.length && !collapsedNodeIds.has(nodeId)) toggleExpanded(nodeId)
|
||||
if (current.node.children.length && expandedNodeIds.has(nodeId)) toggleExpanded(nodeId)
|
||||
else if (parentId && tree.byId.has(parentId)) nextId = parentId
|
||||
} else if (event.key === 'Enter' || event.key === ' ') selectNode(current.node)
|
||||
else return
|
||||
@ -159,7 +184,7 @@ export function DocumentChunkTreePanel({
|
||||
const renderTreeItem = (item: (typeof visibleNodes)[number], style?: React.CSSProperties) => {
|
||||
const { depth, node, positionInSet, setSize } = item
|
||||
const hasChildren = node.children.length > 0
|
||||
const expanded = !collapsedNodeIds.has(node.id)
|
||||
const expanded = expandedNodeIds.has(node.id)
|
||||
const label = chunkTreeLabel(node.label)
|
||||
return (
|
||||
<button
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import type { KnowledgeFsDocumentMultimodalItemResponse } from '@dify/contracts/api/console/knowledge-fs/types.gen'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
import { get } from '@/service/base'
|
||||
|
||||
const CONSOLE_API_PATH = '/console/api'
|
||||
|
||||
function consoleApiAssetPath(source: string) {
|
||||
const url = new URL(source, 'http://dify.invalid')
|
||||
if (!url.pathname.startsWith(`${CONSOLE_API_PATH}/`)) return undefined
|
||||
return `${url.pathname.slice(CONSOLE_API_PATH.length)}${url.search}`
|
||||
}
|
||||
|
||||
export function DocumentMultimodalAsset({
|
||||
item,
|
||||
@ -11,16 +21,51 @@ export function DocumentMultimodalAsset({
|
||||
}) {
|
||||
const { t } = useTranslation('dataset')
|
||||
const [failedSources, setFailedSources] = useState<Set<string>>(() => new Set())
|
||||
const source = [item.thumbnail_url, item.asset_url].find(
|
||||
const rawSource = [item.asset_url, item.thumbnail_url].find(
|
||||
(candidate) => candidate && !failedSources.has(candidate),
|
||||
)
|
||||
const apiAssetPath = rawSource ? consoleApiAssetPath(rawSource) : undefined
|
||||
const [loadedAsset, setLoadedAsset] = useState<{ objectUrl: string; rawSource: string }>()
|
||||
const source = apiAssetPath
|
||||
? loadedAsset && loadedAsset.rawSource === rawSource
|
||||
? loadedAsset.objectUrl
|
||||
: undefined
|
||||
: rawSource
|
||||
const rawLabel =
|
||||
item.caption?.trim() || item.title?.trim() || item.text_preview?.trim() || item.ocr_text?.trim()
|
||||
const label = rawLabel && rawLabel.length > 500 ? `${rawLabel.slice(0, 497)}...` : rawLabel
|
||||
|
||||
useEffect(() => {
|
||||
if (!apiAssetPath || !rawSource) return
|
||||
|
||||
const abortController = new AbortController()
|
||||
let objectUrl: string | undefined
|
||||
|
||||
void get<Response>(
|
||||
apiAssetPath,
|
||||
{ signal: abortController.signal },
|
||||
{ needAllResponseContent: true, silent: true },
|
||||
)
|
||||
.then(async (response) => {
|
||||
const blob = await response.blob()
|
||||
if (abortController.signal.aborted) return
|
||||
objectUrl = URL.createObjectURL(blob)
|
||||
setLoadedAsset({ objectUrl, rawSource })
|
||||
})
|
||||
.catch(() => {
|
||||
if (abortController.signal.aborted) return
|
||||
setFailedSources((current) => new Set(current).add(rawSource))
|
||||
})
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
}, [apiAssetPath, rawSource])
|
||||
|
||||
const handleError = () => {
|
||||
if (!source) return
|
||||
setFailedSources((current) => new Set(current).add(source))
|
||||
if (!rawSource) return
|
||||
setFailedSources((current) => new Set(current).add(rawSource))
|
||||
}
|
||||
|
||||
return (
|
||||
@ -37,6 +82,13 @@ export function DocumentMultimodalAsset({
|
||||
onError={handleError}
|
||||
src={source}
|
||||
/>
|
||||
) : rawSource ? (
|
||||
<div
|
||||
aria-busy="true"
|
||||
className="flex min-h-32 items-center justify-center bg-background-default text-text-tertiary"
|
||||
>
|
||||
<span aria-hidden className="i-ri-loader-4-line size-5 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-32 flex-col items-center justify-center gap-2 px-4 py-6 text-center text-text-tertiary">
|
||||
<span aria-hidden className="i-ri-image-line size-6" />
|
||||
|
||||
@ -188,7 +188,7 @@ function LoadedDocumentRevisionContent({
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="mt-7 grid min-h-0 flex-1 gap-4 xl:grid-cols-[14rem_minmax(0,1fr)_20rem] xl:gap-0">
|
||||
<div className="mt-4 grid min-h-0 flex-1 gap-4 xl:grid-cols-[14rem_minmax(0,1fr)_20rem] xl:gap-0">
|
||||
<DocumentChunkTreePanel
|
||||
chunkCount={chunks.length}
|
||||
error={Boolean(chunksQuery.error)}
|
||||
|
||||
@ -230,11 +230,11 @@
|
||||
"newKnowledge.documentContents": "Contents",
|
||||
"newKnowledge.documentFacts": "Document details",
|
||||
"newKnowledge.documentFilterLabel": "Filter documents by status",
|
||||
"newKnowledge.documentLoadErrorDescription": "We couldn't load this document. Try again.",
|
||||
"newKnowledge.documentLoadErrorTitle": "Document couldn't load",
|
||||
"newKnowledge.documentImageAlt": "Extracted document image",
|
||||
"newKnowledge.documentImageUnavailable": "This image could not be displayed.",
|
||||
"newKnowledge.documentImages": "Document images",
|
||||
"newKnowledge.documentLoadErrorDescription": "We couldn't load this document. Try again.",
|
||||
"newKnowledge.documentLoadErrorTitle": "Document couldn't load",
|
||||
"newKnowledge.documentNotFoundDescription": "This document may have been removed, or you may not have access.",
|
||||
"newKnowledge.documentNotFoundTitle": "Document not found",
|
||||
"newKnowledge.documentOverviewDescription": "Labeling metadata for documents allows AI to access them in a timely manner and exposes the source of references for users.",
|
||||
@ -252,6 +252,7 @@
|
||||
"newKnowledge.documentStatus.processing": "Processing",
|
||||
"newKnowledge.documentStatus.queued": "Queued",
|
||||
"newKnowledge.documentStatus.ready": "Ready",
|
||||
"newKnowledge.documentSummary": "Summary",
|
||||
"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",
|
||||
|
||||
@ -230,11 +230,11 @@
|
||||
"newKnowledge.documentContents": "目录",
|
||||
"newKnowledge.documentFacts": "文档详情",
|
||||
"newKnowledge.documentFilterLabel": "按状态筛选文档",
|
||||
"newKnowledge.documentLoadErrorDescription": "无法加载此文档,请重试。",
|
||||
"newKnowledge.documentLoadErrorTitle": "文档加载失败",
|
||||
"newKnowledge.documentImageAlt": "文档中的图片",
|
||||
"newKnowledge.documentImageUnavailable": "这张图片暂时无法显示。",
|
||||
"newKnowledge.documentImages": "文档图片",
|
||||
"newKnowledge.documentLoadErrorDescription": "无法加载此文档,请重试。",
|
||||
"newKnowledge.documentLoadErrorTitle": "文档加载失败",
|
||||
"newKnowledge.documentNotFoundDescription": "此文档可能已被删除,或你没有访问权限。",
|
||||
"newKnowledge.documentNotFoundTitle": "未找到文档",
|
||||
"newKnowledge.documentOverviewDescription": "为文档添加元数据,可让 AI 及时获取文档内容,并向用户展示引用来源。",
|
||||
@ -252,6 +252,7 @@
|
||||
"newKnowledge.documentStatus.processing": "处理中",
|
||||
"newKnowledge.documentStatus.queued": "排队中",
|
||||
"newKnowledge.documentStatus.ready": "就绪",
|
||||
"newKnowledge.documentSummary": "摘要",
|
||||
"newKnowledge.documentTaskLookupIncomplete": "还有更多任务记录,请继续检查以确认此文档的状态。",
|
||||
"newKnowledge.documentUploadExclusion.batchLimit": "超出批次大小限制",
|
||||
"newKnowledge.documentUploadExclusion.countLimit": "超出文件数量限制",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user