| import React, { useMemo } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import type { QAChunk } from './types' | |||||
| import { QAItemType } from './types' | |||||
| import { PreviewSlice } from '@/app/components/datasets/formatted-text/flavours/preview-slice' | |||||
| import SegmentIndexTag from '@/app/components/datasets/documents/detail/completed/common/segment-index-tag' | |||||
| import Dot from '@/app/components/datasets/documents/detail/completed/common/dot' | |||||
| import { formatNumber } from '@/utils/format' | |||||
| import QAItem from './q-a-item' | |||||
| import { ChunkingMode, type ParentMode } from '@/models/datasets' | |||||
| type ChunkCardProps = { | |||||
| chunkType: ChunkingMode | |||||
| parentMode?: ParentMode | |||||
| content: string | string[] | QAChunk | |||||
| positionId?: string | number | |||||
| wordCount: number | |||||
| } | |||||
| const ChunkCard = (props: ChunkCardProps) => { | |||||
| const { chunkType, parentMode, content, positionId, wordCount } = props | |||||
| const { t } = useTranslation() | |||||
| const isFullDoc = useMemo(() => { | |||||
| return chunkType === ChunkingMode.parentChild && parentMode === 'full-doc' | |||||
| }, [chunkType, parentMode]) | |||||
| const isParagraph = useMemo(() => { | |||||
| return chunkType === ChunkingMode.parentChild && parentMode === 'paragraph' | |||||
| }, [chunkType, parentMode]) | |||||
| const contentElement = useMemo(() => { | |||||
| if (chunkType === ChunkingMode.parentChild) { | |||||
| return (content as string[]).map((child, index) => { | |||||
| const indexForLabel = index + 1 | |||||
| return ( | |||||
| <PreviewSlice | |||||
| key={child} | |||||
| label={`C-${indexForLabel}`} | |||||
| text={child} | |||||
| tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`} | |||||
| labelInnerClassName='text-[10px] font-semibold align-bottom leading-7' | |||||
| dividerClassName='leading-7' | |||||
| /> | |||||
| ) | |||||
| }) | |||||
| } | |||||
| if (chunkType === ChunkingMode.qa) { | |||||
| return ( | |||||
| <div className='flex flex-col gap-2'> | |||||
| <QAItem type={QAItemType.Question} text={(content as QAChunk).question} /> | |||||
| <QAItem type={QAItemType.Answer} text={(content as QAChunk).answer} /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return content as string | |||||
| }, [content, chunkType]) | |||||
| return ( | |||||
| <div className='flex flex-col gap-1 rounded-lg bg-components-panel-bg px-3 py-2.5'> | |||||
| {!isFullDoc && ( | |||||
| <div className='inline-flex items-center justify-start gap-2'> | |||||
| <SegmentIndexTag | |||||
| positionId={positionId} | |||||
| labelPrefix={isParagraph ? 'Parent-Chunk' : 'Chunk'} | |||||
| /> | |||||
| <Dot /> | |||||
| <div className='system-xs-medium text-text-tertiary'>{`${formatNumber(wordCount)} ${t('datasetDocuments.segment.characters', { count: wordCount })}`}</div> | |||||
| </div> | |||||
| )} | |||||
| <div className='body-md-regular text-text-secondary'>{contentElement}</div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default React.memo(ChunkCard) |
| import { useMemo } from 'react' | import { useMemo } from 'react' | ||||
| import SegmentIndexTag from '@/app/components/datasets/documents/detail/completed/common/segment-index-tag' | |||||
| import Dot from '@/app/components/datasets/documents/detail/completed/common/dot' | |||||
| import { PreviewSlice } from '@/app/components/datasets/formatted-text/flavours/preview-slice' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { formatNumber } from '@/utils/format' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| enum QAItemType { | |||||
| Question = 'question', | |||||
| Answer = 'answer', | |||||
| } | |||||
| type QAItemProps = { | |||||
| type: QAItemType | |||||
| text: string | |||||
| } | |||||
| const QAItem = (props: QAItemProps) => { | |||||
| const { type, text } = props | |||||
| return <div className='inline-flex items-start justify-start gap-1 self-stretch'> | |||||
| <div className='w-4 text-[13px] font-medium leading-5 text-text-tertiary'>{type === QAItemType.Question ? 'Q' : 'A'}</div> | |||||
| <div className='body-md-regular flex-1 text-text-secondary'>{text}</div> | |||||
| </div> | |||||
| } | |||||
| export enum ChunkType { | |||||
| General = 'general', | |||||
| Paragraph = 'paragraph', | |||||
| FullDoc = 'full-doc', | |||||
| QA = 'qa', | |||||
| } | |||||
| type ChunkCardProps = { | |||||
| type: ChunkType | |||||
| content: string | string[] | QAChunk | |||||
| positionId?: string | number | |||||
| wordCount: number | |||||
| } | |||||
| const ChunkCard = (props: ChunkCardProps) => { | |||||
| const { type, content, positionId, wordCount } = props | |||||
| const { t } = useTranslation() | |||||
| const renderContent = () => { | |||||
| // ChunkType.Paragraph && ChunkType.FullDoc | |||||
| if (Array.isArray(content)) { | |||||
| return content.map((child, index) => { | |||||
| const indexForLabel = index + 1 | |||||
| return ( | |||||
| <PreviewSlice | |||||
| key={child} | |||||
| label={`C-${indexForLabel}`} | |||||
| text={child} | |||||
| tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`} | |||||
| labelInnerClassName='text-[10px] font-semibold align-bottom leading-7' | |||||
| dividerClassName='leading-7' | |||||
| /> | |||||
| ) | |||||
| }) | |||||
| } | |||||
| // ChunkType.QA | |||||
| if (typeof content === 'object') { | |||||
| return <div className='flex flex-col gap-2'> | |||||
| <QAItem type={QAItemType.Question} text={(content as QAChunk).question} /> | |||||
| <QAItem type={QAItemType.Answer} text={(content as QAChunk).answer} /> | |||||
| </div> | |||||
| } | |||||
| // ChunkType.General | |||||
| return content | |||||
| } | |||||
| return ( | |||||
| <div className='flex flex-col gap-1 rounded-lg bg-components-panel-bg px-3 py-2.5'> | |||||
| {type !== ChunkType.FullDoc && <div className='inline-flex items-center justify-start gap-2'> | |||||
| <SegmentIndexTag | |||||
| positionId={positionId} | |||||
| labelPrefix={type === ChunkType.Paragraph ? 'Parent-Chunk' : 'Chunk'} | |||||
| /> | |||||
| <Dot /> | |||||
| <div className='system-xs-medium text-text-tertiary'>{formatNumber(wordCount)} {t('datasetDocuments.segment.characters', { count: wordCount })}</div> | |||||
| </div>} | |||||
| <div className='body-md-regular text-text-secondary'>{renderContent()}</div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export type ChunkInfo = { | |||||
| general_chunks?: string[] | |||||
| parent_child_chunks?: ParentChildChunk[] | |||||
| parent_mode?: string | |||||
| qa_chunks?: QAChunk[] | |||||
| } | |||||
| type ParentChildChunk = { | |||||
| child_contents: string[] | |||||
| parent_content: string | |||||
| parent_mode: string | |||||
| } | |||||
| type QAChunk = { | |||||
| question: string | |||||
| answer: string | |||||
| } | |||||
| import type { ChunkInfo, GeneralChunks, ParentChildChunk, ParentChildChunks, QAChunk, QAChunks } from './types' | |||||
| import { ChunkingMode, type ParentMode } from '@/models/datasets' | |||||
| import ChunkCard from './chunk-card' | |||||
| type ChunkCardListProps = { | type ChunkCardListProps = { | ||||
| chunkType: ChunkingMode | |||||
| parentMode?: ParentMode | |||||
| chunkInfo: ChunkInfo | chunkInfo: ChunkInfo | ||||
| className?: string | className?: string | ||||
| } | } | ||||
| export const ChunkCardList = (props: ChunkCardListProps) => { | export const ChunkCardList = (props: ChunkCardListProps) => { | ||||
| const { chunkInfo, className } = props | |||||
| const chunkType = useMemo(() => { | |||||
| if (chunkInfo?.general_chunks) | |||||
| return ChunkType.General | |||||
| if (chunkInfo?.parent_child_chunks) | |||||
| return chunkInfo.parent_mode as ChunkType | |||||
| return ChunkType.QA | |||||
| }, [chunkInfo]) | |||||
| const { chunkType, parentMode, chunkInfo, className } = props | |||||
| const chunkList = useMemo(() => { | const chunkList = useMemo(() => { | ||||
| if (chunkInfo?.general_chunks) | |||||
| return chunkInfo.general_chunks | |||||
| if (chunkInfo?.parent_child_chunks) | |||||
| return chunkInfo.parent_child_chunks | |||||
| return chunkInfo?.qa_chunks ?? [] | |||||
| if (chunkType === ChunkingMode.text) | |||||
| return chunkInfo as GeneralChunks | |||||
| if (chunkType === ChunkingMode.parentChild) | |||||
| return (chunkInfo as ParentChildChunks).parent_child_chunks | |||||
| return (chunkInfo as QAChunks).qa_chunks | |||||
| }, [chunkInfo]) | }, [chunkInfo]) | ||||
| const getWordCount = (seg: string | ParentChildChunk | QAChunk) => { | |||||
| if (chunkType === ChunkingMode.parentChild) | |||||
| return (seg as ParentChildChunk).parent_content.length | |||||
| if (chunkType === ChunkingMode.text) | |||||
| return (seg as string).length | |||||
| return (seg as QAChunk).question.length + (seg as QAChunk).answer.length | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={cn('flex w-full flex-col gap-y-1', className)}> | <div className={cn('flex w-full flex-col gap-y-1', className)}> | ||||
| {chunkList.map((seg: string | ParentChildChunk | QAChunk, index: number) => { | |||||
| const isParentChildMode = [ChunkType.Paragraph, ChunkType.FullDoc].includes(chunkType!) | |||||
| let wordCount = 0 | |||||
| if (isParentChildMode) | |||||
| wordCount = (seg as ParentChildChunk)?.parent_content?.length | |||||
| else if (typeof seg === 'string') | |||||
| wordCount = seg.length | |||||
| else | |||||
| wordCount = (seg as QAChunk)?.question?.length + (seg as QAChunk)?.answer?.length | |||||
| {chunkList.map((seg, index: number) => { | |||||
| const wordCount = getWordCount(seg) | |||||
| return ( | return ( | ||||
| <ChunkCard | <ChunkCard | ||||
| key={`${chunkType}-${index}`} | key={`${chunkType}-${index}`} | ||||
| type={chunkType} | |||||
| content={isParentChildMode ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)} | |||||
| chunkType={chunkType} | |||||
| parentMode={parentMode} | |||||
| content={chunkType === ChunkingMode.parentChild ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)} | |||||
| wordCount={wordCount} | wordCount={wordCount} | ||||
| positionId={index + 1} | positionId={index + 1} | ||||
| /> | /> |
| import React from 'react' | |||||
| import { QAItemType } from './types' | |||||
| type QAItemProps = { | |||||
| type: QAItemType | |||||
| text: string | |||||
| } | |||||
| const QAItem = (props: QAItemProps) => { | |||||
| const { type, text } = props | |||||
| return ( | |||||
| <div className='inline-flex items-start justify-start gap-1 self-stretch'> | |||||
| <div className='w-4 text-[13px] font-medium leading-5 text-text-tertiary'>{type === QAItemType.Question ? 'Q' : 'A'}</div> | |||||
| <div className='body-md-regular flex-1 text-text-secondary'>{text}</div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default React.memo(QAItem) |
| export type GeneralChunks = string[] | |||||
| export type ParentChildChunk = { | |||||
| child_contents: string[] | |||||
| parent_content: string | |||||
| parent_mode: string | |||||
| } | |||||
| export type ParentChildChunks = { | |||||
| parent_child_chunks: ParentChildChunk[] | |||||
| parent_mode: string | |||||
| } | |||||
| export type QAChunk = { | |||||
| question: string | |||||
| answer: string | |||||
| } | |||||
| export type QAChunks = { | |||||
| qa_chunks: QAChunk[] | |||||
| } | |||||
| export type ChunkInfo = GeneralChunks | ParentChildChunks | QAChunks | |||||
| export enum QAItemType { | |||||
| Question = 'question', | |||||
| Answer = 'answer', | |||||
| } |
| isRunning={!workflowRunningData?.result || workflowRunningData?.result.status === WorkflowRunningStatus.Running} | isRunning={!workflowRunningData?.result || workflowRunningData?.result.status === WorkflowRunningStatus.Running} | ||||
| outputs={workflowRunningData?.result?.outputs} | outputs={workflowRunningData?.result?.outputs} | ||||
| error={workflowRunningData?.result?.error} | error={workflowRunningData?.result?.error} | ||||
| tracing={workflowRunningData?.tracing} | |||||
| onSwitchToDetail={() => switchTab('DETAIL')} | onSwitchToDetail={() => switchTab('DETAIL')} | ||||
| /> | /> | ||||
| )} | )} |
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { BlockEnum } from '@/app/components/workflow/types' | |||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| import { RiLoader2Line } from '@remixicon/react' | import { RiLoader2Line } from '@remixicon/react' | ||||
| import React, { useMemo } from 'react' | import React, { useMemo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| isRunning?: boolean | isRunning?: boolean | ||||
| outputs?: any | outputs?: any | ||||
| error?: string | error?: string | ||||
| tracing?: NodeTracing[] | |||||
| onSwitchToDetail: () => void | onSwitchToDetail: () => void | ||||
| } | } | ||||
| isRunning, | isRunning, | ||||
| outputs, | outputs, | ||||
| error, | error, | ||||
| tracing, | |||||
| onSwitchToDetail, | onSwitchToDetail, | ||||
| }: ResultTextProps) => { | }: ResultTextProps) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const chunkInfo = useMemo(() => { | |||||
| if (!outputs || !tracing) | |||||
| return undefined | |||||
| const knowledgeIndexNode = tracing.find(node => node.node_type === BlockEnum.KnowledgeBase) | |||||
| return knowledgeIndexNode?.inputs?.chunks | |||||
| }, [outputs, tracing]) | |||||
| const previewChunks = useMemo(() => { | const previewChunks = useMemo(() => { | ||||
| return formatPreviewChunks(chunkInfo, outputs) | |||||
| }, [chunkInfo, outputs]) | |||||
| return formatPreviewChunks(outputs) | |||||
| }, [outputs]) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| )} | )} | ||||
| {outputs && previewChunks && ( | {outputs && previewChunks && ( | ||||
| <div className='flex grow flex-col bg-background-body p-1'> | <div className='flex grow flex-col bg-background-body p-1'> | ||||
| <ChunkCardList chunkInfo={previewChunks} /> | |||||
| <ChunkCardList chunkType={outputs.chunk_structure} chunkInfo={previewChunks} /> | |||||
| <div className='system-xs-regular mt-1 flex items-center gap-x-2 text-text-tertiary'> | <div className='system-xs-regular mt-1 flex items-center gap-x-2 text-text-tertiary'> | ||||
| <div className='h-px flex-1 bg-gradient-to-r from-background-gradient-mask-transparent to-divider-regular' /> | <div className='h-px flex-1 bg-gradient-to-r from-background-gradient-mask-transparent to-divider-regular' /> | ||||
| <span className='shrink-0truncate' title={t('pipeline.result.resultPreview.footerTip', { count: RAG_PIPELINE_PREVIEW_CHUNK_NUM })}> | <span className='shrink-0truncate' title={t('pipeline.result.resultPreview.footerTip', { count: RAG_PIPELINE_PREVIEW_CHUNK_NUM })}> |
| import { RAG_PIPELINE_PREVIEW_CHUNK_NUM } from '@/config' | import { RAG_PIPELINE_PREVIEW_CHUNK_NUM } from '@/config' | ||||
| import { type ChunkInfo, ChunkType } from '../../../../chunk-card-list' | |||||
| import type { ChunkInfo, GeneralChunks, ParentChildChunks, QAChunks } from '../../../../chunk-card-list/types' | |||||
| import type { ParentMode } from '@/models/datasets' | |||||
| import { ChunkingMode } from '@/models/datasets' | |||||
| type GeneralChunkPreview = { | type GeneralChunkPreview = { | ||||
| content: string | content: string | ||||
| } | } | ||||
| const formatGeneralChunks = (outputs: any) => { | const formatGeneralChunks = (outputs: any) => { | ||||
| if (!outputs) return undefined | |||||
| const chunkInfo: ChunkInfo = { | |||||
| general_chunks: [], | |||||
| } | |||||
| const chunkInfo: GeneralChunks = [] | |||||
| const chunks = outputs.preview as GeneralChunkPreview[] | const chunks = outputs.preview as GeneralChunkPreview[] | ||||
| chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => { | chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => { | ||||
| chunkInfo.general_chunks?.push(chunk.content) | |||||
| chunkInfo.push(chunk.content) | |||||
| }) | }) | ||||
| return chunkInfo | return chunkInfo | ||||
| child_chunks: string[] | child_chunks: string[] | ||||
| } | } | ||||
| const formatParentChildChunks = (outputs: any, chunkType: ChunkType) => { | |||||
| if (!outputs) return undefined | |||||
| const chunkInfo: ChunkInfo = { | |||||
| const formatParentChildChunks = (outputs: any, parentMode: ParentMode) => { | |||||
| const chunkInfo: ParentChildChunks = { | |||||
| parent_child_chunks: [], | parent_child_chunks: [], | ||||
| parent_mode: chunkType, | |||||
| parent_mode: parentMode, | |||||
| } | } | ||||
| const chunks = outputs.preview as ParentChildChunkPreview[] | const chunks = outputs.preview as ParentChildChunkPreview[] | ||||
| if (chunkType === ChunkType.Paragraph) { | |||||
| if (parentMode === 'paragraph') { | |||||
| chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => { | chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => { | ||||
| chunkInfo.parent_child_chunks?.push({ | chunkInfo.parent_child_chunks?.push({ | ||||
| parent_content: chunk.content, | parent_content: chunk.content, | ||||
| child_contents: chunk.child_chunks, | child_contents: chunk.child_chunks, | ||||
| parent_mode: chunkType, | |||||
| parent_mode: parentMode, | |||||
| }) | }) | ||||
| }) | }) | ||||
| return chunkInfo | |||||
| } | } | ||||
| else { | |||||
| if (parentMode === 'full-doc') { | |||||
| chunks.forEach((chunk) => { | chunks.forEach((chunk) => { | ||||
| chunkInfo.parent_child_chunks?.push({ | chunkInfo.parent_child_chunks?.push({ | ||||
| parent_content: chunk.content, | parent_content: chunk.content, | ||||
| child_contents: chunk.child_chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM), | child_contents: chunk.child_chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM), | ||||
| parent_mode: chunkType, | |||||
| parent_mode: parentMode, | |||||
| }) | }) | ||||
| }) | }) | ||||
| } | } | ||||
| } | } | ||||
| const formatQAChunks = (outputs: any) => { | const formatQAChunks = (outputs: any) => { | ||||
| if (!outputs) return undefined | |||||
| const chunkInfo: ChunkInfo = { | |||||
| const chunkInfo: QAChunks = { | |||||
| qa_chunks: [], | qa_chunks: [], | ||||
| } | } | ||||
| const chunks = outputs.qa_preview as QAChunkPreview[] | const chunks = outputs.qa_preview as QAChunkPreview[] | ||||
| return chunkInfo | return chunkInfo | ||||
| } | } | ||||
| export const formatPreviewChunks = (chunkInfo: ChunkInfo, outputs: any): ChunkInfo | undefined => { | |||||
| if (!chunkInfo) return undefined | |||||
| let chunkType = ChunkType.General | |||||
| if (chunkInfo?.general_chunks) | |||||
| chunkType = ChunkType.General | |||||
| if (chunkInfo?.parent_child_chunks) | |||||
| chunkType = chunkInfo.parent_mode as ChunkType | |||||
| export const formatPreviewChunks = (outputs: any): ChunkInfo | undefined => { | |||||
| if (!outputs) return undefined | |||||
| if (chunkInfo?.qa_chunks) | |||||
| chunkType = ChunkType.QA | |||||
| const chunkingMode = outputs.chunk_structure | |||||
| const parentMode = outputs.parent_mode | |||||
| if (chunkType === ChunkType.General) | |||||
| if (chunkingMode === ChunkingMode.text) | |||||
| return formatGeneralChunks(outputs) | return formatGeneralChunks(outputs) | ||||
| if (chunkType === ChunkType.Paragraph || chunkType === ChunkType.FullDoc) | |||||
| return formatParentChildChunks(outputs, chunkType) | |||||
| if (chunkingMode === ChunkingMode.parentChild) | |||||
| return formatParentChildChunks(outputs, parentMode) | |||||
| if (chunkType === ChunkType.QA) | |||||
| if (chunkingMode === ChunkingMode.qa) | |||||
| return formatQAChunks(outputs) | return formatQAChunks(outputs) | ||||
| return undefined | return undefined |
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import BoolValue from '../panel/chat-variable-panel/components/bool-value' | import BoolValue from '../panel/chat-variable-panel/components/bool-value' | ||||
| import { useStore } from '@/app/components/workflow/store' | import { useStore } from '@/app/components/workflow/store' | ||||
| import { ChunkCardList, type ChunkInfo } from '@/app/components/rag-pipeline/components/chunk-card-list' | |||||
| import { ChunkCardList } from '@/app/components/rag-pipeline/components/chunk-card-list' | |||||
| import type { ChunkInfo } from '@/app/components/rag-pipeline/components/chunk-card-list/types' | |||||
| import { PreviewMode } from '../../base/features/types' | import { PreviewMode } from '../../base/features/types' | ||||
| import { ChunkingMode } from '@/models/datasets' | |||||
| enum ViewMode { | enum ViewMode { | ||||
| Code = 'code', | Code = 'code', | ||||
| {viewMode === ViewMode.Preview && ( | {viewMode === ViewMode.Preview && ( | ||||
| type === ContentType.Markdown | type === ContentType.Markdown | ||||
| ? <Markdown className='grow overflow-auto rounded-lg !bg-white px-4 py-3' content={(mdString ?? '') as string} /> | ? <Markdown className='grow overflow-auto rounded-lg !bg-white px-4 py-3' content={(mdString ?? '') as string} /> | ||||
| : <ChunkCardList chunkInfo={JSON.parse(jsonString!) as ChunkInfo} /> | |||||
| : <ChunkCardList | |||||
| chunkType={ChunkingMode.text} // todo: delete mock data | |||||
| parentMode={'full-doc'} // todo: delete mock data | |||||
| chunkInfo={JSON.parse(jsonString!) as ChunkInfo} | |||||
| /> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| </div> | </div> |