### What problem does this PR solve? Feat: Allow chat to use meta data #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.2
| @@ -53,14 +53,14 @@ export default defineConfig({ | |||
| memo.optimization.minimizer('terser').use(TerserPlugin); // Fixed the issue that the page displayed an error after packaging lexical with terser | |||
| memo.plugin('eslint').use(ESLintPlugin, [ | |||
| { | |||
| extensions: ['js', 'ts', 'tsx'], | |||
| failOnError: true, | |||
| exclude: ['**/node_modules/**', '**/mfsu**', '**/mfsu-virtual-entry**'], | |||
| files: ['src/**/*.{js,ts,tsx}'], | |||
| }, | |||
| ]); | |||
| // memo.plugin('eslint').use(ESLintPlugin, [ | |||
| // { | |||
| // extensions: ['js', 'ts', 'tsx'], | |||
| // failOnError: true, | |||
| // exclude: ['**/node_modules/**', '**/mfsu**', '**/mfsu-virtual-entry**'], | |||
| // files: ['src/**/*.{js,ts,tsx}'], | |||
| // }, | |||
| // ]); | |||
| return memo; | |||
| }, | |||
| @@ -4,8 +4,8 @@ import isEqual from 'lodash/isEqual'; | |||
| import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| export const useSetModalState = () => { | |||
| const [visible, setVisible] = useState(false); | |||
| export const useSetModalState = (initialVisible = false) => { | |||
| const [visible, setVisible] = useState(initialVisible); | |||
| const showModal = useCallback(() => { | |||
| setVisible(true); | |||
| @@ -28,6 +28,8 @@ export const enum KnowledgeApiAction { | |||
| DeleteKnowledge = 'deleteKnowledge', | |||
| SaveKnowledge = 'saveKnowledge', | |||
| FetchKnowledgeDetail = 'fetchKnowledgeDetail', | |||
| FetchKnowledgeGraph = 'fetchKnowledgeGraph', | |||
| FetchMetadata = 'fetchMetadata', | |||
| } | |||
| export const useKnowledgeBaseId = (): string => { | |||
| @@ -263,7 +265,7 @@ export function useFetchKnowledgeGraph() { | |||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||
| const { data, isFetching: loading } = useQuery<IKnowledgeGraph>({ | |||
| queryKey: ['fetchKnowledgeGraph', knowledgeBaseId], | |||
| queryKey: [KnowledgeApiAction.FetchKnowledgeGraph, knowledgeBaseId], | |||
| initialData: { graph: {}, mind_map: {} } as IKnowledgeGraph, | |||
| enabled: !!knowledgeBaseId, | |||
| gcTime: 0, | |||
| @@ -275,3 +277,20 @@ export function useFetchKnowledgeGraph() { | |||
| return { data, loading }; | |||
| } | |||
| export function useFetchKnowledgeMetadata(kbIds: string[] = []) { | |||
| const { data, isFetching: loading } = useQuery< | |||
| Record<string, Record<string, string[]>> | |||
| >({ | |||
| queryKey: [KnowledgeApiAction.FetchMetadata, kbIds], | |||
| initialData: {}, | |||
| enabled: kbIds.length > 0, | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await kbService.getMeta({ kb_ids: kbIds.join(',') }); | |||
| return data?.data ?? {}; | |||
| }, | |||
| }); | |||
| return { data, loading }; | |||
| } | |||
| @@ -563,6 +563,9 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s | |||
| crossLanguage: 'Cross-language search', | |||
| crossLanguageTip: `Select one or more languages for cross‑language search. If no language is selected, the system searches with the original query.`, | |||
| createChat: 'Create chat', | |||
| metadata: 'Metadata', | |||
| metadataTip: 'Metadata', | |||
| conditions: 'Conditions', | |||
| }, | |||
| setting: { | |||
| profile: 'Profile', | |||
| @@ -8,10 +8,25 @@ import classNames from 'classnames'; | |||
| import { useCallback } from 'react'; | |||
| import { ISegmentedContentProps } from '../interface'; | |||
| import { DatasetMetadata } from '../constants'; | |||
| import styles from './index.less'; | |||
| import { MetadataFilterConditions } from './metadata-filter-conditions'; | |||
| const emptyResponseField = ['prompt_config', 'empty_response']; | |||
| const MetadataOptions = Object.values(DatasetMetadata).map((x) => { | |||
| let value: DatasetMetadata | boolean = x; | |||
| if (x === DatasetMetadata.Disabled) { | |||
| value = false; | |||
| } else if (x === DatasetMetadata.Automatic) { | |||
| value = true; | |||
| } | |||
| return { | |||
| value, | |||
| label: x, | |||
| }; | |||
| }); | |||
| const AssistantSetting = ({ | |||
| show, | |||
| form, | |||
| @@ -20,6 +35,11 @@ const AssistantSetting = ({ | |||
| const { t } = useTranslate('chat'); | |||
| const { data } = useFetchTenantInfo(true); | |||
| const metadata = Form.useWatch(['meta_data_filter', 'auto'], form); | |||
| const kbIds = Form.useWatch(['kb_ids'], form); | |||
| const hasKnowledge = Array.isArray(kbIds) && kbIds.length > 0; | |||
| const handleChange = useCallback(() => { | |||
| const kbIds = form.getFieldValue('kb_ids'); | |||
| const emptyResponse = form.getFieldValue(emptyResponseField); | |||
| @@ -153,6 +173,24 @@ const AssistantSetting = ({ | |||
| required={false} | |||
| onChange={handleChange} | |||
| ></KnowledgeBaseItem> | |||
| {hasKnowledge && ( | |||
| <Form.Item | |||
| label={t('metadata')} | |||
| name={['meta_data_filter', 'auto']} | |||
| tooltip={t('metadataTip')} | |||
| > | |||
| <Select options={MetadataOptions} /> | |||
| </Form.Item> | |||
| )} | |||
| {hasKnowledge && metadata === DatasetMetadata.Manual && ( | |||
| <Form.Item | |||
| label={t('conditions')} | |||
| tooltip={t('ttsTip')} | |||
| initialValue={false} | |||
| > | |||
| <MetadataFilterConditions kbIds={kbIds}></MetadataFilterConditions> | |||
| </Form.Item> | |||
| )} | |||
| </section> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,84 @@ | |||
| import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request'; | |||
| import { SwitchOperatorOptions } from '@/pages/agent/constant'; | |||
| import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| import { | |||
| Button, | |||
| Dropdown, | |||
| Empty, | |||
| Form, | |||
| FormListOperation, | |||
| Input, | |||
| Select, | |||
| Space, | |||
| } from 'antd'; | |||
| import { useCallback } from 'react'; | |||
| export function MetadataFilterConditions({ kbIds }: { kbIds: string[] }) { | |||
| const metadata = useFetchKnowledgeMetadata(kbIds); | |||
| const renderItems = useCallback( | |||
| (add: FormListOperation['add']) => { | |||
| if (Object.keys(metadata.data).length === 0) { | |||
| return [{ key: 'noData', label: <Empty></Empty> }]; | |||
| } | |||
| return Object.keys(metadata.data).map((key) => { | |||
| return { | |||
| key, | |||
| onClick: () => { | |||
| add({ | |||
| key, | |||
| value: '', | |||
| op: SwitchOperatorOptions[0].value, | |||
| }); | |||
| }, | |||
| label: key, | |||
| }; | |||
| }); | |||
| }, | |||
| [metadata], | |||
| ); | |||
| return ( | |||
| <Form.List name={['meta_data_filter', 'manual']}> | |||
| {(fields, { add, remove }) => ( | |||
| <> | |||
| {fields.map(({ key, name, ...restField }) => ( | |||
| <Space | |||
| key={key} | |||
| style={{ display: 'flex', marginBottom: 8 }} | |||
| align="baseline" | |||
| > | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'key']} | |||
| rules={[{ required: true, message: 'Missing first name' }]} | |||
| > | |||
| <Input placeholder="First Name" /> | |||
| </Form.Item> | |||
| <Form.Item {...restField} name={[name, 'op']} className="w-20"> | |||
| <Select | |||
| options={SwitchOperatorOptions} | |||
| popupMatchSelectWidth={false} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'value']} | |||
| rules={[{ required: true, message: 'Missing last name' }]} | |||
| > | |||
| <Input placeholder="Last Name" /> | |||
| </Form.Item> | |||
| <MinusCircleOutlined onClick={() => remove(name)} /> | |||
| </Space> | |||
| ))} | |||
| <Form.Item> | |||
| <Dropdown trigger={['click']} menu={{ items: renderItems(add) }}> | |||
| <Button type="dashed" block icon={<PlusOutlined />}> | |||
| Add Condition | |||
| </Button> | |||
| </Dropdown> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| ); | |||
| } | |||
| @@ -1 +1,7 @@ | |||
| export const EmptyConversationId = 'empty'; | |||
| export enum DatasetMetadata { | |||
| Disabled = 'disabled', | |||
| Automatic = 'automatic', | |||
| Manual = 'manual', | |||
| } | |||
| @@ -1,5 +1,6 @@ | |||
| import { Button } from '@/components/ui/button'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { PanelRightClose } from 'lucide-react'; | |||
| import { FormProvider, useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import ChatBasicSetting from './chat-basic-settings'; | |||
| @@ -7,7 +8,8 @@ import { ChatModelSettings } from './chat-model-settings'; | |||
| import { ChatPromptEngine } from './chat-prompt-engine'; | |||
| import { useChatSettingSchema } from './use-chat-setting-schema'; | |||
| export function ChatSettings() { | |||
| type ChatSettingsProps = { switchSettingVisible(): void }; | |||
| export function ChatSettings({ switchSettingVisible }: ChatSettingsProps) { | |||
| const formSchema = useChatSettingSchema(); | |||
| const form = useForm<z.infer<typeof formSchema>>({ | |||
| @@ -33,11 +35,18 @@ export function ChatSettings() { | |||
| } | |||
| return ( | |||
| <section className="py-6"> | |||
| <section className="p-5 w-[400px] max-w-[20%]"> | |||
| <div className="flex justify-between items-center text-base"> | |||
| Chat Settings | |||
| <PanelRightClose | |||
| className="size-4 cursor-pointer" | |||
| onClick={switchSettingVisible} | |||
| /> | |||
| </div> | |||
| <FormProvider {...form}> | |||
| <form | |||
| onSubmit={form.handleSubmit(onSubmit)} | |||
| className="space-y-6 overflow-auto max-h-[88vh] pr-4" | |||
| className="space-y-6 overflow-auto max-h-[87vh] pr-4" | |||
| > | |||
| <ChatBasicSetting></ChatBasicSetting> | |||
| <ChatPromptEngine></ChatPromptEngine> | |||
| @@ -23,7 +23,7 @@ interface IProps { | |||
| export function ChatBox({ controller }: IProps) { | |||
| const { | |||
| value, | |||
| scrollRef, | |||
| // scrollRef, | |||
| messageContainerRef, | |||
| sendLoading, | |||
| derivedMessages, | |||
| @@ -43,8 +43,8 @@ export function ChatBox({ controller }: IProps) { | |||
| const sendDisabled = useSendButtonDisabled(value); | |||
| return ( | |||
| <section className="border-x flex flex-col p-5 w-full"> | |||
| <div ref={messageContainerRef} className="flex-1 overflow-auto"> | |||
| <section className="border-x flex flex-col p-5 flex-1 min-w-0"> | |||
| <div ref={messageContainerRef} className="flex-1 overflow-auto min-h-0"> | |||
| <div className="w-full"> | |||
| {derivedMessages?.map((message, i) => { | |||
| return ( | |||
| @@ -75,7 +75,7 @@ export function ChatBox({ controller }: IProps) { | |||
| ); | |||
| })} | |||
| </div> | |||
| <div ref={scrollRef} /> | |||
| {/* <div ref={scrollRef} /> */} | |||
| </div> | |||
| <NextMessageInput | |||
| disabled={disabled} | |||
| @@ -7,10 +7,12 @@ import { | |||
| BreadcrumbPage, | |||
| BreadcrumbSeparator, | |||
| } from '@/components/ui/breadcrumb'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||
| import { useFetchDialog } from '@/hooks/use-chat-request'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useHandleClickConversationCard } from '../hooks/use-click-card'; | |||
| import { ChatSettings } from './app-settings/chat-settings'; | |||
| import { ChatBox } from './chat-box'; | |||
| import { Sessions } from './sessions'; | |||
| @@ -20,6 +22,8 @@ export default function Chat() { | |||
| const { t } = useTranslation(); | |||
| const { handleConversationCardClick, controller } = | |||
| useHandleClickConversationCard(); | |||
| const { visible: settingVisible, switchVisible: switchSettingVisible } = | |||
| useSetModalState(true); | |||
| return ( | |||
| <section className="h-full flex flex-col"> | |||
| @@ -39,10 +43,18 @@ export default function Chat() { | |||
| </Breadcrumb> | |||
| </PageHeader> | |||
| <div className="flex flex-1 min-h-0"> | |||
| <Sessions | |||
| handleConversationCardClick={handleConversationCardClick} | |||
| ></Sessions> | |||
| <ChatBox controller={controller}></ChatBox> | |||
| <div className="flex flex-1 min-w-0"> | |||
| <Sessions | |||
| handleConversationCardClick={handleConversationCardClick} | |||
| switchSettingVisible={switchSettingVisible} | |||
| ></Sessions> | |||
| <ChatBox controller={controller}></ChatBox> | |||
| </div> | |||
| {settingVisible && ( | |||
| <ChatSettings | |||
| switchSettingVisible={switchSettingVisible} | |||
| ></ChatSettings> | |||
| )} | |||
| </div> | |||
| </section> | |||
| ); | |||
| @@ -1,21 +1,30 @@ | |||
| import { MoreButton } from '@/components/more-button'; | |||
| import { RAGFlowAvatar } from '@/components/ragflow-avatar'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { useGetChatSearchParams } from '@/hooks/use-chat-request'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { | |||
| useFetchDialog, | |||
| useGetChatSearchParams, | |||
| } from '@/hooks/use-chat-request'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Plus } from 'lucide-react'; | |||
| import { PanelLeftClose, PanelRightClose, Plus } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { useHandleClickConversationCard } from '../hooks/use-click-card'; | |||
| import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list'; | |||
| import { ChatSettingSheet } from './app-settings/chat-settings-sheet'; | |||
| type SessionProps = Pick< | |||
| ReturnType<typeof useHandleClickConversationCard>, | |||
| 'handleConversationCardClick' | |||
| >; | |||
| export function Sessions({ handleConversationCardClick }: SessionProps) { | |||
| > & { switchSettingVisible(): void }; | |||
| export function Sessions({ | |||
| handleConversationCardClick, | |||
| switchSettingVisible, | |||
| }: SessionProps) { | |||
| const { list: conversationList, addTemporaryConversation } = | |||
| useSelectDerivedConversationList(); | |||
| const { data } = useFetchDialog(); | |||
| const { visible, switchVisible } = useSetModalState(true); | |||
| const handleCardClick = useCallback( | |||
| (conversationId: string, isNew: boolean) => () => { | |||
| @@ -26,9 +35,32 @@ export function Sessions({ handleConversationCardClick }: SessionProps) { | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| if (!visible) { | |||
| return ( | |||
| <PanelRightClose | |||
| className="cursor-pointer size-4 mt-8" | |||
| onClick={switchVisible} | |||
| /> | |||
| ); | |||
| } | |||
| return ( | |||
| <section className="p-6 w-[400px] max-w-[20%] flex flex-col"> | |||
| <div className="flex justify-between items-center mb-4"> | |||
| <section className="flex items-center text-base justify-between gap-2"> | |||
| <div className="flex gap-3 items-center min-w-0"> | |||
| <RAGFlowAvatar | |||
| avatar={data.icon} | |||
| name={data.name} | |||
| className="size-8" | |||
| ></RAGFlowAvatar> | |||
| <span className="flex-1 truncate">{data.name}</span> | |||
| </div> | |||
| <PanelLeftClose | |||
| className="cursor-pointer size-4" | |||
| onClick={switchVisible} | |||
| /> | |||
| </section> | |||
| <div className="flex justify-between items-center mb-4 pt-10"> | |||
| <span className="text-xl font-bold">Conversations</span> | |||
| <Button variant={'ghost'} onClick={addTemporaryConversation}> | |||
| <Plus></Plus> | |||
| @@ -51,9 +83,9 @@ export function Sessions({ handleConversationCardClick }: SessionProps) { | |||
| ))} | |||
| </div> | |||
| <div className="py-2"> | |||
| <ChatSettingSheet> | |||
| <Button className="w-full">Chat Settings</Button> | |||
| </ChatSettingSheet> | |||
| <Button className="w-full" onClick={switchSettingVisible}> | |||
| Chat Settings | |||
| </Button> | |||
| </div> | |||
| </section> | |||
| ); | |||
| @@ -37,6 +37,7 @@ const { | |||
| upload_and_parse, | |||
| listTagByKnowledgeIds, | |||
| setMeta, | |||
| getMeta, | |||
| } = api; | |||
| const methods = { | |||
| @@ -159,6 +160,10 @@ const methods = { | |||
| url: api.get_dataset_filter, | |||
| method: 'post', | |||
| }, | |||
| getMeta: { | |||
| url: getMeta, | |||
| method: 'get', | |||
| }, | |||
| }; | |||
| const kbService = registerServer<keyof typeof methods>(methods, request); | |||
| @@ -44,6 +44,7 @@ export default { | |||
| get_kb_detail: `${api_host}/kb/detail`, | |||
| getKnowledgeGraph: (knowledgeId: string) => | |||
| `${api_host}/kb/${knowledgeId}/knowledge_graph`, | |||
| getMeta: `${api_host}/kb/get_meta`, | |||
| // tags | |||
| listTag: (knowledgeId: string) => `${api_host}/kb/${knowledgeId}/tags`, | |||