mirror of https://github.com/langgenius/dify.git
make expand/collapse in question classifier node (#26772)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
parent
1be38183e5
commit
014cbaf387
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdgesInteractions } from '../../../hooks'
|
||||
|
|
@ -11,9 +11,13 @@ import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
|||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { noop } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.questionClassifiers'
|
||||
|
||||
// Layout constants
|
||||
const HANDLE_SIDE_WIDTH = 3 // Width offset for drag handle spacing
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
list: Topic[]
|
||||
|
|
@ -33,6 +37,10 @@ const ClassList: FC<Props> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions()
|
||||
const listContainerRef = useRef<HTMLDivElement>(null)
|
||||
const [shouldScrollToEnd, setShouldScrollToEnd] = useState(false)
|
||||
const prevListLength = useRef(list.length)
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const handleClassChange = useCallback((index: number) => {
|
||||
return (value: Topic) => {
|
||||
|
|
@ -48,7 +56,10 @@ const ClassList: FC<Props> = ({
|
|||
draft.push({ id: `${Date.now()}`, name: '' })
|
||||
})
|
||||
onChange(newList)
|
||||
}, [list, onChange])
|
||||
setShouldScrollToEnd(true)
|
||||
if (collapsed)
|
||||
setCollapsed(false)
|
||||
}, [list, onChange, collapsed])
|
||||
|
||||
const handleRemoveClass = useCallback((index: number) => {
|
||||
return () => {
|
||||
|
|
@ -61,57 +72,96 @@ const ClassList: FC<Props> = ({
|
|||
}, [list, onChange, handleEdgeDeleteByDeleteBranch, nodeId])
|
||||
|
||||
const topicCount = list.length
|
||||
const handleSideWidth = 3
|
||||
// Todo Remove; edit topic name
|
||||
|
||||
// Scroll to the newly added item after the list updates
|
||||
useEffect(() => {
|
||||
if (shouldScrollToEnd && list.length > prevListLength.current)
|
||||
setShouldScrollToEnd(false)
|
||||
prevListLength.current = list.length
|
||||
}, [list.length, shouldScrollToEnd])
|
||||
|
||||
const handleCollapse = useCallback(() => {
|
||||
setCollapsed(!collapsed)
|
||||
}, [collapsed])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactSortable
|
||||
list={list.map(item => ({ ...item }))}
|
||||
setList={handleSortTopic}
|
||||
handle='.handle'
|
||||
ghostClass='bg-components-panel-bg'
|
||||
animation={150}
|
||||
disabled={readonly}
|
||||
className='space-y-2'
|
||||
>
|
||||
{
|
||||
list.map((item, index) => {
|
||||
const canDrag = (() => {
|
||||
if (readonly)
|
||||
return false
|
||||
<div className='mb-2 flex items-center justify-between' onClick={handleCollapse}>
|
||||
<div className='flex cursor-pointer items-center text-xs font-semibold uppercase text-text-secondary'>
|
||||
{t(`${i18nPrefix}.class`)} <span className='text-text-destructive'>*</span>
|
||||
{list.length > 0 && (
|
||||
<ArrowDownRoundFill
|
||||
className={cn(
|
||||
'h-4 w-4 text-text-quaternary transition-transform duration-200',
|
||||
collapsed && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return topicCount >= 2
|
||||
})()
|
||||
return (
|
||||
<div key={item.id}
|
||||
className={cn(
|
||||
'group relative rounded-[10px] bg-components-panel-bg',
|
||||
`-ml-${handleSideWidth} min-h-[40px] px-0 py-0`,
|
||||
)}>
|
||||
<div >
|
||||
<Item
|
||||
className={cn(canDrag && 'handle')}
|
||||
headerClassName={cn(canDrag && 'cursor-grab')}
|
||||
nodeId={nodeId}
|
||||
key={list[index].id}
|
||||
payload={item}
|
||||
onChange={handleClassChange(index)}
|
||||
onRemove={handleRemoveClass(index)}
|
||||
index={index + 1}
|
||||
readonly={readonly}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ReactSortable>
|
||||
{!readonly && (
|
||||
<AddButton
|
||||
onClick={handleAddClass}
|
||||
text={t(`${i18nPrefix}.addClass`)}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<div
|
||||
ref={listContainerRef}
|
||||
className={cn('overflow-y-visible', `pl-${HANDLE_SIDE_WIDTH}`)}
|
||||
>
|
||||
<ReactSortable
|
||||
list={list.map(item => ({ ...item }))}
|
||||
setList={handleSortTopic}
|
||||
handle='.handle'
|
||||
ghostClass='bg-components-panel-bg'
|
||||
animation={150}
|
||||
disabled={readonly}
|
||||
className='space-y-2'
|
||||
>
|
||||
{
|
||||
list.map((item, index) => {
|
||||
const canDrag = (() => {
|
||||
if (readonly)
|
||||
return false
|
||||
|
||||
return topicCount >= 2
|
||||
})()
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'group relative rounded-[10px] bg-components-panel-bg',
|
||||
`-ml-${HANDLE_SIDE_WIDTH} min-h-[40px] px-0 py-0`,
|
||||
)}
|
||||
style={{
|
||||
// Performance hint for browser
|
||||
contain: 'layout style paint',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Item
|
||||
className={cn(canDrag && 'handle')}
|
||||
headerClassName={cn(canDrag && 'cursor-grab')}
|
||||
nodeId={nodeId}
|
||||
key={list[index].id}
|
||||
payload={item}
|
||||
onChange={handleClassChange(index)}
|
||||
onRemove={handleRemoveClass(index)}
|
||||
index={index + 1}
|
||||
readonly={readonly}
|
||||
filterVar={filterVar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
)}
|
||||
{!readonly && !collapsed && (
|
||||
<div className='mt-2'>
|
||||
<AddButton
|
||||
onClick={handleAddClass}
|
||||
text={t(`${i18nPrefix}.addClass`)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TFunction } from 'i18next'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import InfoPanel from '../_base/components/info-panel'
|
||||
import { NodeSourceHandle } from '../_base/components/node-handle'
|
||||
import type { QuestionClassifierNodeType } from './types'
|
||||
import {
|
||||
|
|
@ -10,9 +10,57 @@ import {
|
|||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import ReadonlyInputWithSelectVar from '../_base/components/readonly-input-with-select-var'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.questionClassifiers'
|
||||
|
||||
const MAX_CLASS_TEXT_LENGTH = 50
|
||||
|
||||
type TruncatedClassItemProps = {
|
||||
topic: { id: string; name: string }
|
||||
index: number
|
||||
nodeId: string
|
||||
t: TFunction
|
||||
}
|
||||
|
||||
const TruncatedClassItem: FC<TruncatedClassItemProps> = ({ topic, index, nodeId, t }) => {
|
||||
const truncatedText = topic.name.length > MAX_CLASS_TEXT_LENGTH
|
||||
? `${topic.name.slice(0, MAX_CLASS_TEXT_LENGTH)}...`
|
||||
: topic.name
|
||||
|
||||
const shouldShowTooltip = topic.name.length > MAX_CLASS_TEXT_LENGTH
|
||||
|
||||
const content = (
|
||||
<div className='system-xs-regular truncate text-text-tertiary'>
|
||||
<ReadonlyInputWithSelectVar
|
||||
value={truncatedText}
|
||||
nodeId={nodeId}
|
||||
className='truncate'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-y-0.5 rounded-md bg-workflow-block-parma-bg px-[5px] py-[3px]'>
|
||||
<div className='system-2xs-semibold-uppercase uppercase text-text-secondary'>
|
||||
{`${t(`${i18nPrefix}.class`)} ${index + 1}`}
|
||||
</div>
|
||||
{shouldShowTooltip
|
||||
? (<Tooltip
|
||||
popupContent={
|
||||
<div className='max-w-[300px] break-words'>
|
||||
<ReadonlyInputWithSelectVar value={topic.name} nodeId={nodeId}/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</Tooltip>
|
||||
)
|
||||
: content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
@ -41,27 +89,26 @@ const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
|
|||
{
|
||||
!!topics.length && (
|
||||
<div className='mt-2 space-y-0.5'>
|
||||
{topics.map((topic, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='relative'
|
||||
>
|
||||
<InfoPanel
|
||||
title={`${t(`${i18nPrefix}.class`)} ${index + 1}`}
|
||||
content={
|
||||
<ReadonlyInputWithSelectVar
|
||||
value={topic.name}
|
||||
nodeId={id}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId={topic.id}
|
||||
handleClassName='!top-1/2 !-translate-y-1/2 !-right-[21px]'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<div className='space-y-0.5'>
|
||||
{topics.map((topic, index) => (
|
||||
<div
|
||||
key={topic.id}
|
||||
className='relative'
|
||||
>
|
||||
<TruncatedClassItem
|
||||
topic={topic}
|
||||
index={index}
|
||||
nodeId={id}
|
||||
t={t}
|
||||
/>
|
||||
<NodeSourceHandle
|
||||
{...props}
|
||||
handleId={topic.id}
|
||||
handleClassName='!top-1/2 !-translate-y-1/2 !-right-[21px]'
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,19 +89,14 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
|
|||
config={inputs.vision?.configs}
|
||||
onConfigChange={handleVisionResolutionChange}
|
||||
/>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.class`)}
|
||||
required
|
||||
>
|
||||
<ClassList
|
||||
nodeId={id}
|
||||
list={inputs.classes}
|
||||
onChange={handleTopicsChange}
|
||||
readonly={readOnly}
|
||||
filterVar={filterVar}
|
||||
handleSortTopic={handleSortTopic}
|
||||
/>
|
||||
</Field>
|
||||
<ClassList
|
||||
nodeId={id}
|
||||
list={inputs.classes}
|
||||
onChange={handleTopicsChange}
|
||||
readonly={readOnly}
|
||||
filterVar={filterVar}
|
||||
handleSortTopic={handleSortTopic}
|
||||
/>
|
||||
<Split />
|
||||
</div>
|
||||
<FieldCollapse
|
||||
|
|
|
|||
Loading…
Reference in New Issue