| | |
| | | 'use client' |
| | | import type { FC } from 'react' |
| | | import React, { useEffect, useMemo, useRef, useState } from 'react' |
| | | import React, { useEffect, useRef, useState } from 'react' |
| | | import { useHover } from 'ahooks' |
| | | import { useTranslation } from 'react-i18next' |
| | | import cn from '@/utils/classnames' |
| | |
| | | import Input from '@/app/components/base/input' |
| | | import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' |
| | | import { checkKeys } from '@/utils/var' |
| | | import type { StructuredOutput } from '../../../llm/types' |
| | | import { Type } from '../../../llm/types' |
| | | import PickerStructurePanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker' |
| | | import { varTypeToStructType } from './utils' |
| | | import type { Field } from '@/app/components/workflow/nodes/llm/types' |
| | | import { FILE_STRUCT } from '@/app/components/workflow/constants' |
| | | import { Loop } from '@/app/components/base/icons/src/vender/workflow' |
| | | import { noop } from 'lodash-es' |
| | | |
| | | type ObjectChildrenProps = { |
| | | nodeId: string |
| | |
| | | itemWidth?: number |
| | | isSupportFileVar?: boolean |
| | | isException?: boolean |
| | | isLoopVar?: boolean |
| | | } |
| | | |
| | | const objVarTypes = [VarType.object, VarType.file] |
| | | |
| | | const Item: FC<ItemProps> = ({ |
| | | nodeId, |
| | |
| | | itemData, |
| | | onChange, |
| | | onHovering, |
| | | itemWidth, |
| | | isSupportFileVar, |
| | | isException, |
| | | isLoopVar, |
| | | }) => { |
| | | const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties |
| | | const isFile = itemData.type === VarType.file && !isStructureOutput |
| | | const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && (itemData.children as Var[]).length > 0) |
| | | const isFile = itemData.type === VarType.file |
| | | const isObj = ([VarType.object, VarType.file].includes(itemData.type) && itemData.children && itemData.children.length > 0) |
| | | const isSys = itemData.variable.startsWith('sys.') |
| | | const isEnv = itemData.variable.startsWith('env.') |
| | | const isChatVar = itemData.variable.startsWith('conversation.') |
| | | |
| | | const objStructuredOutput: StructuredOutput | null = useMemo(() => { |
| | | if (!isObj) return null |
| | | const properties: Record<string, Field> = {}; |
| | | (isFile ? FILE_STRUCT : (itemData.children as Var[])).forEach((c) => { |
| | | properties[c.variable] = { |
| | | type: varTypeToStructType(c.type), |
| | | } |
| | | }) |
| | | return { |
| | | schema: { |
| | | type: Type.object, |
| | | properties, |
| | | required: [], |
| | | additionalProperties: false, |
| | | }, |
| | | } |
| | | }, [isFile, isObj, itemData.children]) |
| | | |
| | | const structuredOutput = (() => { |
| | | if (isStructureOutput) |
| | | return itemData.children as StructuredOutput |
| | | return objStructuredOutput |
| | | })() |
| | | |
| | | const itemRef = useRef<HTMLDivElement>(null) |
| | | const itemRef = useRef(null) |
| | | const [isItemHovering, setIsItemHovering] = useState(false) |
| | | useHover(itemRef, { |
| | | const _ = useHover(itemRef, { |
| | | onChange: (hovering) => { |
| | | if (hovering) { |
| | | setIsItemHovering(true) |
| | | } |
| | | else { |
| | | if (isObj || isStructureOutput) { |
| | | if (isObj) { |
| | | setTimeout(() => { |
| | | setIsItemHovering(false) |
| | | }, 100) |
| | |
| | | }) |
| | | const [isChildrenHovering, setIsChildrenHovering] = useState(false) |
| | | const isHovering = isItemHovering || isChildrenHovering |
| | | const open = (isObj || isStructureOutput) && isHovering |
| | | const open = isObj && isHovering |
| | | useEffect(() => { |
| | | onHovering && onHovering(isHovering) |
| | | // eslint-disable-next-line react-hooks/exhaustive-deps |
| | |
| | | return ( |
| | | <PortalToFollowElem |
| | | open={open} |
| | | onOpenChange={noop} |
| | | onOpenChange={() => { }} |
| | | placement='left-start' |
| | | > |
| | | <PortalToFollowElemTrigger className='w-full'> |
| | | <div |
| | | ref={itemRef} |
| | | className={cn( |
| | | (isObj || isStructureOutput) ? ' pr-1' : 'pr-[18px]', |
| | | isHovering && ((isObj || isStructureOutput) ? 'bg-primary-50' : 'bg-state-base-hover'), |
| | | 'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3') |
| | | isObj ? ' pr-1' : 'pr-[18px]', |
| | | isHovering && (isObj ? 'bg-primary-50' : 'bg-state-base-hover'), |
| | | 'relative w-full flex items-center h-6 pl-3 rounded-md cursor-pointer') |
| | | } |
| | | onClick={handleChosen} |
| | | onMouseDown={e => e.preventDefault()} |
| | | > |
| | | <div className='flex w-0 grow items-center'> |
| | | {!isEnv && !isChatVar && !isLoopVar && <Variable02 className={cn('h-3.5 w-3.5 shrink-0 text-text-accent', isException && 'text-text-warning')} />} |
| | | {isEnv && <Env className='h-3.5 w-3.5 shrink-0 text-util-colors-violet-violet-600' />} |
| | | {isChatVar && <BubbleX className='h-3.5 w-3.5 shrink-0 text-util-colors-teal-teal-700' />} |
| | | {isLoopVar && <Loop className='h-3.5 w-3.5 shrink-0 text-util-colors-cyan-cyan-500' />} |
| | | <div className='flex items-center w-0 grow'> |
| | | {!isEnv && !isChatVar && <Variable02 className={cn('shrink-0 w-3.5 h-3.5 text-text-accent', isException && 'text-text-warning')} />} |
| | | {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />} |
| | | {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />} |
| | | {!isEnv && !isChatVar && ( |
| | | <div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable}</div> |
| | | <div title={itemData.variable} className='ml-1 w-0 grow truncate text-text-secondary system-sm-medium'>{itemData.variable}</div> |
| | | )} |
| | | {isEnv && ( |
| | | <div title={itemData.variable} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('env.', '')}</div> |
| | | <div title={itemData.variable} className='ml-1 w-0 grow truncate text-text-secondary system-sm-medium'>{itemData.variable.replace('env.', '')}</div> |
| | | )} |
| | | {isChatVar && ( |
| | | <div title={itemData.des} className='system-sm-medium ml-1 w-0 grow truncate text-text-secondary'>{itemData.variable.replace('conversation.', '')}</div> |
| | | <div title={itemData.des} className='ml-1 w-0 grow truncate text-text-secondary system-sm-medium'>{itemData.variable.replace('conversation.', '')}</div> |
| | | )} |
| | | </div> |
| | | <div className='ml-1 shrink-0 text-xs font-normal capitalize text-text-tertiary'>{itemData.type}</div> |
| | | { |
| | | (isObj || isStructureOutput) && ( |
| | | <ChevronRight className={cn('ml-0.5 h-3 w-3 text-text-quaternary', isHovering && 'text-text-tertiary')} /> |
| | | ) |
| | | } |
| | | <div className='ml-1 shrink-0 text-xs font-normal text-text-tertiary capitalize'>{itemData.type}</div> |
| | | {isObj && ( |
| | | <ChevronRight className={cn('ml-0.5 w-3 h-3 text-text-quaternary', isHovering && 'text-text-tertiary')} /> |
| | | )} |
| | | </div > |
| | | </PortalToFollowElemTrigger > |
| | | <PortalToFollowElemContent style={{ |
| | | zIndex: 100, |
| | | }}> |
| | | {(isStructureOutput || isObj) && ( |
| | | <PickerStructurePanel |
| | | root={{ nodeId, nodeName: title, attrName: itemData.variable }} |
| | | payload={structuredOutput!} |
| | | {(isObj && !isFile) && ( |
| | | // eslint-disable-next-line @typescript-eslint/no-use-before-define |
| | | <ObjectChildren |
| | | nodeId={nodeId} |
| | | title={title} |
| | | objPath={[...objPath, itemData.variable]} |
| | | data={itemData.children as Var[]} |
| | | onChange={onChange} |
| | | onHovering={setIsChildrenHovering} |
| | | onSelect={(valueSelector) => { |
| | | onChange(valueSelector, itemData) |
| | | }} |
| | | itemWidth={itemWidth} |
| | | isSupportFileVar={isSupportFileVar} |
| | | /> |
| | | )} |
| | | {isFile && ( |
| | | // eslint-disable-next-line @typescript-eslint/no-use-before-define |
| | | <ObjectChildren |
| | | nodeId={nodeId} |
| | | title={title} |
| | | objPath={[...objPath, itemData.variable]} |
| | | data={FILE_STRUCT} |
| | | onChange={onChange} |
| | | onHovering={setIsChildrenHovering} |
| | | itemWidth={itemWidth} |
| | | isSupportFileVar={isSupportFileVar} |
| | | /> |
| | | )} |
| | | </PortalToFollowElemContent> |
| | |
| | | isSupportFileVar, |
| | | }) => { |
| | | const currObjPath = objPath |
| | | const itemRef = useRef<HTMLDivElement>(null) |
| | | const itemRef = useRef(null) |
| | | const [isItemHovering, setIsItemHovering] = useState(false) |
| | | useHover(itemRef, { |
| | | const _ = useHover(itemRef, { |
| | | onChange: (hovering) => { |
| | | if (hovering) { |
| | | setIsItemHovering(true) |
| | |
| | | }, [isItemHovering]) |
| | | // absolute top-[-2px] |
| | | return ( |
| | | <div ref={itemRef} className=' space-y-1 rounded-lg border border-gray-200 bg-white shadow-lg' style={{ |
| | | <div ref={itemRef} className=' bg-white rounded-lg border border-gray-200 shadow-lg space-y-1' style={{ |
| | | right: itemWidth ? itemWidth - 10 : 215, |
| | | minWidth: 252, |
| | | }}> |
| | | <div className='flex h-[22px] items-center px-3 text-xs font-normal text-gray-700'><span className='text-gray-500'>{title}.</span>{currObjPath.join('.')}</div> |
| | | <div className='flex items-center h-[22px] px-3 text-xs font-normal text-gray-700'><span className='text-gray-500'>{title}.</span>{currObjPath.join('.')}</div> |
| | | { |
| | | (data && data.length > 0) |
| | | && data.map((v, i) => ( |
| | |
| | | onChange: (value: ValueSelector, item: Var) => void |
| | | itemWidth?: number |
| | | maxHeightClass?: string |
| | | onClose?: () => void |
| | | onBlur?: () => void |
| | | } |
| | | const VarReferenceVars: FC<Props> = ({ |
| | | hideSearch, |
| | |
| | | onChange, |
| | | itemWidth, |
| | | maxHeightClass, |
| | | onClose, |
| | | onBlur, |
| | | }) => { |
| | | const { t } = useTranslation() |
| | | const [searchText, setSearchText] = useState('') |
| | | |
| | | const handleKeyDown = (e: React.KeyboardEvent) => { |
| | | if (e.key === 'Escape') { |
| | | e.preventDefault() |
| | | onClose?.() |
| | | } |
| | | } |
| | | |
| | | const filteredVars = vars.filter((v) => { |
| | | const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.') || v.variable.startsWith('conversation.')) |
| | |
| | | { |
| | | !hideSearch && ( |
| | | <> |
| | | <div className={cn('var-search-input-wrapper mx-2 mb-1 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}> |
| | | <div className={cn('mb-1 mx-2 mt-2', searchBoxClassName)} onClick={e => e.stopPropagation()}> |
| | | <Input |
| | | className='var-search-input' |
| | | showLeftIcon |
| | | showClearIcon |
| | | value={searchText} |
| | | placeholder={t('workflow.common.searchVar') || ''} |
| | | onChange={e => setSearchText(e.target.value)} |
| | | onKeyDown={handleKeyDown} |
| | | onClear={() => setSearchText('')} |
| | | onBlur={onBlur} |
| | | autoFocus |
| | | /> |
| | | </div> |
| | | <div className='relative left-[-4px] h-[0.5px] bg-black/5' style={{ |
| | | <div className='h-[0.5px] bg-black/5 relative left-[-4px]' style={{ |
| | | width: 'calc(100% + 8px)', |
| | | }}></div> |
| | | </> |
| | |
| | | filteredVars.map((item, i) => ( |
| | | <div key={i}> |
| | | <div |
| | | className='system-xs-medium-uppercase truncate px-3 leading-[22px] text-text-tertiary' |
| | | className='leading-[22px] px-3 text-text-tertiary system-xs-medium-uppercase truncate' |
| | | title={item.title} |
| | | >{item.title}</div> |
| | | {item.vars.map((v, j) => ( |
| | |
| | | itemWidth={itemWidth} |
| | | isSupportFileVar={isSupportFileVar} |
| | | isException={v.isException} |
| | | isLoopVar={item.isLoop} |
| | | /> |
| | | ))} |
| | | </div>)) |
| | | } |
| | | </div> |
| | | : <div className='pl-3 text-xs font-medium uppercase leading-[18px] text-gray-500'>{t('workflow.common.noVar')}</div>} |
| | | : <div className='pl-3 leading-[18px] text-xs font-medium text-gray-500 uppercase'>{t('workflow.common.noVar')}</div>} |
| | | </> |
| | | ) |
| | | } |