mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 17:32:13 +08:00
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import type {
|
|
DocumentRevisionChunk,
|
|
LogicalDocument,
|
|
LogicalDocumentRevision,
|
|
} from '@dify/contracts/knowledge-fs/types.gen'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { toast } from '@langgenius/dify-ui/toast'
|
|
import copy from 'copy-to-clipboard'
|
|
import { useEffect, useMemo } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import {
|
|
chunkCharacterCount,
|
|
chunkContentParts,
|
|
chunkMetadataEntries,
|
|
} from './document-detail-model'
|
|
|
|
function formatBytes(bytes: number, locale: string) {
|
|
const numberFormat = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 })
|
|
if (bytes < 1024) return `${numberFormat.format(bytes)} B`
|
|
const units = ['KB', 'MB', 'GB']
|
|
let value = bytes / 1024
|
|
let unit = units[0]
|
|
for (let index = 1; index < units.length && value >= 1024; index++) {
|
|
value /= 1024
|
|
unit = units[index]
|
|
}
|
|
return `${new Intl.NumberFormat(locale, {
|
|
maximumFractionDigits: value >= 10 ? 0 : 1,
|
|
}).format(value)} ${unit}`
|
|
}
|
|
|
|
function formatDate(value: string | undefined, locale: string) {
|
|
if (!value) return '—'
|
|
const date = new Date(value)
|
|
if (Number.isNaN(date.getTime())) return value
|
|
return new Intl.DateTimeFormat(locale, {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
}).format(date)
|
|
}
|
|
|
|
export function DocumentChunkDetail({
|
|
document,
|
|
chunks,
|
|
chunksComplete,
|
|
locale,
|
|
revision,
|
|
selectedChunkId,
|
|
}: {
|
|
document: LogicalDocument
|
|
chunks: DocumentRevisionChunk[]
|
|
chunksComplete: boolean
|
|
locale: string
|
|
revision?: Exclude<LogicalDocumentRevision, null>
|
|
selectedChunkId?: string
|
|
}) {
|
|
const { t } = useTranslation('dataset')
|
|
const { t: tCommon } = useTranslation('common')
|
|
const selectedChunk = useMemo(
|
|
() => chunks.find((chunk) => chunk.id === selectedChunkId) ?? chunks[0],
|
|
[chunks, selectedChunkId],
|
|
)
|
|
const characterCount = useMemo(
|
|
() => chunks.reduce((total, chunk) => total + chunkCharacterCount(chunk.text), 0),
|
|
[chunks],
|
|
)
|
|
const averageChunkLength = chunks.length ? Math.round(characterCount / chunks.length) : 0
|
|
const sizeBytes = revision?.sizeBytes ?? document.active?.sizeBytes
|
|
const sourceName =
|
|
typeof document.userMetadata.sourceName === 'string'
|
|
? document.userMetadata.sourceName
|
|
: undefined
|
|
const retrievalCount =
|
|
typeof document.userMetadata.retrievalCount === 'number'
|
|
? document.userMetadata.retrievalCount
|
|
: undefined
|
|
|
|
useEffect(() => {
|
|
if (!selectedChunkId) return
|
|
globalThis.document
|
|
?.getElementById(`document-chunk-${selectedChunkId}`)
|
|
?.scrollIntoView({ block: 'nearest' })
|
|
}, [selectedChunkId])
|
|
|
|
return (
|
|
<>
|
|
<article
|
|
aria-busy={!chunksComplete}
|
|
className="min-h-72 min-w-0 overflow-hidden bg-background-default xl:px-7"
|
|
>
|
|
{!chunksComplete && (
|
|
<p
|
|
className="border-b border-divider-subtle bg-state-accent-hover px-5 py-2 system-xs-regular text-text-accent"
|
|
role="status"
|
|
>
|
|
{t(($) => $['newKnowledge.documentContentIncomplete'])}
|
|
</p>
|
|
)}
|
|
{chunks.length ? (
|
|
<div
|
|
className="max-h-[70vh] space-y-6 overflow-auto px-2 py-1 xl:px-0"
|
|
data-testid="chunk-content-scroll"
|
|
>
|
|
{chunks.map((chunk) => {
|
|
const content = chunkContentParts(chunk.text)
|
|
return (
|
|
<section
|
|
key={chunk.id}
|
|
id={`document-chunk-${chunk.id}`}
|
|
className="group scroll-mt-4 rounded-lg px-3 py-4 [contain-intrinsic-size:auto_160px] [content-visibility:auto] xl:px-0"
|
|
>
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="min-w-0">
|
|
<h3 className="system-sm-semibold wrap-break-word text-text-primary">
|
|
{content.heading ||
|
|
t(($) => $['newKnowledge.chunkHeading'], { position: chunk.ordinal })}
|
|
</h3>
|
|
<p className="mt-1 system-2xs-regular text-text-tertiary">
|
|
{t(($) => $['newKnowledge.chunkLocation'], { position: chunk.ordinal })}
|
|
</p>
|
|
</div>
|
|
<Button
|
|
aria-label={tCommon(($) => $['operation.copy'])}
|
|
className="opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 motion-reduce:transition-none"
|
|
onClick={() => {
|
|
copy(chunk.text)
|
|
toast.success(tCommon(($) => $['actionMsg.copySuccessfully']))
|
|
}}
|
|
size="small"
|
|
variant="ghost"
|
|
>
|
|
<span aria-hidden className="i-ri-file-copy-line size-4" />
|
|
</Button>
|
|
</div>
|
|
{content.body && (
|
|
<p className="mt-3 body-md-regular wrap-break-word whitespace-pre-wrap text-text-primary">
|
|
{content.body}
|
|
</p>
|
|
)}
|
|
{!chunk.text && (
|
|
<p className="mt-3 body-md-regular text-text-tertiary">
|
|
{t(($) => $['newKnowledge.emptyChunk'])}
|
|
</p>
|
|
)}
|
|
</section>
|
|
)
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div className="flex min-h-72 items-center justify-center px-6 text-center body-sm-regular text-text-tertiary">
|
|
{t(($) => $['newKnowledge.selectChunk'])}
|
|
</div>
|
|
)}
|
|
</article>
|
|
|
|
<aside className="min-w-0 xl:pl-6">
|
|
<section className="rounded-xl bg-background-default-subtle p-4">
|
|
<h2 className="system-sm-semibold text-text-primary">
|
|
{t(($) => $['newKnowledge.metadata'])}
|
|
</h2>
|
|
<p className="mt-2 system-xs-regular text-text-tertiary">
|
|
{t(($) => $['newKnowledge.documentOverviewDescription'])}
|
|
</p>
|
|
<Button
|
|
className="mt-2"
|
|
onClick={() => toast.info(t(($) => $['newKnowledge.filtersUnavailable']))}
|
|
variant="primary"
|
|
>
|
|
{t(($) => $['metadata.documentMetadata.startLabeling'])}
|
|
</Button>
|
|
</section>
|
|
<section className="mt-6">
|
|
<dl className="space-y-3">
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.sourceColumn'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular wrap-break-word text-text-secondary">
|
|
{sourceName ??
|
|
(document.sourceId
|
|
? t(($) => $['newKnowledge.sourceType.connector'])
|
|
: t(($) => $['newKnowledge.manualUpload']))}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.fileSize'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{sizeBytes !== undefined ? formatBytes(sizeBytes, locale) : '—'}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.createdAt'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{formatDate(document.createdAt, locale)}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.lastIndexed'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{formatDate(revision?.activatedAt ?? revision?.createdAt, locale)}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.documentRevision'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{revision?.revision ?? document.activeRevision ?? '—'}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</section>
|
|
<section className="mt-7">
|
|
<h2 className="system-sm-semibold text-text-primary">
|
|
{t(($) => $['newKnowledge.indexInformation'])}
|
|
</h2>
|
|
<dl className="mt-3 space-y-3">
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.chunkCount'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{chunksComplete ? new Intl.NumberFormat(locale).format(chunks.length) : '—'}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.averageChunkLength'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{chunksComplete ? new Intl.NumberFormat(locale).format(averageChunkLength) : '—'}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.retrievalCount'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{retrievalCount === undefined
|
|
? '—'
|
|
: new Intl.NumberFormat(locale).format(retrievalCount)}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.mimeType'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular wrap-break-word text-text-secondary">
|
|
{revision?.mimeType ?? document.active?.mimeType ?? '—'}
|
|
</dd>
|
|
</div>
|
|
{selectedChunk && (
|
|
<>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.tokenCount'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{new Intl.NumberFormat(locale).format(selectedChunk.tokenCount)}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="system-2xs-medium text-text-tertiary">
|
|
{t(($) => $['newKnowledge.characterCount'])}
|
|
</dt>
|
|
<dd className="mt-1 system-xs-regular text-text-secondary">
|
|
{new Intl.NumberFormat(locale).format(chunkCharacterCount(selectedChunk.text))}
|
|
</dd>
|
|
</div>
|
|
{chunkMetadataEntries(selectedChunk.userMetadata).map(([key, value]) => (
|
|
<div key={key}>
|
|
<dt className="system-2xs-medium wrap-break-word text-text-tertiary">{key}</dt>
|
|
<dd className="mt-1 system-xs-regular wrap-break-word text-text-secondary">
|
|
{value}
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</>
|
|
)}
|
|
</dl>
|
|
</section>
|
|
</aside>
|
|
</>
|
|
)
|
|
}
|