Ver código fonte

feat: variable preview

tags/2.0.0-beta.1
yessenia 2 meses atrás
pai
commit
ae183b348c

+ 9
- 0
web/app/components/base/features/types.ts Ver arquivo

@@ -29,6 +29,11 @@ export type SensitiveWordAvoidance = EnabledOrDisabled & {
config?: any
}

export enum PreviewMode {
NewPage = 'new_page',
CurrentPage = 'current_page',
}

export type FileUpload = {
image?: EnabledOrDisabled & {
detail?: Resolution
@@ -56,6 +61,10 @@ export type FileUpload = {
allowed_file_upload_methods?: TransferMethod[]
number_limits?: number
fileUploadConfig?: FileUploadConfigResponse
preview_config?: {
mode?: PreviewMode
file_type_list?: string[]
}
} & EnabledOrDisabled

export type AnnotationReplyConfig = {

+ 10
- 1
web/app/components/base/file-uploader/file-uploader-in-attachment/file-item.tsx Ver arquivo

@@ -23,6 +23,7 @@ import cn from '@/utils/classnames'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
import { PreviewMode } from '@/app/components/base/features/types'

type FileInAttachmentItemProps = {
file: FileEntity
@@ -31,6 +32,7 @@ type FileInAttachmentItemProps = {
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
canPreview?: boolean
previewMode?: PreviewMode
}
const FileInAttachmentItem = ({
file,
@@ -39,6 +41,7 @@ const FileInAttachmentItem = ({
onRemove,
onReUpload,
canPreview,
previewMode = PreviewMode.CurrentPage,
}: FileInAttachmentItemProps) => {
const { id, name, type, progress, supportFileType, base64Url, url, isRemote } = file
const ext = getFileExtension(name, type, isRemote)
@@ -49,7 +52,13 @@ const FileInAttachmentItem = ({
<div className={cn(
'flex h-12 items-center rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pr-3 shadow-xs',
progress === -1 && 'border-state-destructive-border bg-state-destructive-hover',
)}>
canPreview && previewMode === PreviewMode.NewPage && 'cursor-pointer',
)}
onClick={() => {
if (canPreview && previewMode === PreviewMode.NewPage)
window.open(url || base64Url || '', '_blank')
}}
>
<div className='flex h-12 w-12 items-center justify-center'>
{
isImageFile && (

+ 2
- 0
web/app/components/base/file-uploader/file-uploader-in-attachment/index.tsx Ver arquivo

@@ -106,6 +106,8 @@ const FileUploaderInAttachment = ({
showDownloadAction={false}
onRemove={() => handleRemoveFile(file.id)}
onReUpload={() => handleReUploadFile(file.id)}
canPreview={fileConfig.preview_config?.file_type_list?.includes(file.type)}
previewMode={fileConfig.preview_config?.mode}
/>
))
}

+ 3
- 0
web/app/components/base/segmented-control/index.tsx Ver arquivo

@@ -20,6 +20,7 @@ type SegmentedControlProps<T extends string | number | symbol> = {
onChange: (value: T) => void
className?: string
activeClassName?: string
btnClassName?: string
}

const SegmentedControlVariants = cva(
@@ -90,6 +91,7 @@ export const SegmentedControl = <T extends string | number | symbol>({
padding,
activeState,
activeClassName,
btnClassName,
}: SegmentedControlProps<T>
& VariantProps<typeof SegmentedControlVariants>
& VariantProps<typeof SegmentedControlItemVariants>
@@ -115,6 +117,7 @@ export const SegmentedControl = <T extends string | number | symbol>({
SegmentedControlItemVariants({ size, activeState: isSelected ? activeState : 'default' }),
isSelected && activeClassName,
disabled && 'disabled',
btnClassName,
)}
onClick={() => {
if (!isSelected)

+ 5
- 1
web/app/components/base/textarea/index.tsx Ver arquivo

@@ -24,12 +24,16 @@ export type TextareaProps = {
disabled?: boolean
destructive?: boolean
styleCss?: CSSProperties
onFocus?: () => void
onBlur?: () => void
} & React.TextareaHTMLAttributes<HTMLTextAreaElement> & VariantProps<typeof textareaVariants>

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, value, onChange, disabled, size, destructive, styleCss, ...props }, ref) => {
({ className, value, onChange, disabled, size, destructive, styleCss, onFocus, onBlur, ...props }, ref) => {
return (
<textarea
onFocus={onFocus}
onBlur={onBlur}
ref={ref}
style={styleCss}
className={cn(

+ 141
- 0
web/app/components/rag-pipeline/components/chunk-card-list/index.tsx Ver arquivo

@@ -0,0 +1,141 @@
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'

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>
}

enum ChunkType {
General = 'genaral',
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="inline-flex flex-col gap-1 self-stretch 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
}

type ChunkCardListProps = {
chunkInfo: ChunkInfo
}

export const ChunkCardList = (props: ChunkCardListProps) => {
const { chunkInfo } = 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])

return <div className='flex grow flex-col gap-1'>
{(chunkInfo.general_chunks ?? chunkInfo.parent_child_chunks ?? chunkInfo?.qa_chunks ?? []).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

return <ChunkCard
type={chunkType}
content={isParentChildMode ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)}
wordCount={wordCount}
positionId={index + 1}
/>
})}
</div>
}

+ 12
- 0
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor.tsx Ver arquivo

@@ -15,6 +15,8 @@ type CodeEditorProps = {
editorWrapperClassName?: string
readOnly?: boolean
hideTopMenu?: boolean
onFocus?: () => void
onBlur?: () => void
} & React.HTMLAttributes<HTMLDivElement>

const CodeEditor: FC<CodeEditorProps> = ({
@@ -25,6 +27,8 @@ const CodeEditor: FC<CodeEditorProps> = ({
readOnly = false,
hideTopMenu = false,
className,
onFocus,
onBlur,
}) => {
const { t } = useTranslation()
const { theme } = useTheme()
@@ -45,6 +49,14 @@ const CodeEditor: FC<CodeEditorProps> = ({
const handleEditorDidMount = useCallback((editor: any, monaco: any) => {
editorRef.current = editor
monacoRef.current = monaco

editor.onDidFocusEditorText(() => {
onFocus?.()
})
editor.onDidBlurEditorText(() => {
onBlur?.()
})

monaco.editor.defineTheme('light-theme', {
base: 'vs',
inherit: true,

+ 6
- 0
web/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor.tsx Ver arquivo

@@ -8,6 +8,8 @@ type SchemaEditorProps = {
hideTopMenu?: boolean
className?: string
readonly?: boolean
onFocus?: () => void
onBlur?: () => void
}

const SchemaEditor: FC<SchemaEditorProps> = ({
@@ -16,6 +18,8 @@ const SchemaEditor: FC<SchemaEditorProps> = ({
hideTopMenu,
className,
readonly = false,
onFocus,
onBlur,
}) => {
return (
<CodeEditor
@@ -25,6 +29,8 @@ const SchemaEditor: FC<SchemaEditorProps> = ({
value={schema}
onUpdate={onUpdate}
hideTopMenu={hideTopMenu}
onFocus={onFocus}
onBlur={onBlur}
/>
)
}

+ 116
- 10
web/app/components/workflow/variable-inspect/value-content.tsx Ver arquivo

@@ -1,6 +1,9 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks'
import { RiBracesLine, RiEyeLine } from '@remixicon/react'
import Textarea from '@/app/components/base/textarea'
import { Markdown } from '@/app/components/base/markdown'
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
@@ -13,6 +16,7 @@ import {
validateJSONSchema,
} from '@/app/components/workflow/variable-inspect/utils'
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
import { SegmentedControl } from '@/app/components/base/segmented-control'
import { JSON_SCHEMA_MAX_DEPTH } from '@/config'
import { TransferMethod } from '@/types/app'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
@@ -21,6 +25,84 @@ import type { VarInInspect } from '@/types/workflow'
import { VarInInspectType } from '@/types/workflow'
import cn from '@/utils/classnames'
import { useStore } from '@/app/components/workflow/store'
import { ChunkCardList, type ChunkInfo } from '@/app/components/rag-pipeline/components/chunk-card-list'
import { PreviewMode } from '../../base/features/types'

enum ViewMode {
Code = 'code',
Preview = 'preview',
}

enum ContentType {
Markdown = 'markdown',
Chunks = 'chunks',
}

type DisplayContentProps = {
type: ContentType
mdString?: string
jsonString?: string
readonly: boolean
handleTextChange?: (value: string) => void
handleEditorChange?: (value: string) => void
}

const DisplayContent = (props: DisplayContentProps) => {
const { type, mdString, jsonString, readonly, handleTextChange, handleEditorChange } = props
const [viewMode, setViewMode] = useState<ViewMode>(ViewMode.Code)
const [isFocused, setIsFocused] = useState(false)
const { t } = useTranslation()

return (
<div className={cn('flex h-full flex-col rounded-[10px] bg-components-input-bg-normal', isFocused && 'bg-components-input-bg-active outline outline-1 outline-components-input-border-active')}>
<div className='flex shrink-0 items-center justify-between p-1'>
<div className='system-xs-semibold-uppercase flex items-center px-2 py-0.5 text-text-secondary'>
{type.toUpperCase()}
</div>
<SegmentedControl
options={[
{ value: ViewMode.Code, text: t('workflow.nodes.templateTransform.code'), Icon: RiBracesLine },
{ value: ViewMode.Preview, text: t('workflow.common.preview'), Icon: RiEyeLine },
]}
value={viewMode}
onChange={setViewMode}
size='small'
padding='with'
activeClassName='!text-text-accent-light-mode-only'
btnClassName='!pl-1.5 !pr-0.5 gap-[3px]'
/>
</div>
<div className='flex flex-1 overflow-auto rounded-b-[10px] pb-1 pl-3 pr-1'>
{viewMode === ViewMode.Code && (
type === ContentType.Markdown
? <Textarea
readOnly={readonly}
disabled={readonly}
className='h-full border-none bg-transparent p-0 text-text-secondary hover:bg-transparent focus:bg-transparent focus:shadow-none'
value={mdString as any}
onChange={e => handleTextChange?.(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
: <SchemaEditor
readonly={readonly}
className='overflow-y-auto bg-transparent'
hideTopMenu
schema={jsonString!}
onUpdate={handleEditorChange!}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
)}
{viewMode === ViewMode.Preview && (
type === ContentType.Markdown
? <Markdown className='grow overflow-auto rounded-lg !bg-white px-4 py-3' content={(mdString ?? '') as string} />
: <ChunkCardList chunkInfo={JSON.parse(jsonString!) as ChunkInfo} />
)}
</div>
</div>
)
}

type Props = {
currentVar: VarInInspect
@@ -42,6 +124,13 @@ const ValueContent = ({
const JSONEditorDisabled = currentVar.value_type === 'array[any]'
const fileUploadConfig = useStore(s => s.fileUploadConfig)

const hasChunks = useMemo(() => {
return currentVar.value_type === 'object'
&& currentVar.value
&& typeof currentVar.value === 'object'
&& ['parent_child_chunks', 'general_chunks', 'qa_chunks'].some(key => key in currentVar.value)
}, [currentVar.value_type, currentVar.value])

const formatFileValue = (value: VarInInspect) => {
if (value.value_type === 'file')
return value.value ? getProcessedFilesFromResponse([value.value]) : []
@@ -72,7 +161,6 @@ const ValueContent = ({

if (showFileEditor)
setFileValue(formatFileValue(currentVar))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentVar.id, currentVar.value])

const handleTextChange = (value: string) => {
@@ -170,7 +258,14 @@ const ValueContent = ({
>
<div className={cn('grow')} style={{ height: `${editorHeight}px` }}>
{showTextEditor && (
<Textarea
currentVar.value_type === 'string' ? (
<DisplayContent
type={ContentType.Markdown}
mdString={value as any}
readonly={textEditorDisabled}
handleTextChange={handleTextChange}
/>
) : <Textarea
readOnly={textEditorDisabled}
disabled={textEditorDisabled}
className='h-full'
@@ -179,13 +274,20 @@ const ValueContent = ({
/>
)}
{showJSONEditor && (
<SchemaEditor
readonly={JSONEditorDisabled}
className='overflow-y-auto'
hideTopMenu
schema={json}
onUpdate={handleEditorChange}
/>
hasChunks
? <DisplayContent
type={ContentType.Chunks}
jsonString={json ?? '{}'}
readonly={JSONEditorDisabled}
handleEditorChange={handleEditorChange}
/>
: <SchemaEditor
readonly={JSONEditorDisabled}
className='overflow-y-auto'
hideTopMenu
schema={json}
onUpdate={handleEditorChange}
/>
)}
{showFileEditor && (
<div className='max-w-[460px]'>
@@ -208,6 +310,10 @@ const ValueContent = ({
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
number_limits: currentVar.value_type === 'file' ? 1 : fileUploadConfig?.workflow_file_upload_limit || 5,
fileUploadConfig,
preview_config: {
mode: PreviewMode.NewPage,
file_type_list: ['application/pdf'],
},
}}
isDisabled={textEditorDisabled}
/>

Carregando…
Cancelar
Salvar