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:
znn 2025-11-20 10:17:34 +07:00 committed by GitHub
parent 1be38183e5
commit 014cbaf387
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 177 additions and 85 deletions

View File

@ -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,10 +72,39 @@ 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 (
<>
<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>
{!collapsed && (
<div
ref={listContainerRef}
className={cn('overflow-y-visible', `pl-${HANDLE_SIDE_WIDTH}`)}
>
<ReactSortable
list={list.map(item => ({ ...item }))}
setList={handleSortTopic}
@ -83,11 +123,17 @@ const ClassList: FC<Props> = ({
return topicCount >= 2
})()
return (
<div key={item.id}
<div
key={item.id}
className={cn(
'group relative rounded-[10px] bg-components-panel-bg',
`-ml-${handleSideWidth} min-h-[40px] px-0 py-0`,
)}>
`-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')}
@ -107,11 +153,15 @@ const ClassList: FC<Props> = ({
})
}
</ReactSortable>
{!readonly && (
</div>
)}
{!readonly && !collapsed && (
<div className='mt-2'>
<AddButton
onClick={handleAddClass}
text={t(`${i18nPrefix}.addClass`)}
/>
</div>
)}
</>
)

View File

@ -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,19 +89,17 @@ const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
{
!!topics.length && (
<div className='mt-2 space-y-0.5'>
<div className='space-y-0.5'>
{topics.map((topic, index) => (
<div
key={index}
key={topic.id}
className='relative'
>
<InfoPanel
title={`${t(`${i18nPrefix}.class`)} ${index + 1}`}
content={
<ReadonlyInputWithSelectVar
value={topic.name}
<TruncatedClassItem
topic={topic}
index={index}
nodeId={id}
/>
}
t={t}
/>
<NodeSourceHandle
{...props}
@ -63,6 +109,7 @@ const Node: FC<NodeProps<QuestionClassifierNodeType>> = (props) => {
</div>
))}
</div>
</div>
)
}
</div>

View File

@ -89,10 +89,6 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
config={inputs.vision?.configs}
onConfigChange={handleVisionResolutionChange}
/>
<Field
title={t(`${i18nPrefix}.class`)}
required
>
<ClassList
nodeId={id}
list={inputs.classes}
@ -101,7 +97,6 @@ const Panel: FC<NodePanelProps<QuestionClassifierNodeType>> = ({
filterVar={filterVar}
handleSortTopic={handleSortTopic}
/>
</Field>
<Split />
</div>
<FieldCollapse