ソースを参照

feat(workflow): add DisplayContent component for improved variable inspection and integrate schema type handling

tags/2.0.0-beta.1
twwu 2ヶ月前
コミット
9d3772f7d6

+ 1
- 1
web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts ファイルの表示

const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions) const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId) const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
const varsWithSchemaType = vars.map((varItem) => { const varsWithSchemaType = vars.map((varItem) => {
const schemaType = currentNodeOutputVars[0].vars.find(v => v.variable === varItem.name)?.schemaType || ''
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
return { return {
...varItem, ...varItem,
schemaType, schemaType,

+ 115
- 0
web/app/components/workflow/variable-inspect/display-content.tsx ファイルの表示

import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { SegmentedControl } from '@/app/components/base/segmented-control'
import cn from '@/utils/classnames'
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 type { ParentMode } from '@/models/datasets'
import { ChunkingMode } from '@/models/datasets'
import { PreviewType, ViewMode } from './types'
import type { VarType } from '../types'

type DisplayContentProps = {
previewType: PreviewType
varType: VarType
schemaType?: string
mdString?: string
jsonString?: string
readonly: boolean
handleTextChange?: (value: string) => void
handleEditorChange?: (value: string) => void
}

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

const chunkType = useMemo(() => {
if (previewType !== PreviewType.Chunks || !schemaType)
return undefined
if (schemaType === 'general_structure')
return ChunkingMode.text
if (schemaType === 'parent_child_structure')
return ChunkingMode.parentChild
if (schemaType === 'qa_structure')
return ChunkingMode.qa
}, [previewType, schemaType])

const parentMode = useMemo(() => {
if (previewType !== PreviewType.Chunks || !schemaType || !jsonString)
return undefined
if (schemaType === 'parent_child_structure')
return JSON.parse(jsonString!)?.parent_mode as ParentMode
return undefined
}, [previewType, schemaType, jsonString])

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-end p-1'>
{previewType === PreviewType.Markdown && (
<div className='system-xs-semibold-uppercase flex grow items-center px-2 py-0.5 text-text-secondary'>
{previewType.toUpperCase()}
</div>
)}
{previewType === PreviewType.Chunks && (
<div className='system-xs-semibold-uppercase flex grow items-center px-2 py-0.5 text-text-secondary'>
{varType.toUpperCase()}{schemaType ? `(${schemaType})` : ''}
</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]'
className='shrink-0'
/>
</div>
<div className='flex flex-1 overflow-auto rounded-b-[10px] pl-3 pr-1'>
{viewMode === ViewMode.Code && (
previewType === PreviewType.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 && (
previewType === PreviewType.Markdown
? <Markdown className='grow overflow-auto rounded-lg !bg-white px-4 py-3' content={(mdString ?? '') as string} />
: <ChunkCardList
chunkType={chunkType!}
parentMode={parentMode}
chunkInfo={JSON.parse(jsonString!) as ChunkInfo}
/>
)}
</div>
</div>
)
}

export default React.memo(DisplayContent)

+ 26
- 14
web/app/components/workflow/variable-inspect/right.tsx ファイルの表示

} as any) } as any)
handleHidePromptGenerator() handleHidePromptGenerator()
}, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator]) }, [setInputs, blockType, nodeId, node?.data, handleHidePromptGenerator])

const displaySchemaType = currentNodeVar?.var.schemaType ? (`(${currentNodeVar.var.schemaType})`) : ''

return ( return (
<div className={cn('flex h-full flex-col')}> <div className={cn('flex h-full flex-col')}>
{/* header */} {/* header */}
/> />
) )
} }
{currentNodeVar.nodeType !== VarInInspectType.environment && currentNodeVar.nodeType !== VarInInspectType.conversation && currentNodeVar.nodeType !== VarInInspectType.system && (
<>
<BlockIcon
className='shrink-0'
type={currentNodeVar.nodeType as BlockEnum}
size='xs'
toolIcon={toolIcon}
/>
<div className='system-sm-regular shrink-0 text-text-secondary'>{currentNodeVar.title}</div>
<div className='system-sm-regular shrink-0 text-text-quaternary'>/</div>
</>
)}
{currentNodeVar.nodeType !== VarInInspectType.environment
&& currentNodeVar.nodeType !== VarInInspectType.conversation
&& currentNodeVar.nodeType !== VarInInspectType.system
&& (
<>
<BlockIcon
className='shrink-0'
type={currentNodeVar.nodeType as BlockEnum}
size='xs'
toolIcon={toolIcon}
/>
<div className='system-sm-regular shrink-0 text-text-secondary'>{currentNodeVar.title}</div>
<div className='system-sm-regular shrink-0 text-text-quaternary'>/</div>
</>
)}
<div title={currentNodeVar.var.name} className='system-sm-semibold truncate text-text-secondary'>{currentNodeVar.var.name}</div> <div title={currentNodeVar.var.name} className='system-sm-semibold truncate text-text-secondary'>{currentNodeVar.var.name}</div>
<div className='system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary'> <div className='system-xs-medium ml-1 shrink-0 space-x-2 text-text-tertiary'>
<span>{`${currentNodeVar.var.value_type}${currentNodeVar.var.schemaType ? (`(${currentNodeVar.var.schemaType})`) : ''}`}</span>
<span>{`${currentNodeVar.var.value_type}${displaySchemaType}`}</span>
{isTruncated && ( {isTruncated && (
<> <>
<span>·</span> <span>·</span>
<Loading /> <Loading />
</div> </div>
)} )}
{currentNodeVar && !isValueFetching && <ValueContent currentVar={currentNodeVar.var} handleValueChange={handleValueChange} isTruncated={!!isTruncated} />}
{currentNodeVar && !isValueFetching && (
<ValueContent
currentVar={currentNodeVar.var}
handleValueChange={handleValueChange}
isTruncated={!!isTruncated}
/>
)}
</div> </div>
{isShowPromptGenerator && ( {isShowPromptGenerator && (
isCodeBlock isCodeBlock

+ 12
- 0
web/app/components/workflow/variable-inspect/types.ts ファイルの表示

export const EVENT_WORKFLOW_STOP = 'WORKFLOW_STOP' export const EVENT_WORKFLOW_STOP = 'WORKFLOW_STOP'

export const CHUNK_SCHEMA_TYPES = ['general_structure', 'parent_child_structure', 'qa_structure']

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

export enum PreviewType {
Markdown = 'markdown',
Chunks = 'chunks',
}

+ 37
- 114
web/app/components/workflow/variable-inspect/value-content.tsx ファイルの表示

import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useDebounceFn } from 'ahooks' import { useDebounceFn } from 'ahooks'
import { RiBracesLine, RiEyeLine } from '@remixicon/react'
import Textarea from '@/app/components/base/textarea' 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 SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader' import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message' import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
validateJSONSchema, validateJSONSchema,
} from '@/app/components/workflow/variable-inspect/utils' } from '@/app/components/workflow/variable-inspect/utils'
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/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 { JSON_SCHEMA_MAX_DEPTH } from '@/config'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import LargeDataAlert from './large-data-alert' import LargeDataAlert from './large-data-alert'
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 } 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 {
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] 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
chunkType={ChunkingMode.text} // todo: delete mock data
parentMode={'full-doc'} // todo: delete mock data
chunkInfo={JSON.parse(jsonString!) as ChunkInfo}
/>
)}
</div>
</div>
)
}
import DisplayContent from './display-content'
import { CHUNK_SCHEMA_TYPES, PreviewType } from './types'


type Props = { type Props = {
currentVar: VarInInspect currentVar: VarInInspect
const fileUploadConfig = useStore(s => s.fileUploadConfig) const fileUploadConfig = useStore(s => s.fileUploadConfig)


const hasChunks = useMemo(() => { 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])
if (!currentVar.schemaType)
return false
return CHUNK_SCHEMA_TYPES.includes(currentVar.schemaType)
}, [currentVar.schemaType])


const formatFileValue = (value: VarInInspect) => { const formatFileValue = (value: VarInInspect) => {
if (value.value_type === 'file') if (value.value_type === 'file')
{ {
currentVar.value_type === 'string' ? ( currentVar.value_type === 'string' ? (
<DisplayContent <DisplayContent
type={ContentType.Markdown}
previewType={PreviewType.Markdown}
varType={currentVar.value_type}
mdString={value as any} mdString={value as any}
readonly={textEditorDisabled} readonly={textEditorDisabled}
handleTextChange={handleTextChange} handleTextChange={handleTextChange}
/> />
) : <Textarea
readOnly={textEditorDisabled}
disabled={textEditorDisabled || isTruncated}
className={cn('h-full', isTruncated && 'pt-[48px]')}
value={value as any}
onChange={e => handleTextChange(e.target.value)}
/>
) : (
<Textarea
readOnly={textEditorDisabled}
disabled={textEditorDisabled || isTruncated}
className={cn('h-full', isTruncated && 'pt-[48px]')}
value={value as any}
onChange={e => handleTextChange(e.target.value)}
/>
)
} }
</> </>
)} )}
} }
{showJSONEditor && ( {showJSONEditor && (
hasChunks hasChunks
? <DisplayContent
type={ContentType.Chunks}
jsonString={json ?? '{}'}
readonly={JSONEditorDisabled}
handleEditorChange={handleEditorChange}
/>
: <SchemaEditor
readonly={JSONEditorDisabled || isTruncated}
className='overflow-y-auto'
hideTopMenu
schema={json}
onUpdate={handleEditorChange}
isTruncated={isTruncated}
/>
? (
<DisplayContent
previewType={PreviewType.Chunks}
varType={currentVar.value_type}
schemaType={currentVar.schemaType ?? ''}
jsonString={json ?? '{}'}
readonly={JSONEditorDisabled}
handleEditorChange={handleEditorChange}
/>
)
: (
<SchemaEditor
readonly={JSONEditorDisabled || isTruncated}
className='overflow-y-auto'
hideTopMenu
schema={json}
onUpdate={handleEditorChange}
isTruncated={isTruncated}
/>
)
)} )}
{showFileEditor && ( {showFileEditor && (
<div className='max-w-[460px]'> <div className='max-w-[460px]'>

読み込み中…
キャンセル
保存