Feat: Modify the style of the classification operator and fix some console errors. (#13314)

### What problem does this PR solve?

Feat: Modify the style of the classification operator and fix some
console errors.

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2026-03-02 16:53:24 +08:00
committed by GitHub
parent 5fc3bd38b0
commit 7d6f20585f
9 changed files with 216 additions and 190 deletions

View File

@ -16,7 +16,7 @@ interface EditTagsProps {
}
const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
({ value = [], onChange, disabled }: EditTagsProps) => {
function EditTag({ value = [], onChange, disabled }, ref) {
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
@ -82,7 +82,7 @@ const EditTag = React.forwardRef<HTMLDivElement, EditTagsProps>(
const tagChild = value?.map(forMap);
return (
<div>
<div ref={ref}>
{inputVisible && (
<Input
ref={inputRef}

View File

@ -2,12 +2,14 @@ import { isNumber, trim } from 'lodash';
import { MinusIcon, PlusIcon } from 'lucide-react';
import React, {
FocusEventHandler,
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { InputProps } from '../ui/input';
interface NumberInputProps {
className?: string;
@ -18,113 +20,119 @@ interface NumberInputProps {
max?: number;
}
const NumberInput: React.FC<NumberInputProps> = ({
className,
value: initialValue,
onChange,
height,
min = 0,
max = Infinity,
}) => {
const [value, setValue] = useState<number | ''>(() => {
return initialValue ?? 0;
});
const NumberInput = forwardRef<HTMLInputElement, InputProps & NumberInputProps>(
function NumberInput(
{
className,
value: initialValue,
onChange,
height,
min = 0,
max = Infinity,
},
ref,
) {
const [value, setValue] = useState<number | ''>(() => {
return initialValue ?? 0;
});
const valueRef = useRef<number>();
const valueRef = useRef<number>();
useEffect(() => {
if (initialValue !== undefined) {
setValue(initialValue);
}
}, [initialValue]);
const handleDecrement = () => {
if (isNumber(value) && value > min) {
setValue(value - 1);
onChange?.(value - 1);
}
};
const handleIncrement = () => {
if (!isNumber(value)) {
return;
}
if (value > max - 1) {
return;
}
setValue(value + 1);
onChange?.(value + 1);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const currentValue = e.target.value;
const newValue = Number(currentValue);
if (trim(currentValue) === '') {
if (isNumber(value)) {
valueRef.current = value;
useEffect(() => {
if (initialValue !== undefined) {
setValue(initialValue);
}
setValue('');
return;
}
}, [initialValue]);
if (!isNaN(newValue)) {
if (newValue > max || newValue < min) {
const handleDecrement = () => {
if (isNumber(value) && value > min) {
setValue(value - 1);
onChange?.(value - 1);
}
};
const handleIncrement = () => {
if (!isNumber(value)) {
return;
}
setValue(newValue);
onChange?.(newValue);
}
};
if (value > max - 1) {
return;
}
setValue(value + 1);
onChange?.(value + 1);
};
const handleBlur: FocusEventHandler<HTMLInputElement> = useCallback(() => {
if (isNumber(value)) {
onChange?.(value);
} else {
const previousValue = valueRef.current ?? min;
setValue(previousValue);
onChange?.(previousValue);
}
}, [min, onChange, value]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const currentValue = e.target.value;
const newValue = Number(currentValue);
const style = useMemo(
() => ({
height: height ? `${height.toString().replace('px', '')}px` : 'auto',
}),
[height],
);
return (
<div
className={`flex h-10 items-center space-x-2 border-[1px] rounded-lg w-[150px] ${className || ''}`}
style={style}
>
<button
type="button"
className="w-10 p-2 focus:outline-none border-r-[1px]"
onClick={handleDecrement}
if (trim(currentValue) === '') {
if (isNumber(value)) {
valueRef.current = value;
}
setValue('');
return;
}
if (!isNaN(newValue)) {
if (newValue > max || newValue < min) {
return;
}
setValue(newValue);
onChange?.(newValue);
}
};
const handleBlur: FocusEventHandler<HTMLInputElement> = useCallback(() => {
if (isNumber(value)) {
onChange?.(value);
} else {
const previousValue = valueRef.current ?? min;
setValue(previousValue);
onChange?.(previousValue);
}
}, [min, onChange, value]);
const style = useMemo(
() => ({
height: height ? `${height.toString().replace('px', '')}px` : 'auto',
}),
[height],
);
return (
<div
className={`flex h-10 items-center space-x-2 border-[1px] rounded-lg w-[150px] ${className || ''}`}
style={style}
ref={ref}
>
<MinusIcon size={16} aria-hidden="true" />
</button>
<input
type="text"
value={value}
onChange={handleChange}
onBlur={handleBlur}
className="w-full flex-1 text-center bg-transparent focus:outline-none"
style={style}
min={min}
/>
<button
type="button"
className="w-10 p-2 focus:outline-none border-l-[1px]"
onClick={handleIncrement}
style={style}
>
<PlusIcon size={16} aria-hidden="true" />
</button>
</div>
);
};
<button
type="button"
className="w-10 p-2 focus:outline-none border-r-[1px]"
onClick={handleDecrement}
style={style}
>
<MinusIcon size={16} aria-hidden="true" />
</button>
<input
type="text"
value={value}
onChange={handleChange}
onBlur={handleBlur}
className="w-full flex-1 text-center bg-transparent focus:outline-none"
style={style}
min={min}
/>
<button
type="button"
className="w-10 p-2 focus:outline-none border-l-[1px]"
onClick={handleIncrement}
style={style}
>
<PlusIcon size={16} aria-hidden="true" />
</button>
</div>
);
},
);
export default NumberInput;

View File

@ -10,7 +10,7 @@ export const RunTooltip = ({ children }: PropsWithChildren) => {
const { t } = useTranslation();
return (
<Tooltip>
<TooltipTrigger>{children}</TooltipTrigger>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent>
<p>{t('flow.testRun')}</p>
</TooltipContent>

View File

@ -181,4 +181,10 @@ export const FormConfigMap = {
[Operator.ExitLoop]: {
component: () => <></>,
},
[Operator.LoopStart]: {
component: () => <></>,
},
[Operator.ExcelProcessor]: {
component: () => <></>,
},
};

View File

@ -11,17 +11,19 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Input, InputProps } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { BlurTextarea } from '@/components/ui/textarea';
import { useTranslate } from '@/hooks/common-hooks';
import { PlusOutlined } from '@ant-design/icons';
import { useUpdateNodeInternals } from '@xyflow/react';
import humanId from 'human-id';
import trim from 'lodash/trim';
import { ChevronsUpDown, X } from 'lucide-react';
import { ChevronsUpDown, Trash2 } from 'lucide-react';
import {
ChangeEventHandler,
FocusEventHandler,
forwardRef,
memo,
useCallback,
useEffect,
@ -32,7 +34,7 @@ import { v4 as uuid } from 'uuid';
import { z } from 'zod';
import useGraphStore from '../../store';
import DynamicExample from './dynamic-example';
import { useCreateCategorizeFormSchema } from './use-form-schema';
import { CreateCategorizeFormSchema } from './use-form-schema';
interface IProps {
nodeId?: string;
@ -58,12 +60,10 @@ const getOtherFieldValues = (
x !== form.getValues(`${formListName}.${index}.${latestField}`),
);
const InnerNameInput = ({
value,
onChange,
otherNames,
validate,
}: INameInputProps) => {
const InnerNameInput = forwardRef<
HTMLInputElement,
InputProps & INameInputProps
>(function InnerNameInput({ value, onChange, otherNames, validate }, ref) {
const [name, setName] = useState<string | undefined>();
const { t } = useTranslate('flow');
@ -103,9 +103,10 @@ const InnerNameInput = ({
value={name}
onChange={handleNameChange}
onBlur={handleNameBlur}
ref={ref}
></Input>
);
};
});
const NameInput = memo(InnerNameInput);
@ -127,7 +128,6 @@ const InnerFormSet = ({ index }: IProps & { index: number }) => {
name={buildFieldName('name')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('categoryName')}</FormLabel>
<FormControl>
<NameInput
{...field}
@ -174,12 +174,11 @@ const FormSet = memo(InnerFormSet);
const DynamicCategorize = ({ nodeId }: IProps) => {
const updateNodeInternals = useUpdateNodeInternals();
const FormSchema = useCreateCategorizeFormSchema();
const deleteCategorizeCaseEdges = useGraphStore(
(state) => state.deleteEdgesBySourceAndSourceHandle,
);
const form = useFormContext<z.infer<typeof FormSchema>>();
const form = useFormContext<z.infer<CreateCategorizeFormSchema>>();
const { t } = useTranslate('flow');
const { fields, remove, append } = useFieldArray({
name: 'items',
@ -208,41 +207,42 @@ const DynamicCategorize = ({ nodeId }: IProps) => {
);
return (
<div className="flex flex-col gap-4 ">
<section className="flex flex-col gap-4 ">
{fields.map((field, index) => (
<Collapsible key={field.id} defaultOpen>
<div className="flex items-center justify-between space-x-4">
<h4 className="font-bold">
{form.getValues(`items.${index}.name`)}
</h4>
<CollapsibleTrigger asChild>
<div className="flex gap-4">
<Button
variant="ghost"
size="sm"
className="w-9 p-0"
onClick={handleRemove(index)}
>
<X className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</div>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<FormSet nodeId={nodeId} index={index}></FormSet>
</CollapsibleContent>
</Collapsible>
<div key={field.id}>
<Collapsible defaultOpen>
<div className="flex items-center justify-between space-x-4 pb-5">
<span>{form.getValues(`items.${index}.name`)}</span>
<CollapsibleTrigger asChild>
<div className="flex gap-4">
<Button
variant="ghost"
size="sm"
className="w-9 p-0"
onClick={handleRemove(index)}
>
<Trash2 className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</div>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<FormSet nodeId={nodeId} index={index}></FormSet>
</CollapsibleContent>
</Collapsible>
<Separator />
</div>
))}
<Button type={'button'} onClick={handleAdd}>
<PlusOutlined />
{t('addCategory')}
</Button>
</div>
</section>
);
};

View File

@ -1,3 +1,4 @@
import { Collapse } from '@/components/collapse';
import { Button } from '@/components/ui/button';
import {
FormControl,
@ -7,7 +8,7 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { Plus, X } from 'lucide-react';
import { Plus, Trash2 } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@ -24,44 +25,49 @@ const DynamicExample = ({ name }: DynamicExampleProps) => {
});
return (
<FormItem>
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.examples')}</FormLabel>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-start gap-2">
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Textarea {...field}> </Textarea>
</FormControl>
</FormItem>
<Collapse
title={
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.examples')}</FormLabel>
}
>
<FormItem>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-start gap-2">
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Textarea {...field}> </Textarea>
</FormControl>
</FormItem>
)}
/>
{index === 0 ? (
<Button
type="button"
variant={'ghost'}
onClick={() => append({ value: '' })}
>
<Plus />
</Button>
) : (
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<Trash2 />
</Button>
)}
/>
{index === 0 ? (
<Button
type="button"
variant={'ghost'}
onClick={() => append({ value: '' })}
>
<Plus />
</Button>
) : (
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<X />
</Button>
)}
</div>
))}
</div>
<FormMessage />
</FormItem>
</div>
))}
</div>
<FormMessage />
</FormItem>
</Collapse>
);
};

View File

@ -1,7 +1,7 @@
import { FormContainer } from '@/components/form-container';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { Form } from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
@ -33,13 +33,12 @@ function CategorizeForm({ node }: INextOperatorForm) {
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
<LargeModelFormField></LargeModelFormField>
</FormContainer>
<QueryVariable></QueryVariable>
<LargeModelFormField></LargeModelFormField>
<MessageHistoryWindowSizeFormField
min={0}
></MessageHistoryWindowSizeFormField>
<Separator />
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
<Output list={outputList}></Output>
</FormWrapper>

View File

@ -30,3 +30,7 @@ export function useCreateCategorizeFormSchema() {
return FormSchema;
}
export type CreateCategorizeFormSchema = ReturnType<
typeof useCreateCategorizeFormSchema
>;

View File

@ -1,6 +1,6 @@
import { Input } from '@/components/ui/input';
import { Input, InputProps } from '@/components/ui/input';
import { PenLine } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
import { useHandleNameChange } from './use-handle-name-change';
type NameInputProps = {
@ -8,7 +8,10 @@ type NameInputProps = {
onChange: (value: string) => void;
};
export function NameInput({ value, onChange }: NameInputProps) {
export const NameInput = forwardRef<
HTMLInputElement,
InputProps & NameInputProps
>(function NameInput({ value, onChange }, ref) {
const { name, handleNameBlur, handleNameChange } = useHandleNameChange(value);
const inputRef = useRef<HTMLInputElement>(null);
@ -33,7 +36,7 @@ export function NameInput({ value, onChange }: NameInputProps) {
}, [isEditingMode]);
return (
<div className="flex items-center gap-1 flex-1">
<div className="flex items-center gap-1 flex-1" ref={ref}>
{isEditingMode ? (
<Input
ref={inputRef}
@ -52,4 +55,4 @@ export function NameInput({ value, onChange }: NameInputProps) {
)}
</div>
);
}
});