mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-21 15:11:08 +08:00
### What problem does this PR solve? Closes #14674. This PR improves RAPTOR configuration and tree construction while preserving the existing RAPTOR behavior as the default. RAPTOR currently builds summary layers with the original UMAP + GMM clustering path. This PR keeps that default path, and adds: - A hidden backend tree-builder option: - `tree_builder="raptor"`: default, existing RAPTOR behavior. - `tree_builder="psi"`: rank-aware Psi-style tree builder using original embedding-space cosine ranking. - A user-facing clustering method option for the default RAPTOR builder: - `clustering_method="gmm"`: existing default. - `clustering_method="ahc"`: agglomerative hierarchical clustering path. - A RAPTOR UI setting for `Clustering method` and `Max cluster`. ### What changed #### Backend - Added `tree_builder` support for RAPTOR/Psi. - Added `clustering_method` support for GMM/AHC. - Kept existing RAPTOR + GMM as the default. - Added Psi tree building from original-space cosine similarity. - Added bucketed Psi building controls for large inputs: - `raptor.ext.psi_exact_max_leaves` - `raptor.ext.psi_bucket_size` - Added method-aware RAPTOR summary metadata using existing `extra.raptor_method`. - Avoided adding a dedicated DB schema field for experimental method tracking. - Added cleanup/migration logic to avoid mixing stale RAPTOR summary trees. - Added defensive checks for Psi tree construction and summary failures. #### Frontend/UI - Added `Clustering method` in RAPTOR settings with `GMM` and `AHC`. - Added/kept `Max cluster` in RAPTOR settings. - Enlarged max cluster UI limit to `1024`, matching backend validation. - Kept AHC editable even when a RAPTOR task has already finished. - Fixed the UI save payload so `clustering_method` and `tree_builder` are serialized through `parser_config.raptor.ext`, avoiding backend validation errors for extra top-level RAPTOR fields. Example saved RAPTOR config: ```json { "raptor": { "max_cluster": 317, "ext": { "clustering_method": "ahc", "tree_builder": "raptor" } } } Co-authored-by: CaptainTimon <CaptainTimon@users.noreply.github.com>
176 lines
4.2 KiB
TypeScript
176 lines
4.2 KiB
TypeScript
import { cn } from '@/lib/utils';
|
|
import React, { useContext, useState } from 'react';
|
|
|
|
const RadioGroupContext = React.createContext<{
|
|
name?: string;
|
|
value: string | number;
|
|
onChange: (value: string | number) => void;
|
|
disabled?: boolean;
|
|
} | null>(null);
|
|
|
|
type RadioProps = {
|
|
value: string | number;
|
|
checked?: boolean;
|
|
disabled?: boolean;
|
|
onChange?: (checked: boolean) => void;
|
|
testId?: string;
|
|
children?: React.ReactNode;
|
|
} & Omit<
|
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
'value' | 'checked' | 'onChange'
|
|
>;
|
|
|
|
function Radio({
|
|
className,
|
|
value,
|
|
checked,
|
|
disabled,
|
|
onChange,
|
|
testId,
|
|
children,
|
|
...props
|
|
}: RadioProps) {
|
|
const groupContext = useContext(RadioGroupContext);
|
|
const isControlled = checked !== undefined;
|
|
// const [internalChecked, setInternalChecked] = useState(false);
|
|
|
|
const isChecked = isControlled ? checked : groupContext?.value === value;
|
|
const mergedDisabled = disabled || groupContext?.disabled;
|
|
|
|
const handleChange = () => {
|
|
if (mergedDisabled) return;
|
|
|
|
// if (!isControlled) {
|
|
// setInternalChecked(!isChecked);
|
|
// }
|
|
|
|
if (onChange) {
|
|
onChange(!isChecked);
|
|
}
|
|
|
|
if (groupContext && !groupContext.disabled) {
|
|
groupContext.onChange(value);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<label
|
|
className={cn(
|
|
'group/radio relative flex items-center cursor-pointer gap-2 text-sm',
|
|
mergedDisabled && 'cursor-not-allowed opacity-50',
|
|
)}
|
|
>
|
|
<input
|
|
type="radio"
|
|
value={value}
|
|
checked={isChecked}
|
|
onChange={handleChange}
|
|
disabled={mergedDisabled}
|
|
className={cn('peer absolute size-[1px] opacity-0', className)}
|
|
data-testid={testId}
|
|
{...props}
|
|
name={groupContext?.name}
|
|
/>
|
|
|
|
<div
|
|
className={cn(
|
|
'flex h-4 w-4 items-center justify-center rounded-full text-border-button border border-current transition-colors',
|
|
'group-hover/radio:text-border-default hover:text-border-default',
|
|
'peer-focus:text-text-primary',
|
|
isChecked && 'border-primary bg-primary/10',
|
|
mergedDisabled && 'border-muted',
|
|
)}
|
|
>
|
|
<div
|
|
className={cn(
|
|
'h-2 w-2 fill-primary text-primary bg-text-primary rounded-full opacity-0 scale-0 transition-all',
|
|
isChecked && 'opacity-100 scale-100',
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{children && <span className="text-foreground">{children}</span>}
|
|
</label>
|
|
);
|
|
}
|
|
|
|
type RadioGroupProps = {
|
|
name?: string;
|
|
value?: string | number;
|
|
defaultValue?: string | number;
|
|
onChange?: (value: string | number) => void;
|
|
disabled?: boolean;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
direction?: 'horizontal' | 'vertical';
|
|
};
|
|
|
|
const Group = React.forwardRef<HTMLDivElement, RadioGroupProps>(
|
|
(
|
|
{
|
|
name,
|
|
value,
|
|
defaultValue,
|
|
onChange,
|
|
disabled,
|
|
children,
|
|
className,
|
|
direction = 'horizontal',
|
|
},
|
|
ref,
|
|
) => {
|
|
const [internalValue, setInternalValue] = useState(defaultValue || '');
|
|
|
|
const isControlled = value !== undefined;
|
|
const mergedValue = isControlled ? value : internalValue;
|
|
|
|
const handleChange = (val: string | number) => {
|
|
if (disabled) return;
|
|
|
|
if (!isControlled) {
|
|
setInternalValue(val);
|
|
}
|
|
|
|
if (onChange) {
|
|
onChange(val);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<RadioGroupContext.Provider
|
|
value={{
|
|
name,
|
|
value: mergedValue,
|
|
onChange: handleChange,
|
|
disabled,
|
|
}}
|
|
>
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'flex gap-4',
|
|
direction === 'vertical' ? 'flex-col' : 'flex-row',
|
|
className,
|
|
)}
|
|
>
|
|
{React.Children.map(children, (child) => {
|
|
if (!React.isValidElement<RadioProps>(child)) {
|
|
return child;
|
|
}
|
|
return React.cloneElement(child, {
|
|
disabled: disabled || child.props.disabled,
|
|
});
|
|
})}
|
|
</div>
|
|
</RadioGroupContext.Provider>
|
|
);
|
|
},
|
|
);
|
|
|
|
const RadioComponent = Object.assign(Radio, {
|
|
Group,
|
|
});
|
|
|
|
Group.displayName = 'RadioGroup';
|
|
export { RadioComponent as Radio };
|