### What problem does this PR solve? Feat: New Agent startup parameters add knowledge base parameter #9194 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.1
| @@ -1,9 +1,13 @@ | |||
| import { DocumentParserType } from '@/constants/knowledge'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | |||
| import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query'; | |||
| import { UserOutlined } from '@ant-design/icons'; | |||
| import { Avatar as AntAvatar, Form, Select, Space } from 'antd'; | |||
| import { toLower } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { RAGFlowAvatar } from './ragflow-avatar'; | |||
| import { FormControl, FormField, FormItem, FormLabel } from './ui/form'; | |||
| import { MultiSelect } from './ui/multi-select'; | |||
| @@ -66,9 +70,13 @@ const KnowledgeBaseItem = ({ | |||
| export default KnowledgeBaseItem; | |||
| export function KnowledgeBaseFormField() { | |||
| export function KnowledgeBaseFormField({ | |||
| showVariable = false, | |||
| }: { | |||
| showVariable?: boolean; | |||
| }) { | |||
| const form = useFormContext(); | |||
| const { t } = useTranslate('chat'); | |||
| const { t } = useTranslation(); | |||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | |||
| @@ -76,6 +84,8 @@ export function KnowledgeBaseFormField() { | |||
| (x) => x.parser_id !== DocumentParserType.Tag, | |||
| ); | |||
| const nextOptions = useBuildQueryVariableOptions(); | |||
| const knowledgeOptions = filteredKnowledgeList.map((x) => ({ | |||
| label: x.name, | |||
| value: x.id, | |||
| @@ -84,18 +94,48 @@ export function KnowledgeBaseFormField() { | |||
| ), | |||
| })); | |||
| const options = useMemo(() => { | |||
| if (showVariable) { | |||
| return [ | |||
| { | |||
| label: t('knowledgeDetails.dataset'), | |||
| options: knowledgeOptions, | |||
| }, | |||
| ...nextOptions.map((x) => { | |||
| return { | |||
| ...x, | |||
| options: x.options | |||
| .filter((y) => toLower(y.type).includes('string')) | |||
| .map((x) => ({ | |||
| ...x, | |||
| icon: () => ( | |||
| <RAGFlowAvatar | |||
| className="size-4 mr-2" | |||
| avatar={x.label} | |||
| name={x.label} | |||
| /> | |||
| ), | |||
| })), | |||
| }; | |||
| }), | |||
| ]; | |||
| } | |||
| return knowledgeOptions; | |||
| }, [knowledgeOptions, nextOptions, showVariable, t]); | |||
| return ( | |||
| <FormField | |||
| control={form.control} | |||
| name="kb_ids" | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel>{t('knowledgeBases')}</FormLabel> | |||
| <FormLabel>{t('chat.knowledgeBases')}</FormLabel> | |||
| <FormControl> | |||
| <MultiSelect | |||
| options={knowledgeOptions} | |||
| options={options} | |||
| onValueChange={field.onChange} | |||
| placeholder={t('knowledgeBasesMessage')} | |||
| placeholder={t('chat.knowledgeBasesMessage')} | |||
| variant="inverted" | |||
| maxCount={100} | |||
| defaultValue={field.value} | |||
| @@ -1,3 +1,4 @@ | |||
| // https://github.com/sersavan/shadcn-multi-select-component | |||
| // src/components/multi-select.tsx | |||
| import { cva, type VariantProps } from 'class-variance-authority'; | |||
| @@ -29,6 +30,51 @@ import { | |||
| import { Separator } from '@/components/ui/separator'; | |||
| import { cn } from '@/lib/utils'; | |||
| export type MultiSelectOptionType = { | |||
| label: React.ReactNode; | |||
| value: string; | |||
| disabled?: boolean; | |||
| icon?: React.ComponentType<{ className?: string }>; | |||
| }; | |||
| export type MultiSelectGroupOptionType = { | |||
| label: React.ReactNode; | |||
| options: MultiSelectOptionType[]; | |||
| }; | |||
| function MultiCommandItem({ | |||
| option, | |||
| isSelected, | |||
| toggleOption, | |||
| }: { | |||
| option: MultiSelectOptionType; | |||
| isSelected: boolean; | |||
| toggleOption(value: string): void; | |||
| }) { | |||
| return ( | |||
| <CommandItem | |||
| key={option.value} | |||
| onSelect={() => toggleOption(option.value)} | |||
| className="cursor-pointer" | |||
| > | |||
| <div | |||
| className={cn( | |||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
| isSelected | |||
| ? 'bg-primary text-primary-foreground' | |||
| : 'opacity-50 [&_svg]:invisible', | |||
| )} | |||
| > | |||
| <CheckIcon className="h-4 w-4" /> | |||
| </div> | |||
| {option.icon && ( | |||
| <option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> | |||
| )} | |||
| <span>{option.label}</span> | |||
| </CommandItem> | |||
| ); | |||
| } | |||
| /** | |||
| * Variants for the multi-select component to handle different styles. | |||
| * Uses class-variance-authority (cva) to define different styles based on "variant" prop. | |||
| @@ -63,14 +109,7 @@ interface MultiSelectProps | |||
| * An array of option objects to be displayed in the multi-select component. | |||
| * Each option object has a label, value, and an optional icon. | |||
| */ | |||
| options: { | |||
| /** The text to display for the option. */ | |||
| label: string; | |||
| /** The unique value associated with the option. */ | |||
| value: string; | |||
| /** Optional icon component to display alongside the option. */ | |||
| icon?: React.ComponentType<{ className?: string }>; | |||
| }[]; | |||
| options: (MultiSelectGroupOptionType | MultiSelectOptionType)[]; | |||
| /** | |||
| * Callback function triggered when the selected values change. | |||
| @@ -144,6 +183,11 @@ export const MultiSelect = React.forwardRef< | |||
| const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); | |||
| const [isAnimating, setIsAnimating] = React.useState(false); | |||
| const flatOptions = React.useMemo(() => { | |||
| return options.flatMap((option) => | |||
| 'options' in option ? option.options : [option], | |||
| ); | |||
| }, [options]); | |||
| const handleInputKeyDown = ( | |||
| event: React.KeyboardEvent<HTMLInputElement>, | |||
| ) => { | |||
| @@ -181,10 +225,10 @@ export const MultiSelect = React.forwardRef< | |||
| }; | |||
| const toggleAll = () => { | |||
| if (selectedValues.length === options.length) { | |||
| if (selectedValues.length === flatOptions.length) { | |||
| handleClear(); | |||
| } else { | |||
| const allValues = options.map((option) => option.value); | |||
| const allValues = flatOptions.map((option) => option.value); | |||
| setSelectedValues(allValues); | |||
| onValueChange(allValues); | |||
| } | |||
| @@ -210,7 +254,7 @@ export const MultiSelect = React.forwardRef< | |||
| <div className="flex justify-between items-center w-full"> | |||
| <div className="flex flex-wrap items-center"> | |||
| {selectedValues?.slice(0, maxCount)?.map((value) => { | |||
| const option = options.find((o) => o.value === value); | |||
| const option = flatOptions.find((o) => o.value === value); | |||
| const IconComponent = option?.icon; | |||
| return ( | |||
| <Badge | |||
| @@ -304,7 +348,7 @@ export const MultiSelect = React.forwardRef< | |||
| <div | |||
| className={cn( | |||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
| selectedValues.length === options.length | |||
| selectedValues.length === flatOptions.length | |||
| ? 'bg-primary text-primary-foreground' | |||
| : 'opacity-50 [&_svg]:invisible', | |||
| )} | |||
| @@ -313,32 +357,38 @@ export const MultiSelect = React.forwardRef< | |||
| </div> | |||
| <span>(Select All)</span> | |||
| </CommandItem> | |||
| {options.map((option) => { | |||
| const isSelected = selectedValues.includes(option.value); | |||
| return ( | |||
| <CommandItem | |||
| key={option.value} | |||
| onSelect={() => toggleOption(option.value)} | |||
| className="cursor-pointer" | |||
| > | |||
| <div | |||
| className={cn( | |||
| 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | |||
| isSelected | |||
| ? 'bg-primary text-primary-foreground' | |||
| : 'opacity-50 [&_svg]:invisible', | |||
| )} | |||
| > | |||
| <CheckIcon className="h-4 w-4" /> | |||
| </div> | |||
| {option.icon && ( | |||
| <option.icon className="mr-2 h-4 w-4 text-muted-foreground" /> | |||
| )} | |||
| <span>{option.label}</span> | |||
| </CommandItem> | |||
| ); | |||
| })} | |||
| {!options.some((x) => 'options' in x) && | |||
| (options as unknown as MultiSelectOptionType[]).map( | |||
| (option) => { | |||
| const isSelected = selectedValues.includes(option.value); | |||
| return ( | |||
| <MultiCommandItem | |||
| option={option} | |||
| key={option.value} | |||
| isSelected={isSelected} | |||
| toggleOption={toggleOption} | |||
| ></MultiCommandItem> | |||
| ); | |||
| }, | |||
| )} | |||
| </CommandGroup> | |||
| {options.every((x) => 'options' in x) && | |||
| options.map((x, idx) => ( | |||
| <CommandGroup heading={x.label} key={idx}> | |||
| {x.options.map((option) => { | |||
| const isSelected = selectedValues.includes(option.value); | |||
| return ( | |||
| <MultiCommandItem | |||
| option={option} | |||
| key={option.value} | |||
| isSelected={isSelected} | |||
| toggleOption={toggleOption} | |||
| ></MultiCommandItem> | |||
| ); | |||
| })} | |||
| </CommandGroup> | |||
| ))} | |||
| <CommandSeparator /> | |||
| <CommandGroup> | |||
| <div className="flex items-center justify-between"> | |||
| @@ -2,11 +2,11 @@ import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | |||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | |||
| import { IRetrievalNode } from '@/interfaces/database/flow'; | |||
| import { NodeProps, Position } from '@xyflow/react'; | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { get } from 'lodash'; | |||
| import { memo, useMemo } from 'react'; | |||
| import { NodeHandleId } from '../../constant'; | |||
| import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query'; | |||
| import { CommonHandle } from './handle'; | |||
| import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; | |||
| import styles from './index.less'; | |||
| @@ -21,6 +21,7 @@ function InnerRetrievalNode({ | |||
| selected, | |||
| }: NodeProps<IRetrievalNode>) { | |||
| const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); | |||
| console.log('🚀 ~ InnerRetrievalNode ~ knowledgeBaseIds:', knowledgeBaseIds); | |||
| const { list: knowledgeList } = useFetchKnowledgeList(true); | |||
| const knowledgeBases = useMemo(() => { | |||
| return knowledgeBaseIds.map((x) => { | |||
| @@ -33,6 +34,8 @@ function InnerRetrievalNode({ | |||
| }); | |||
| }, [knowledgeList, knowledgeBaseIds]); | |||
| const getLabel = useGetVariableLabelByValue(id); | |||
| return ( | |||
| <ToolBar selected={selected} id={id} label={data.label}> | |||
| <NodeWrapper selected={selected}> | |||
| @@ -63,25 +66,27 @@ function InnerRetrievalNode({ | |||
| [styles.nodeHeader]: knowledgeBaseIds.length > 0, | |||
| })} | |||
| ></NodeHeader> | |||
| <Flex vertical gap={8}> | |||
| {knowledgeBases.map((knowledge) => { | |||
| <section className="flex flex-col gap-2"> | |||
| {knowledgeBaseIds.map((id) => { | |||
| const item = knowledgeList.find((y) => id === y.id); | |||
| const label = getLabel(id); | |||
| return ( | |||
| <div className={styles.nodeText} key={knowledge.id}> | |||
| <Flex align={'center'} gap={6}> | |||
| <div className={styles.nodeText} key={id}> | |||
| <div className="flex items-center gap-1.5"> | |||
| <RAGFlowAvatar | |||
| className="size-6 rounded-lg" | |||
| avatar={knowledge.avatar} | |||
| name={knowledge.name || 'CN'} | |||
| avatar={id} | |||
| name={item?.name || (label as string) || 'CN'} | |||
| isPerson={true} | |||
| /> | |||
| <Flex className={styles.knowledgeNodeName} flex={1}> | |||
| {knowledge.name} | |||
| </Flex> | |||
| </Flex> | |||
| <div className={'truncate flex-1'}>{label || item?.name}</div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| })} | |||
| </Flex> | |||
| </section> | |||
| </NodeWrapper> | |||
| </ToolBar> | |||
| ); | |||
| @@ -97,7 +97,7 @@ function RetrievalForm({ node }: INextOperatorForm) { | |||
| <FormWrapper> | |||
| <FormContainer> | |||
| <QueryVariable></QueryVariable> | |||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | |||
| <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | |||
| </FormContainer> | |||
| <Collapse title={<div>Advanced Settings</div>}> | |||
| <FormContainer> | |||
| @@ -43,7 +43,7 @@ const RetrievalForm = () => { | |||
| > | |||
| <FormContainer> | |||
| <DescriptionField></DescriptionField> | |||
| <KnowledgeBaseFormField></KnowledgeBaseFormField> | |||
| <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | |||
| </FormContainer> | |||
| <Collapse title={<div>Advanced Settings</div>}> | |||
| <FormContainer> | |||
| @@ -175,7 +175,7 @@ const ChatContainer = () => { | |||
| ); | |||
| })} | |||
| </div> | |||
| <div ref={ref} /> | |||
| <div ref={ref.scrollRef} /> | |||
| </div> | |||
| <div className="flex w-full justify-center mb-8"> | |||
| <div className="w-5/6"> | |||