diff --git a/web/src/assets/svg/home-icon/memory.svg b/web/src/assets/svg/home-icon/memory.svg new file mode 100644 index 000000000..f50d755f4 --- /dev/null +++ b/web/src/assets/svg/home-icon/memory.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index ca0b08763..2fdc55af0 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -1,6 +1,13 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'; import { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react'; +import { + ControllerRenderProps, DefaultValues, FieldValues, SubmitHandler, @@ -26,6 +33,7 @@ import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import { t } from 'i18next'; import { Loader } from 'lucide-react'; +import { MultiSelect, MultiSelectOptionType } from './ui/multi-select'; // Field type enumeration export enum FormFieldType { @@ -35,14 +43,17 @@ export enum FormFieldType { Number = 'number', Textarea = 'textarea', Select = 'select', + MultiSelect = 'multi-select', Checkbox = 'checkbox', Tag = 'tag', + Custom = 'custom', } // Field configuration interface export interface FormFieldConfig { name: string; label: string; + hideLabel?: boolean; type: FormFieldType; hidden?: boolean; required?: boolean; @@ -57,7 +68,7 @@ export interface FormFieldConfig { max?: number; message?: string; }; - render?: (fieldProps: any) => React.ReactNode; + render?: (fieldProps: ControllerRenderProps) => React.ReactNode; horizontal?: boolean; onChange?: (value: any) => void; tooltip?: React.ReactNode; @@ -78,10 +89,10 @@ interface DynamicFormProps { className?: string; children?: React.ReactNode; defaultValues?: DefaultValues; - onFieldUpdate?: ( - fieldName: string, - updatedField: Partial, - ) => void; + // onFieldUpdate?: ( + // fieldName: string, + // updatedField: Partial, + // ) => void; labelClassName?: string; } @@ -92,6 +103,10 @@ export interface DynamicFormRef { reset: (values?: any) => void; watch: (field: string, callback: (value: any) => void) => () => void; updateFieldType: (fieldName: string, newType: FormFieldType) => void; + onFieldUpdate: ( + fieldName: string, + newFieldProperties: Partial, + ) => void; } // Generate Zod validation schema based on field configurations @@ -110,6 +125,14 @@ const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { case FormFieldType.Email: fieldSchema = z.string().email('Please enter a valid email address'); break; + case FormFieldType.MultiSelect: + fieldSchema = z.array(z.string()).optional(); + if (field.required) { + fieldSchema = z.array(z.string()).min(1, { + message: `${field.label} is required`, + }); + } + break; case FormFieldType.Number: fieldSchema = z.coerce.number(); if (field.validation?.min !== undefined) { @@ -275,7 +298,10 @@ const generateDefaultValues = ( defaultValues[field.name] = field.defaultValue; } else if (field.type === FormFieldType.Checkbox) { defaultValues[field.name] = false; - } else if (field.type === FormFieldType.Tag) { + } else if ( + field.type === FormFieldType.Tag || + field.type === FormFieldType.MultiSelect + ) { defaultValues[field.name] = []; } else { defaultValues[field.name] = ''; @@ -291,17 +317,21 @@ const DynamicForm = { Root: forwardRef( ( { - fields, + fields: originFields, onSubmit, className = '', children, defaultValues: formDefaultValues = {} as DefaultValues, - onFieldUpdate, + // onFieldUpdate, labelClassName, }: DynamicFormProps, ref: React.Ref, ) => { // Generate validation schema and default values + const [fields, setFields] = useState(originFields); + useMemo(() => { + setFields(originFields); + }, [originFields]); const schema = useMemo(() => generateSchema(fields), [fields]); const defaultValues = useMemo(() => { @@ -406,43 +436,54 @@ const DynamicForm = { }, [fields, form]); // Expose form methods via ref - useImperativeHandle(ref, () => ({ - submit: () => form.handleSubmit(onSubmit)(), - getValues: () => form.getValues(), - reset: (values?: T) => { - if (values) { - form.reset(values); - } else { - form.reset(); - } - }, - setError: form.setError, - clearErrors: form.clearErrors, - trigger: form.trigger, - watch: (field: string, callback: (value: any) => void) => { - const { unsubscribe } = form.watch((values: any) => { - if (values && values[field] !== undefined) { - callback(values[field]); - } - }); - return unsubscribe; - }, - - onFieldUpdate: ( - fieldName: string, - updatedField: Partial, - ) => { - setTimeout(() => { - if (onFieldUpdate) { - onFieldUpdate(fieldName, updatedField); + useImperativeHandle( + ref, + () => ({ + submit: () => form.handleSubmit(onSubmit)(), + getValues: () => form.getValues(), + reset: (values?: T) => { + if (values) { + form.reset(values); } else { - console.warn( - 'onFieldUpdate prop is not provided. Cannot update field type.', - ); + form.reset(); } - }, 0); - }, - })); + }, + setError: form.setError, + clearErrors: form.clearErrors, + trigger: form.trigger, + watch: (field: string, callback: (value: any) => void) => { + const { unsubscribe } = form.watch((values: any) => { + if (values && values[field] !== undefined) { + callback(values[field]); + } + }); + return unsubscribe; + }, + + onFieldUpdate: ( + fieldName: string, + updatedField: Partial, + ) => { + setFields((prevFields: any) => + prevFields.map((field: any) => + field.name === fieldName + ? { ...field, ...updatedField } + : field, + ), + ); + // setTimeout(() => { + // if (onFieldUpdate) { + // onFieldUpdate(fieldName, updatedField); + // } else { + // console.warn( + // 'onFieldUpdate prop is not provided. Cannot update field type.', + // ); + // } + // }, 0); + }, + }), + [form], + ); useEffect(() => { if (formDefaultValues && Object.keys(formDefaultValues).length > 0) { @@ -459,6 +500,9 @@ const DynamicForm = { // Render form fields const renderField = (field: FormFieldConfig) => { if (field.render) { + if (field.type === FormFieldType.Custom && field.hideLabel) { + return
{field.render({})}
; + } return ( ); + case FormFieldType.MultiSelect: + return ( + + {(fieldProps) => { + console.log('multi select value', fieldProps); + const finalFieldProps = { + ...fieldProps, + onValueChange: (value: string[]) => { + if (fieldProps.onChange) { + fieldProps.onChange(value); + } + field.onChange?.(value); + }, + }; + return ( + { + // console.log(data); + // field.onChange?.(data); + // }} + options={field.options as MultiSelectOptionType[]} + /> + ); + }} + + ); + case FormFieldType.Checkbox: return ( , title: t('empty.agentTitle'), + notFound: t('empty.notFoundAgent'), }, [EmptyCardType.Dataset]: { icon: , title: t('empty.datasetTitle'), + notFound: t('empty.notFoundDataset'), }, [EmptyCardType.Chat]: { icon: , title: t('empty.chatTitle'), + notFound: t('empty.notFoundChat'), }, [EmptyCardType.Search]: { icon: , title: t('empty.searchTitle'), + notFound: t('empty.notFoundSearch'), + }, + [EmptyCardType.Memory]: { + icon: , + title: t('empty.memoryTitle'), + notFound: t('empty.notFoundMemory'), }, }; diff --git a/web/src/components/empty/empty.tsx b/web/src/components/empty/empty.tsx index abf28dd3a..3623f43d0 100644 --- a/web/src/components/empty/empty.tsx +++ b/web/src/components/empty/empty.tsx @@ -76,9 +76,10 @@ export const EmptyAppCard = (props: { onClick?: () => void; showIcon?: boolean; className?: string; + isSearch?: boolean; size?: 'small' | 'large'; }) => { - const { type, showIcon, className } = props; + const { type, showIcon, className, isSearch } = props; let defaultClass = ''; let style = {}; switch (props.size) { @@ -95,19 +96,29 @@ export const EmptyAppCard = (props: { break; } return ( -
+
-
- -
+ {!isSearch && ( +
+ +
+ )}
); diff --git a/web/src/components/llm-setting-items/llm-form-field.tsx b/web/src/components/llm-setting-items/llm-form-field.tsx index 594c17df4..b4106ed0e 100644 --- a/web/src/components/llm-setting-items/llm-form-field.tsx +++ b/web/src/components/llm-setting-items/llm-form-field.tsx @@ -9,13 +9,19 @@ export type LLMFormFieldProps = { name?: string; }; -export function LLMFormField({ options, name }: LLMFormFieldProps) { - const { t } = useTranslation(); - +export const useModelOptions = () => { const modelOptions = useComposeLlmOptionsByModelTypes([ LlmModelType.Chat, LlmModelType.Image2text, ]); + return { + modelOptions, + }; +}; + +export function LLMFormField({ options, name }: LLMFormFieldProps) { + const { t } = useTranslation(); + const { modelOptions } = useModelOptions(); return ( diff --git a/web/src/components/ragflow-form.tsx b/web/src/components/ragflow-form.tsx index c59776824..5f21980b0 100644 --- a/web/src/components/ragflow-form.tsx +++ b/web/src/components/ragflow-form.tsx @@ -53,14 +53,16 @@ export function RAGFlowFormItem({ {label} )} - - {typeof children === 'function' - ? children(field) - : isValidElement(children) - ? cloneElement(children, { ...field }) - : children} - - +
+ + {typeof children === 'function' + ? children(field) + : isValidElement(children) + ? cloneElement(children, { ...field }) + : children} + + +
)} /> diff --git a/web/src/constants/llm.ts b/web/src/constants/llm.ts index c7757f805..a5f5e4b82 100644 --- a/web/src/constants/llm.ts +++ b/web/src/constants/llm.ts @@ -126,3 +126,53 @@ export const IconMap = { [LLMFactory.JiekouAI]: 'jiekouai', [LLMFactory.Builtin]: 'builtin', }; + +export const APIMapUrl = { + [LLMFactory.OpenAI]: 'https://platform.openai.com/api-keys', + [LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys', + [LLMFactory.Gemini]: 'https://aistudio.google.com/app/apikey', + [LLMFactory.DeepSeek]: 'https://platform.deepseek.com/api_keys', + [LLMFactory.Moonshot]: 'https://platform.moonshot.cn/console/api-keys', + [LLMFactory.TongYiQianWen]: 'https://dashscope.console.aliyun.com/apiKey', + [LLMFactory.ZhipuAI]: 'https://open.bigmodel.cn/usercenter/apikeys', + [LLMFactory.XAI]: 'https://x.ai/api/', + [LLMFactory.HuggingFace]: 'https://huggingface.co/settings/tokens', + [LLMFactory.Mistral]: 'https://console.mistral.ai/api-keys/', + [LLMFactory.Cohere]: 'https://dashboard.cohere.com/api-keys', + [LLMFactory.BaiduYiYan]: 'https://wenxin.baidu.com/user/key', + [LLMFactory.Meituan]: 'https://longcat.chat/platform/api_keys', + [LLMFactory.Bedrock]: + 'https://us-east-2.console.aws.amazon.com/bedrock/home#/api-keys', + [LLMFactory.AzureOpenAI]: + 'https://portal.azure.com/#create/Microsoft.CognitiveServicesOpenAI', + [LLMFactory.OpenRouter]: 'https://openrouter.ai/keys', + [LLMFactory.XunFeiSpark]: 'https://console.xfyun.cn/services/cbm', + [LLMFactory.MiniMax]: + 'https://platform.minimaxi.com/user-center/basic-information', + [LLMFactory.Groq]: 'https://console.groq.com/keys', + [LLMFactory.NVIDIA]: 'https://build.nvidia.com/settings/api-keys', + [LLMFactory.SILICONFLOW]: 'https://cloud.siliconflow.cn/account/ak', + [LLMFactory.Replicate]: 'https://replicate.com/account/api-tokens', + [LLMFactory.VolcEngine]: 'https://console.volcengine.com/ark', + [LLMFactory.Jina]: 'https://jina.ai/embeddings/', + [LLMFactory.TencentHunYuan]: + 'https://console.cloud.tencent.com/hunyuan/api-key', + [LLMFactory.TencentCloud]: 'https://console.cloud.tencent.com/cam/capi', + [LLMFactory.ModelScope]: 'https://modelscope.cn/my/myaccesstoken', + [LLMFactory.GoogleCloud]: 'https://console.cloud.google.com/apis/credentials', + [LLMFactory.FishAudio]: 'https://fish.audio/app/api-keys/', + [LLMFactory.GiteeAI]: + 'https://ai.gitee.com/hhxzgrjn/dashboard/settings/tokens', + [LLMFactory.StepFun]: 'https://platform.stepfun.com/interface-key', + [LLMFactory.BaiChuan]: 'https://platform.baichuan-ai.com/console/apikey', + [LLMFactory.PPIO]: 'https://ppio.com/settings/key-management', + [LLMFactory.VoyageAI]: 'https://dash.voyageai.com/api-keys', + [LLMFactory.TogetherAI]: 'https://api.together.xyz/settings/api-keys', + [LLMFactory.NovitaAI]: 'https://novita.ai/dashboard/key', + [LLMFactory.Upstage]: 'https://console.upstage.ai/api-keys', + [LLMFactory.CometAPI]: 'https://api.cometapi.com/console/token', + [LLMFactory.Ai302]: 'https://302.ai/apis/list', + [LLMFactory.DeerAPI]: 'https://api.deerapi.com/token', + [LLMFactory.TokenPony]: 'https://www.tokenpony.cn/#/user/keys', + [LLMFactory.DeepInfra]: 'https://deepinfra.com/dash/api_keys', +}; diff --git a/web/src/hooks/logic-hooks.ts b/web/src/hooks/logic-hooks.ts index 73b389fd7..4fa4ef218 100644 --- a/web/src/hooks/logic-hooks.ts +++ b/web/src/hooks/logic-hooks.ts @@ -1,6 +1,7 @@ import { Authorization } from '@/constants/authorization'; import { MessageType } from '@/constants/chat'; import { LanguageTranslationMap } from '@/constants/common'; +import { Pagination } from '@/interfaces/common'; import { ResponseType } from '@/interfaces/database/base'; import { IAnswer, @@ -12,7 +13,7 @@ import { IKnowledgeFile } from '@/interfaces/database/knowledge'; import api from '@/utils/api'; import { getAuthorization } from '@/utils/authorization-util'; import { buildMessageUuid } from '@/utils/chat'; -import { PaginationProps, message } from 'antd'; +import { message } from 'antd'; import { FormInstance } from 'antd/lib'; import axios from 'axios'; import { EventSourceParserStream } from 'eventsource-parser/stream'; @@ -71,8 +72,8 @@ export const useGetPaginationWithRouter = () => { size: pageSize, } = useSetPaginationParams(); - const onPageChange: PaginationProps['onChange'] = useCallback( - (pageNumber: number, pageSize: number) => { + const onPageChange: Pagination['onChange'] = useCallback( + (pageNumber: number, pageSize?: number) => { setPaginationParams(pageNumber, pageSize); }, [setPaginationParams], @@ -88,7 +89,7 @@ export const useGetPaginationWithRouter = () => { [setPaginationParams, pageSize], ); - const pagination: PaginationProps = useMemo(() => { + const pagination: Pagination = useMemo(() => { return { showQuickJumper: true, total: 0, @@ -97,7 +98,7 @@ export const useGetPaginationWithRouter = () => { pageSize: pageSize, pageSizeOptions: [1, 2, 10, 20, 50, 100], onChange: onPageChange, - showTotal: (total) => `${t('total')} ${total}`, + showTotal: (total: number) => `${t('total')} ${total}`, }; }, [t, onPageChange, page, pageSize]); @@ -109,7 +110,7 @@ export const useGetPaginationWithRouter = () => { export const useHandleSearchChange = () => { const [searchString, setSearchString] = useState(''); - const { setPagination } = useGetPaginationWithRouter(); + const { pagination, setPagination } = useGetPaginationWithRouter(); const handleInputChange = useCallback( (e: React.ChangeEvent) => { const value = e.target.value; @@ -119,21 +120,21 @@ export const useHandleSearchChange = () => { [setPagination], ); - return { handleInputChange, searchString }; + return { handleInputChange, searchString, pagination, setPagination }; }; export const useGetPagination = () => { const [pagination, setPagination] = useState({ page: 1, pageSize: 10 }); const { t } = useTranslate('common'); - const onPageChange: PaginationProps['onChange'] = useCallback( + const onPageChange: Pagination['onChange'] = useCallback( (pageNumber: number, pageSize: number) => { setPagination({ page: pageNumber, pageSize }); }, [], ); - const currentPagination: PaginationProps = useMemo(() => { + const currentPagination: Pagination = useMemo(() => { return { showQuickJumper: true, total: 0, @@ -142,7 +143,7 @@ export const useGetPagination = () => { pageSize: pagination.pageSize, pageSizeOptions: [1, 2, 10, 20, 50, 100], onChange: onPageChange, - showTotal: (total) => `${t('total')} ${total}`, + showTotal: (total: number) => `${t('total')} ${total}`, }; }, [t, onPageChange, pagination]); diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts index 2f4f770e0..3fd62b689 100644 --- a/web/src/hooks/logic-hooks/navigate-hooks.ts +++ b/web/src/hooks/logic-hooks/navigate-hooks.ts @@ -25,6 +25,17 @@ export const useNavigatePage = () => { [navigate], ); + const navigateToMemoryList = useCallback( + ({ isCreate = false }: { isCreate?: boolean }) => { + if (isCreate) { + navigate(Routes.Memories + '?isCreate=true'); + } else { + navigate(Routes.Memories); + } + }, + [navigate], + ); + const navigateToDataset = useCallback( (id: string) => () => { // navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`); @@ -105,6 +116,12 @@ export const useNavigatePage = () => { }, [navigate], ); + const navigateToMemory = useCallback( + (id: string) => () => { + navigate(`${Routes.Memory}${Routes.MemoryMessage}/${id}`); + }, + [navigate], + ); const navigateToChunkParsedResult = useCallback( (id: string, knowledgeId?: string) => () => { @@ -196,5 +213,7 @@ export const useNavigatePage = () => { navigateToDataflowResult, navigateToDataFile, navigateToDataSourceDetail, + navigateToMemory, + navigateToMemoryList, }; }; diff --git a/web/src/interfaces/common.ts b/web/src/interfaces/common.ts index 21553d653..771ff5aa6 100644 --- a/web/src/interfaces/common.ts +++ b/web/src/interfaces/common.ts @@ -2,6 +2,7 @@ export interface Pagination { current: number; pageSize: number; total: number; + onChange?: (page: number, pageSize: number) => void; } export interface BaseState { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 479cafb64..215d8b9f4 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -99,6 +99,29 @@ export default { search: 'Search', welcome: 'Welcome to', dataset: 'Dataset', + Memories: 'Memory', + }, + memory: { + memory: 'Memory', + createMemory: 'Create Memory', + name: 'Name', + memoryNamePlaceholder: 'memory name', + memoryType: 'Memory type', + embeddingModel: 'Embedding model', + selectModel: 'Select model', + llm: 'LLM', + }, + memoryDetail: { + messages: { + sessionId: 'Session ID', + agent: 'Agent', + type: 'Type', + validDate: 'Valid date', + forgetAt: 'Forget at', + source: 'Source', + enable: 'Enable', + action: 'Action', + }, }, knowledgeList: { welcome: 'Welcome back', @@ -2044,14 +2067,21 @@ Important structured information may include: names, dates, locations, events, k delFilesContent: 'Selected {{count}} files', delChat: 'Delete chat', delMember: 'Delete member', + delMemory: 'Delete memory', }, empty: { noMCP: 'No MCP servers available', agentTitle: 'No agent app created yet', + notFoundAgent: 'Agent app not found', datasetTitle: 'No dataset created yet', + notFoundDataset: 'Dataset not found', chatTitle: 'No chat app created yet', + notFoundChat: 'Chat app not found', searchTitle: 'No search app created yet', + notFoundSearch: 'Search app not found', + memoryTitle: 'No memory created yet', + notFoundMemory: 'Memory not found', addNow: 'Add Now', }, diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 4179557c3..5d114594c 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1900,9 +1900,15 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`, empty: { noMCP: '暂无 MCP 服务器可用', agentTitle: '尚未创建智能体', + notFoundAgent: '未查询到智能体', datasetTitle: '尚未创建数据集', + notFoundDataset: '未查询到数据集', chatTitle: '尚未创建聊天应用', + notFoundChat: '未查询到聊天应用', searchTitle: '尚未创建搜索应用', + notFoundSearch: '未查询到搜索应用', + memoryTitle: '尚未创建记忆', + notFoundMemory: '未查询到记忆', addNow: '立即添加', }, }, diff --git a/web/src/pages/agents/index.tsx b/web/src/pages/agents/index.tsx index 3600d3bd6..fd091c138 100644 --- a/web/src/pages/agents/index.tsx +++ b/web/src/pages/agents/index.tsx @@ -81,19 +81,20 @@ export default function Agents() { }, [isCreate, showCreatingModal, searchUrl, setSearchUrl]); return ( <> - {(!data?.length || data?.length <= 0) && ( + {(!data?.length || data?.length <= 0) && !searchString && (
showCreatingModal()} />
)}
- {!!data?.length && ( + {(!!data?.length || searchString) && ( <>
+ {(!data?.length || data?.length <= 0) && searchString && ( +
+ showCreatingModal()} + /> +
+ )}
{data.map((x) => { diff --git a/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx b/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx index c6d18af13..a27dcfe6d 100644 --- a/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx +++ b/web/src/pages/dataset/dataset-setting/configuration/common-item.tsx @@ -12,7 +12,7 @@ import { Switch } from '@/components/ui/switch'; import { useTranslate } from '@/hooks/common-hooks'; import { cn } from '@/lib/utils'; import { useMemo, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { FieldValues, useFormContext } from 'react-hook-form'; import { useHandleKbEmbedding, useHasParsedDocument, @@ -65,17 +65,59 @@ export function ChunkMethodItem(props: IProps) { /> ); } -export function EmbeddingModelItem({ line = 1, isEdit }: IProps) { + +export const EmbeddingSelect = ({ + isEdit, + field, + name, +}: { + isEdit: boolean; + field: FieldValues; + name?: string; +}) => { const { t } = useTranslate('knowledgeConfiguration'); const form = useFormContext(); const embeddingModelOptions = useSelectEmbeddingModelOptions(); const { handleChange } = useHandleKbEmbedding(); const disabled = useHasParsedDocument(isEdit); const oldValue = useMemo(() => { - const embdStr = form.getValues('embd_id'); + const embdStr = form.getValues(name || 'embd_id'); return embdStr || ''; }, [form]); const [loading, setLoading] = useState(false); + return ( + + { + field.onChange(value); + if (isEdit && disabled) { + setLoading(true); + const res = await handleChange({ + embed_id: value, + callback: field.onChange, + }); + if (res.code !== 0) { + field.onChange(oldValue); + } + setLoading(false); + } + }} + value={field.value} + options={embeddingModelOptions} + placeholder={t('embeddingModelPlaceholder')} + /> + + ); +}; + +export function EmbeddingModelItem({ line = 1, isEdit }: IProps) { + const { t } = useTranslate('knowledgeConfiguration'); + const form = useFormContext(); return ( <> - - { - field.onChange(value); - if (isEdit && disabled) { - setLoading(true); - const res = await handleChange({ - embed_id: value, - callback: field.onChange, - }); - if (res.code !== 0) { - field.onChange(oldValue); - } - setLoading(false); - } - }} - value={field.value} - options={embeddingModelOptions} - placeholder={t('embeddingModelPlaceholder')} - triggerClassName="!bg-bg-base" - /> - +
diff --git a/web/src/pages/datasets/index.tsx b/web/src/pages/datasets/index.tsx index c0515fc99..110a1e485 100644 --- a/web/src/pages/datasets/index.tsx +++ b/web/src/pages/datasets/index.tsx @@ -70,18 +70,19 @@ export default function Datasets() { return ( <>
- {(!kbs?.length || kbs?.length <= 0) && ( + {(!kbs?.length || kbs?.length <= 0) && !searchString && (
showModal()} />
)} - {!!kbs?.length && ( + {(!!kbs?.length || searchString) && ( <> + {(!kbs?.length || kbs?.length <= 0) && searchString && ( +
+ showModal()} + /> +
+ )}
{kbs.map((dataset) => { diff --git a/web/src/pages/memories/add-or-edit-modal.tsx b/web/src/pages/memories/add-or-edit-modal.tsx new file mode 100644 index 000000000..e5ec1082e --- /dev/null +++ b/web/src/pages/memories/add-or-edit-modal.tsx @@ -0,0 +1,75 @@ +import { DynamicForm, DynamicFormRef } from '@/components/dynamic-form'; +import { useModelOptions } from '@/components/llm-setting-items/llm-form-field'; +import { HomeIcon } from '@/components/svg-icon'; +import { Modal } from '@/components/ui/modal/modal'; +import { t } from 'i18next'; +import { useCallback, useEffect, useState } from 'react'; +import { createMemoryFields } from './constants'; +import { IMemory } from './interface'; + +type IProps = { + open: boolean; + onClose: () => void; + onSubmit?: (data: any) => void; + initialMemory: IMemory; + loading?: boolean; +}; +export const AddOrEditModal = (props: IProps) => { + const { open, onClose, onSubmit, initialMemory } = props; + // const [fields, setFields] = useState(createMemoryFields); + // const formRef = useRef(null); + const [formInstance, setFormInstance] = useState(null); + + const formCallbackRef = useCallback((node: DynamicFormRef | null) => { + if (node) { + // formRef.current = node; + setFormInstance(node); + } + }, []); + const { modelOptions } = useModelOptions(); + + useEffect(() => { + if (initialMemory && initialMemory.id) { + formInstance?.onFieldUpdate('memory_type', { hidden: true }); + formInstance?.onFieldUpdate('embedding', { hidden: true }); + formInstance?.onFieldUpdate('llm', { hidden: true }); + } else { + formInstance?.onFieldUpdate('llm', { options: modelOptions as any }); + } + }, [modelOptions, formInstance, initialMemory]); + + return ( + +
+ +
+ {t('memory.createMemory')} +
+ } + showfooter={false} + confirmLoading={props.loading} + > + {}} + defaultValues={initialMemory} + > +
+ + { + onSubmit?.(data); + }} + /> +
+
+ + ); +}; diff --git a/web/src/pages/memories/constants/index.tsx b/web/src/pages/memories/constants/index.tsx new file mode 100644 index 000000000..004298465 --- /dev/null +++ b/web/src/pages/memories/constants/index.tsx @@ -0,0 +1,41 @@ +import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form'; +import { EmbeddingSelect } from '@/pages/dataset/dataset-setting/configuration/common-item'; +import { t } from 'i18next'; + +export const createMemoryFields = [ + { + name: 'memory_name', + label: t('memory.name'), + placeholder: t('memory.memoryNamePlaceholder'), + required: true, + }, + { + name: 'memory_type', + label: t('memory.memoryType'), + type: FormFieldType.MultiSelect, + placeholder: t('memory.descriptionPlaceholder'), + options: [ + { label: 'Raw', value: 'raw' }, + { label: 'Semantic', value: 'semantic' }, + { label: 'Episodic', value: 'episodic' }, + { label: 'Procedural', value: 'procedural' }, + ], + required: true, + }, + { + name: 'embedding', + label: t('memory.embeddingModel'), + placeholder: t('memory.selectModel'), + required: true, + // hideLabel: true, + // type: 'custom', + render: (field) => , + }, + { + name: 'llm', + label: t('memory.llm'), + placeholder: t('memory.selectModel'), + required: true, + type: FormFieldType.Select, + }, +] as FormFieldConfig[]; diff --git a/web/src/pages/memories/hooks.ts b/web/src/pages/memories/hooks.ts new file mode 100644 index 000000000..d1fa157e3 --- /dev/null +++ b/web/src/pages/memories/hooks.ts @@ -0,0 +1,288 @@ +// src/pages/next-memoryes/hooks.ts + +import message from '@/components/ui/message'; +import { useSetModalState } from '@/hooks/common-hooks'; +import { useHandleSearchChange } from '@/hooks/logic-hooks'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import memoryService, { updateMemoryById } from '@/services/memory-service'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useDebounce } from 'ahooks'; +import { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams, useSearchParams } from 'umi'; +import { + CreateMemoryResponse, + DeleteMemoryProps, + DeleteMemoryResponse, + ICreateMemoryProps, + IMemory, + IMemoryAppDetailProps, + MemoryDetailResponse, + MemoryListResponse, +} from './interface'; + +export const useCreateMemory = () => { + const { t } = useTranslation(); + + const { + data, + isError, + mutateAsync: createMemoryMutation, + } = useMutation({ + mutationKey: ['createMemory'], + mutationFn: async (props) => { + const { data: response } = await memoryService.createMemory(props); + if (response.code !== 0) { + throw new Error(response.message || 'Failed to create memory'); + } + return response.data; + }, + onSuccess: () => { + message.success(t('message.created')); + }, + onError: (error) => { + message.error(t('message.error', { error: error.message })); + }, + }); + + const createMemory = useCallback( + (props: ICreateMemoryProps) => { + return createMemoryMutation(props); + }, + [createMemoryMutation], + ); + + return { data, isError, createMemory }; +}; + +export const useFetchMemoryList = () => { + const { handleInputChange, searchString, pagination, setPagination } = + useHandleSearchChange(); + const debouncedSearchString = useDebounce(searchString, { wait: 500 }); + const { data, isLoading, isError, refetch } = useQuery< + MemoryListResponse, + Error + >({ + queryKey: [ + 'memoryList', + { + debouncedSearchString, + ...pagination, + }, + ], + queryFn: async () => { + const { data: response } = await memoryService.getMemoryList( + { + params: { + keywords: debouncedSearchString, + page_size: pagination.pageSize, + page: pagination.current, + }, + data: {}, + }, + true, + ); + if (response.code !== 0) { + throw new Error(response.message || 'Failed to fetch memory list'); + } + console.log(response); + return response; + }, + }); + + // const setMemoryListParams = (newParams: MemoryListParams) => { + // setMemoryParams((prevParams) => ({ + // ...prevParams, + // ...newParams, + // })); + // }; + + return { + data, + isLoading, + isError, + pagination, + searchString, + handleInputChange, + setPagination, + refetch, + }; +}; + +export const useFetchMemoryDetail = (tenantId?: string) => { + const { id } = useParams(); + + const [memoryParams] = useSearchParams(); + const shared_id = memoryParams.get('shared_id'); + const memoryId = id || shared_id; + let param: { id: string | null; tenant_id?: string } = { + id: memoryId, + }; + if (shared_id) { + param = { + id: memoryId, + tenant_id: tenantId, + }; + } + const fetchMemoryDetailFunc = shared_id + ? memoryService.getMemoryDetailShare + : memoryService.getMemoryDetail; + + const { data, isLoading, isError } = useQuery({ + queryKey: ['memoryDetail', memoryId], + enabled: !shared_id || !!tenantId, + queryFn: async () => { + const { data: response } = await fetchMemoryDetailFunc(param); + if (response.code !== 0) { + throw new Error(response.message || 'Failed to fetch memory detail'); + } + return response; + }, + }); + + return { data: data?.data, isLoading, isError }; +}; + +export const useDeleteMemory = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const { + data, + isError, + mutateAsync: deleteMemoryMutation, + } = useMutation({ + mutationKey: ['deleteMemory'], + mutationFn: async (props) => { + const { data: response } = await memoryService.deleteMemory(props); + if (response.code !== 0) { + throw new Error(response.message || 'Failed to delete memory'); + } + + queryClient.invalidateQueries({ queryKey: ['memoryList'] }); + return response; + }, + onSuccess: () => { + message.success(t('message.deleted')); + }, + onError: (error) => { + message.error(t('message.error', { error: error.message })); + }, + }); + + const deleteMemory = useCallback( + (props: DeleteMemoryProps) => { + return deleteMemoryMutation(props); + }, + [deleteMemoryMutation], + ); + + return { data, isError, deleteMemory }; +}; + +export const useUpdateMemory = () => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const { + data, + isError, + mutateAsync: updateMemoryMutation, + } = useMutation({ + mutationKey: ['updateMemory'], + mutationFn: async (formData) => { + const { data: response } = await updateMemoryById(formData.id, formData); + if (response.code !== 0) { + throw new Error(response.message || 'Failed to update memory'); + } + return response.data; + }, + onSuccess: (data, variables) => { + message.success(t('message.updated')); + queryClient.invalidateQueries({ + queryKey: ['memoryDetail', variables.id], + }); + }, + onError: (error) => { + message.error(t('message.error', { error: error.message })); + }, + }); + + const updateMemory = useCallback( + (formData: IMemoryAppDetailProps) => { + return updateMemoryMutation(formData); + }, + [updateMemoryMutation], + ); + + return { data, isError, updateMemory }; +}; + +export const useRenameMemory = () => { + const [memory, setMemory] = useState({} as IMemory); + const { navigateToMemory } = useNavigatePage(); + const { + visible: openCreateModal, + hideModal: hideChatRenameModal, + showModal: showChatRenameModal, + } = useSetModalState(); + const { updateMemory } = useUpdateMemory(); + const { createMemory } = useCreateMemory(); + const [loading, setLoading] = useState(false); + + const handleShowChatRenameModal = useCallback( + (record?: IMemory) => { + if (record) { + setMemory(record); + } + showChatRenameModal(); + }, + [showChatRenameModal], + ); + + const handleHideModal = useCallback(() => { + hideChatRenameModal(); + setMemory({} as IMemory); + }, [hideChatRenameModal]); + + const onMemoryRenameOk = useCallback( + async (data: ICreateMemoryProps, callBack?: () => void) => { + let res; + setLoading(true); + if (memory?.id) { + try { + // const reponse = await memoryService.getMemoryDetail({ + // id: memory?.id, + // }); + // const detail = reponse.data?.data; + // console.log('detail-->', detail); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // const { id, created_by, update_time, ...memoryDataTemp } = detail; + res = await updateMemory({ + // ...memoryDataTemp, + name: data.memory_name, + id: memory?.id, + } as unknown as IMemoryAppDetailProps); + } catch (e) { + console.error('error', e); + } + } else { + res = await createMemory(data); + } + if (res && !memory?.id) { + navigateToMemory(res?.id)(); + } + callBack?.(); + setLoading(false); + handleHideModal(); + }, + [memory, createMemory, handleHideModal, navigateToMemory, updateMemory], + ); + return { + memoryRenameLoading: loading, + initialMemory: memory, + onMemoryRenameOk, + openCreateModal, + hideMemoryModal: handleHideModal, + showMemoryRenameModal: handleShowChatRenameModal, + }; +}; diff --git a/web/src/pages/memories/index.tsx b/web/src/pages/memories/index.tsx new file mode 100644 index 000000000..49407765d --- /dev/null +++ b/web/src/pages/memories/index.tsx @@ -0,0 +1,163 @@ +import { CardContainer } from '@/components/card-container'; +import { EmptyCardType } from '@/components/empty/constant'; +import { EmptyAppCard } from '@/components/empty/empty'; +import ListFilterBar from '@/components/list-filter-bar'; +import { Button } from '@/components/ui/button'; +import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; +import { useTranslate } from '@/hooks/common-hooks'; +import { pick } from 'lodash'; +import { Plus } from 'lucide-react'; +import { useCallback, useEffect } from 'react'; +import { useSearchParams } from 'umi'; +import { AddOrEditModal } from './add-or-edit-modal'; +import { useFetchMemoryList, useRenameMemory } from './hooks'; +import { ICreateMemoryProps } from './interface'; +import { MemoryCard } from './memory-card'; + +export default function MemoryList() { + // const { data } = useFetchFlowList(); + const { t } = useTranslate('memory'); + // const [isEdit, setIsEdit] = useState(false); + const { + data: list, + pagination, + searchString, + handleInputChange, + setPagination, + refetch: refetchList, + } = useFetchMemoryList(); + + const { + openCreateModal, + showMemoryRenameModal, + hideMemoryModal, + searchRenameLoading, + onMemoryRenameOk, + initialMemory, + } = useRenameMemory(); + + const onMemoryConfirm = (data: ICreateMemoryProps) => { + onMemoryRenameOk(data, () => { + refetchList(); + }); + }; + const openCreateModalFun = useCallback(() => { + // setIsEdit(false); + showMemoryRenameModal(); + }, [showMemoryRenameModal]); + const handlePageChange = useCallback( + (page: number, pageSize?: number) => { + setPagination({ page, pageSize }); + }, + [setPagination], + ); + + const [searchUrl, setMemoryUrl] = useSearchParams(); + const isCreate = searchUrl.get('isCreate') === 'true'; + useEffect(() => { + if (isCreate) { + openCreateModalFun(); + searchUrl.delete('isCreate'); + setMemoryUrl(searchUrl); + } + }, [isCreate, openCreateModalFun, searchUrl, setMemoryUrl]); + + return ( +
+ {(!list?.data?.memory_list?.length || + list?.data?.memory_list?.length <= 0) && + !searchString && ( +
+ openCreateModalFun()} + /> +
+ )} + {(!!list?.data?.memory_list?.length || searchString) && ( + <> +
+ + + +
+ {(!list?.data?.memory_list?.length || + list?.data?.memory_list?.length <= 0) && + searchString && ( +
+ openCreateModalFun()} + /> +
+ )} +
+ + {list?.data.memory_list.map((x) => { + return ( + { + showMemoryRenameModal(x); + }} + > + ); + })} + +
+ {list?.data.total && list?.data.total > 0 && ( +
+ +
+ )} + + )} + {/* {openCreateModal && ( + } + > + )} */} + {openCreateModal && ( + + )} +
+ ); +} diff --git a/web/src/pages/memories/interface.ts b/web/src/pages/memories/interface.ts new file mode 100644 index 000000000..46cba578c --- /dev/null +++ b/web/src/pages/memories/interface.ts @@ -0,0 +1,121 @@ +export interface ICreateMemoryProps { + memory_name: string; + memory_type: Array; + embedding: string; + llm: string; +} + +export interface CreateMemoryResponse { + id: string; + name: string; + description: string; +} + +export interface MemoryListParams { + keywords?: string; + parser_id?: string; + page?: number; + page_size?: number; + orderby?: string; + desc?: boolean; + owner_ids?: string; +} +export type MemoryType = 'raw' | 'semantic' | 'episodic' | 'procedural'; +export type StorageType = 'table' | 'graph'; +export type Permissions = 'me' | 'team'; +export type ForgettingPolicy = 'fifo' | 'lru'; + +export interface IMemory { + id: string; + name: string; + avatar: string; + tenant_id: string; + owner_name: string; + memory_type: MemoryType[]; + storage_type: StorageType; + embedding: string; + llm: string; + permissions: Permissions; + description: string; + memory_size: number; + forgetting_policy: ForgettingPolicy; + temperature: string; + system_prompt: string; + user_prompt: string; +} +export interface MemoryListResponse { + code: number; + data: { + memory_list: Array; + total: number; + }; + message: string; +} + +export interface DeleteMemoryProps { + memory_id: string; +} + +export interface DeleteMemoryResponse { + code: number; + data: boolean; + message: string; +} + +export interface IllmSettingProps { + llm_id: string; + parameter: string; + temperature?: number; + top_p?: number; + frequency_penalty?: number; + presence_penalty?: number; +} +interface IllmSettingEnableProps { + temperatureEnabled?: boolean; + topPEnabled?: boolean; + presencePenaltyEnabled?: boolean; + frequencyPenaltyEnabled?: boolean; +} +export interface IMemoryAppDetailProps { + avatar: any; + created_by: string; + description: string; + id: string; + name: string; + memory_config: { + cross_languages: string[]; + doc_ids: string[]; + chat_id: string; + highlight: boolean; + kb_ids: string[]; + keyword: boolean; + query_mindmap: boolean; + related_memory: boolean; + rerank_id: string; + use_rerank?: boolean; + similarity_threshold: number; + summary: boolean; + llm_setting: IllmSettingProps & IllmSettingEnableProps; + top_k: number; + use_kg: boolean; + vector_similarity_weight: number; + web_memory: boolean; + chat_settingcross_languages: string[]; + meta_data_filter?: { + method: string; + manual: { key: string; op: string; value: string }[]; + }; + }; + tenant_id: string; + update_time: number; +} + +export interface MemoryDetailResponse { + code: number; + data: IMemoryAppDetailProps; + message: string; +} + +// export type IUpdateMemoryProps = Omit & { +// id: string; +// }; diff --git a/web/src/pages/memories/memory-card.tsx b/web/src/pages/memories/memory-card.tsx new file mode 100644 index 000000000..716b19313 --- /dev/null +++ b/web/src/pages/memories/memory-card.tsx @@ -0,0 +1,32 @@ +import { HomeCard } from '@/components/home-card'; +import { MoreButton } from '@/components/more-button'; +import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; +import { IMemory } from './interface'; +import { MemoryDropdown } from './memory-dropdown'; + +interface IProps { + data: IMemory; + showMemoryRenameModal: (data: IMemory) => void; +} +export function MemoryCard({ data, showMemoryRenameModal }: IProps) { + const { navigateToMemory } = useNavigatePage(); + + return ( + + + + } + onClick={navigateToMemory(data?.id)} + /> + ); +} diff --git a/web/src/pages/memories/memory-dropdown.tsx b/web/src/pages/memories/memory-dropdown.tsx new file mode 100644 index 000000000..2bcdcac1a --- /dev/null +++ b/web/src/pages/memories/memory-dropdown.tsx @@ -0,0 +1,74 @@ +import { + ConfirmDeleteDialog, + ConfirmDeleteDialogNode, +} from '@/components/confirm-delete-dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { PenLine, Trash2 } from 'lucide-react'; +import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IMemoryAppProps, useDeleteMemory } from './hooks'; + +export function MemoryDropdown({ + children, + dataset, + showMemoryRenameModal, +}: PropsWithChildren & { + dataset: IMemoryAppProps; + showMemoryRenameModal: (dataset: IMemoryAppProps) => void; +}) { + const { t } = useTranslation(); + const { deleteMemory } = useDeleteMemory(); + const handleShowChatRenameModal: MouseEventHandler = + useCallback( + (e) => { + e.stopPropagation(); + showMemoryRenameModal(dataset); + }, + [dataset, showMemoryRenameModal], + ); + const handleDelete: MouseEventHandler = useCallback(() => { + deleteMemory({ search_id: dataset.id }); + }, [dataset.id, deleteMemory]); + + return ( + + {children} + + + {t('common.rename')} + + + + ), + }} + > + { + e.preventDefault(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + > + {t('common.delete')} + + + + + ); +} diff --git a/web/src/pages/memory/constant.tsx b/web/src/pages/memory/constant.tsx new file mode 100644 index 000000000..1377fccc5 --- /dev/null +++ b/web/src/pages/memory/constant.tsx @@ -0,0 +1,3 @@ +export enum MemoryApiAction { + FetchMemoryDetail = 'fetchMemoryDetail', +} diff --git a/web/src/pages/memory/hooks/use-memory-messages.ts b/web/src/pages/memory/hooks/use-memory-messages.ts new file mode 100644 index 000000000..e6345ae1a --- /dev/null +++ b/web/src/pages/memory/hooks/use-memory-messages.ts @@ -0,0 +1,59 @@ +import { useHandleSearchChange } from '@/hooks/logic-hooks'; +import { getMemoryDetailById } from '@/services/memory-service'; +import { useQuery } from '@tanstack/react-query'; +import { useParams, useSearchParams } from 'umi'; +import { MemoryApiAction } from '../constant'; +import { IMessageTableProps } from '../memory-message/interface'; + +export const useFetchMemoryMessageList = (props?: { + refreshCount?: number; +}) => { + const { refreshCount } = props || {}; + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const memoryBaseId = searchParams.get('id') || id; + const { handleInputChange, searchString, pagination, setPagination } = + useHandleSearchChange(); + + let queryKey: (MemoryApiAction | number)[] = [ + MemoryApiAction.FetchMemoryDetail, + ]; + if (typeof refreshCount === 'number') { + queryKey = [MemoryApiAction.FetchMemoryDetail, refreshCount]; + } + + const { data, isFetching: loading } = useQuery({ + queryKey: [...queryKey, searchString, pagination], + initialData: {} as IMessageTableProps, + gcTime: 0, + queryFn: async () => { + if (memoryBaseId) { + const { data } = await getMemoryDetailById(memoryBaseId as string, { + // filter: { + // agent_id: '', + // }, + keyword: searchString, + page: pagination.current, + page_size: pagination.pageSize, + }); + // setPagination({ + // page: data?.page ?? 1, + // pageSize: data?.page_size ?? 10, + // total: data?.total ?? 0, + // }); + return data?.data ?? {}; + } else { + return {}; + } + }, + }); + + return { + data, + loading, + handleInputChange, + searchString, + pagination, + setPagination, + }; +}; diff --git a/web/src/pages/memory/hooks/use-memory-setting.ts b/web/src/pages/memory/hooks/use-memory-setting.ts new file mode 100644 index 000000000..bbca1c6ee --- /dev/null +++ b/web/src/pages/memory/hooks/use-memory-setting.ts @@ -0,0 +1,59 @@ +import { useHandleSearchChange } from '@/hooks/logic-hooks'; +import { IMemory } from '@/pages/memories/interface'; +import { getMemoryDetailById } from '@/services/memory-service'; +import { useQuery } from '@tanstack/react-query'; +import { useParams, useSearchParams } from 'umi'; +import { MemoryApiAction } from '../constant'; + +export const useFetchMemoryBaseConfiguration = (props?: { + refreshCount?: number; +}) => { + const { refreshCount } = props || {}; + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const memoryBaseId = searchParams.get('id') || id; + const { handleInputChange, searchString, pagination, setPagination } = + useHandleSearchChange(); + + let queryKey: (MemoryApiAction | number)[] = [ + MemoryApiAction.FetchMemoryDetail, + ]; + if (typeof refreshCount === 'number') { + queryKey = [MemoryApiAction.FetchMemoryDetail, refreshCount]; + } + + const { data, isFetching: loading } = useQuery({ + queryKey: [...queryKey, searchString, pagination], + initialData: {} as IMemory, + gcTime: 0, + queryFn: async () => { + if (memoryBaseId) { + const { data } = await getMemoryDetailById(memoryBaseId as string, { + // filter: { + // agent_id: '', + // }, + keyword: searchString, + page: pagination.current, + page_size: pagination.size, + }); + // setPagination({ + // page: data?.page ?? 1, + // pageSize: data?.page_size ?? 10, + // total: data?.total ?? 0, + // }); + return data?.data ?? {}; + } else { + return {}; + } + }, + }); + + return { + data, + loading, + handleInputChange, + searchString, + pagination, + setPagination, + }; +}; diff --git a/web/src/pages/memory/index.tsx b/web/src/pages/memory/index.tsx new file mode 100644 index 000000000..3536a71b7 --- /dev/null +++ b/web/src/pages/memory/index.tsx @@ -0,0 +1,17 @@ +import Spotlight from '@/components/spotlight'; +import { Outlet } from 'umi'; +import { SideBar } from './sidebar'; + +export default function DatasetWrapper() { + return ( +
+
+ +
+ + +
+
+
+ ); +} diff --git a/web/src/pages/memory/memory-message/index.tsx b/web/src/pages/memory/memory-message/index.tsx new file mode 100644 index 000000000..c0ec80823 --- /dev/null +++ b/web/src/pages/memory/memory-message/index.tsx @@ -0,0 +1,51 @@ +import ListFilterBar from '@/components/list-filter-bar'; +import { t } from 'i18next'; +import { useFetchMemoryMessageList } from '../hooks/use-memory-messages'; +import { MemoryTable } from './message-table'; + +export default function MemoryMessage() { + const { + searchString, + // documents, + data, + pagination, + handleInputChange, + setPagination, + // filterValue, + // handleFilterSubmit, + loading, + } = useFetchMemoryMessageList(); + return ( +
+ +
{t('knowledgeDetails.subbarFiles')}
+
+ {t('knowledgeDetails.datasetDescription')} +
+
+ } + > + +
+
message
+
+ + ); +} diff --git a/web/src/pages/memory/memory-message/interface.ts b/web/src/pages/memory/memory-message/interface.ts new file mode 100644 index 000000000..234ca438a --- /dev/null +++ b/web/src/pages/memory/memory-message/interface.ts @@ -0,0 +1,19 @@ +export interface IMessageInfo { + message_id: number; + message_type: 'semantic' | 'raw' | 'procedural'; + source_id: string | '-'; + id: string; + user_id: string; + agent_id: string; + agent_name: string; + session_id: string; + valid_at: string; + invalid_at: string; + forget_at: string; + status: boolean; +} + +export interface IMessageTableProps { + messages: { message_list: Array; total: number }; + storage_type: string; +} diff --git a/web/src/pages/memory/memory-message/message-table.tsx b/web/src/pages/memory/memory-message/message-table.tsx new file mode 100644 index 000000000..2174c2f79 --- /dev/null +++ b/web/src/pages/memory/memory-message/message-table.tsx @@ -0,0 +1,225 @@ +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import * as React from 'react'; + +import { EmptyType } from '@/components/empty/constant'; +import Empty from '@/components/empty/empty'; +import { Button } from '@/components/ui/button'; +import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; +import { Switch } from '@/components/ui/switch'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Pagination } from '@/interfaces/common'; +import { t } from 'i18next'; +import { pick } from 'lodash'; +import { Eraser, TextSelect } from 'lucide-react'; +import { useMemo } from 'react'; +import { IMessageInfo } from './interface'; + +export type MemoryTableProps = { + messages: Array; + total: number; + pagination: Pagination; + setPagination: (params: { page: number; pageSize: number }) => void; +}; + +export function MemoryTable({ + messages, + total, + pagination, + setPagination, +}: MemoryTableProps) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [], + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + + // Define columns for the memory table + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'session_id', + header: () => {t('memoryDetail.messages.sessionId')}, + cell: ({ row }) => ( +
+ {row.getValue('session_id')} +
+ ), + }, + { + accessorKey: 'agent_name', + header: () => {t('memoryDetail.messages.agent')}, + cell: ({ row }) => ( +
+ {row.getValue('agent_name')} +
+ ), + }, + { + accessorKey: 'message_type', + header: () => {t('memoryDetail.messages.type')}, + cell: ({ row }) => ( +
+ {row.getValue('message_type')} +
+ ), + }, + { + accessorKey: 'valid_at', + header: () => {t('memoryDetail.messages.validDate')}, + cell: ({ row }) => ( +
{row.getValue('valid_at')}
+ ), + }, + { + accessorKey: 'forget_at', + header: () => {t('memoryDetail.messages.forgetAt')}, + cell: ({ row }) => ( +
{row.getValue('forget_at')}
+ ), + }, + { + accessorKey: 'source_id', + header: () => {t('memoryDetail.messages.source')}, + cell: ({ row }) => ( +
{row.getValue('source_id')}
+ ), + }, + { + accessorKey: 'status', + header: () => {t('memoryDetail.messages.enable')}, + cell: ({ row }) => { + const isEnabled = row.getValue('status') as boolean; + return ( +
+ {}} /> +
+ ); + }, + }, + { + accessorKey: 'action', + header: () => {t('memoryDetail.messages.action')}, + meta: { + cellClassName: 'w-12', + }, + cell: () => ( +
+ + +
+ ), + }, + ], + [], + ); + + const currentPagination = useMemo(() => { + return { + pageIndex: (pagination.current || 1) - 1, + pageSize: pagination.pageSize || 10, + }; + }, [pagination]); + + const table = useReactTable({ + data: messages, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + manualPagination: true, + state: { + sorting, + columnFilters, + columnVisibility, + pagination: currentPagination, + }, + rowCount: total, + }); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + + + + )} + +
+ +
+ { + setPagination({ page, pageSize }); + }} + /> +
+
+ ); +} diff --git a/web/src/pages/memory/memory-setting/index.tsx b/web/src/pages/memory/memory-setting/index.tsx new file mode 100644 index 000000000..27be86cfd --- /dev/null +++ b/web/src/pages/memory/memory-setting/index.tsx @@ -0,0 +1,13 @@ +export default function MemoryMessage() { + return ( +
+
+
11
+
11
+
+
+
setting
+
+
+ ); +} diff --git a/web/src/pages/memory/sidebar/hooks.tsx b/web/src/pages/memory/sidebar/hooks.tsx new file mode 100644 index 000000000..1dd28785a --- /dev/null +++ b/web/src/pages/memory/sidebar/hooks.tsx @@ -0,0 +1,17 @@ +import { Routes } from '@/routes'; +import { useCallback } from 'react'; +import { useNavigate, useParams } from 'umi'; + +export const useHandleMenuClick = () => { + const navigate = useNavigate(); + const { id } = useParams(); + + const handleMenuClick = useCallback( + (key: Routes) => () => { + navigate(`${Routes.Memory}${key}/${id}`); + }, + [id, navigate], + ); + + return { handleMenuClick }; +}; diff --git a/web/src/pages/memory/sidebar/index.tsx b/web/src/pages/memory/sidebar/index.tsx new file mode 100644 index 000000000..98928fe43 --- /dev/null +++ b/web/src/pages/memory/sidebar/index.tsx @@ -0,0 +1,88 @@ +import { RAGFlowAvatar } from '@/components/ragflow-avatar'; +import { Button } from '@/components/ui/button'; +import { useSecondPathName } from '@/hooks/route-hook'; +import { cn, formatBytes } from '@/lib/utils'; +import { Routes } from '@/routes'; +import { formatPureDate } from '@/utils/date'; +import { Banknote, Logs } from 'lucide-react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFetchMemoryBaseConfiguration } from '../hooks/use-memory-setting'; +import { useHandleMenuClick } from './hooks'; + +type PropType = { + refreshCount?: number; +}; + +export function SideBar({ refreshCount }: PropType) { + const pathName = useSecondPathName(); + const { handleMenuClick } = useHandleMenuClick(); + // refreshCount: be for avatar img sync update on top left + const { data } = useFetchMemoryBaseConfiguration({ refreshCount }); + const { t } = useTranslation(); + + const items = useMemo(() => { + const list = [ + { + icon: , + label: t(`knowledgeDetails.overview`), + key: Routes.MemoryMessage, + }, + { + icon: , + label: t(`knowledgeDetails.configuration`), + key: Routes.MemorySetting, + }, + ]; + return list; + }, [t]); + + return ( + + ); +} diff --git a/web/src/pages/next-chats/index.tsx b/web/src/pages/next-chats/index.tsx index e6667252f..15940e6d8 100644 --- a/web/src/pages/next-chats/index.tsx +++ b/web/src/pages/next-chats/index.tsx @@ -50,18 +50,19 @@ export default function ChatList() { return (
- {data.dialogs?.length <= 0 && ( + {data.dialogs?.length <= 0 && !searchString && (
handleShowCreateModal()} />
)} - {data.dialogs?.length > 0 && ( + {(data.dialogs?.length > 0 || searchString) && ( <>
+ {data.dialogs?.length <= 0 && searchString && ( +
+ handleShowCreateModal()} + /> +
+ )}
{data.dialogs.map((x) => { diff --git a/web/src/pages/next-searches/hooks.ts b/web/src/pages/next-searches/hooks.ts index 1787d7f23..64699e64a 100644 --- a/web/src/pages/next-searches/hooks.ts +++ b/web/src/pages/next-searches/hooks.ts @@ -2,9 +2,11 @@ import message from '@/components/ui/message'; import { useSetModalState } from '@/hooks/common-hooks'; +import { useHandleSearchChange } from '@/hooks/logic-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import searchService from '@/services/search-service'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useDebounce } from 'ahooks'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams, useSearchParams } from 'umi'; @@ -84,21 +86,34 @@ interface SearchListResponse { message: string; } -export const useFetchSearchList = (params?: SearchListParams) => { - const [searchParams, setSearchParams] = useState({ - page: 1, - page_size: 50, - ...params, - }); +export const useFetchSearchList = () => { + const { handleInputChange, searchString, pagination, setPagination } = + useHandleSearchChange(); + const debouncedSearchString = useDebounce(searchString, { wait: 500 }); const { data, isLoading, isError, refetch } = useQuery< SearchListResponse, Error >({ - queryKey: ['searchList', searchParams], + queryKey: [ + 'searchList', + { + debouncedSearchString, + ...pagination, + }, + ], queryFn: async () => { - const { data: response } = - await searchService.getSearchList(searchParams); + const { data: response } = await searchService.getSearchList( + { + params: { + keywords: debouncedSearchString, + page_size: pagination.pageSize, + page: pagination.current, + }, + data: {}, + }, + true, + ); if (response.code !== 0) { throw new Error(response.message || 'Failed to fetch search list'); } @@ -106,19 +121,14 @@ export const useFetchSearchList = (params?: SearchListParams) => { }, }); - const setSearchListParams = (newParams: SearchListParams) => { - setSearchParams((prevParams) => ({ - ...prevParams, - ...newParams, - })); - }; - return { data, isLoading, isError, - searchParams, - setSearchListParams, + pagination, + searchString, + handleInputChange, + setPagination, refetch, }; }; diff --git a/web/src/pages/next-searches/index.tsx b/web/src/pages/next-searches/index.tsx index 4267dbbe7..89a44a9cc 100644 --- a/web/src/pages/next-searches/index.tsx +++ b/web/src/pages/next-searches/index.tsx @@ -7,6 +7,7 @@ import { RenameDialog } from '@/components/rename-dialog'; import { Button } from '@/components/ui/button'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { useTranslate } from '@/hooks/common-hooks'; +import { pick } from 'lodash'; import { Plus } from 'lucide-react'; import { useCallback, useEffect } from 'react'; import { useSearchParams } from 'umi'; @@ -19,10 +20,13 @@ export default function SearchList() { // const [isEdit, setIsEdit] = useState(false); const { data: list, - searchParams, - setSearchListParams, + pagination, + searchString, + handleInputChange, + setPagination, refetch: refetchList, } = useFetchSearchList(); + const { openCreateModal, showSearchRenameModal, @@ -32,9 +36,9 @@ export default function SearchList() { initialSearchName, } = useRenameSearch(); - const handleSearchChange = (value: string) => { - console.log(value); - }; + // const handleSearchChange = (value: string) => { + // console.log(value); + // }; const onSearchRenameConfirm = (name: string) => { onSearchRenameOk(name, () => { refetchList(); @@ -44,10 +48,12 @@ export default function SearchList() { // setIsEdit(false); showSearchRenameModal(); }, [showSearchRenameModal]); - const handlePageChange = (page: number, pageSize: number) => { - // setIsEdit(false); - setSearchListParams({ ...searchParams, page, page_size: pageSize }); - }; + const handlePageChange = useCallback( + (page: number, pageSize?: number) => { + setPagination({ page, pageSize }); + }, + [setPagination], + ); const [searchUrl, setSearchUrl] = useSearchParams(); const isCreate = searchUrl.get('isCreate') === 'true'; @@ -62,25 +68,28 @@ export default function SearchList() { return (
{(!list?.data?.search_apps?.length || - list?.data?.search_apps?.length <= 0) && ( -
- openCreateModalFun()} - /> -
- )} - {!!list?.data?.search_apps?.length && ( + list?.data?.search_apps?.length <= 0) && + !searchString && ( +
+ openCreateModalFun()} + /> +
+ )} + {(!!list?.data?.search_apps?.length || searchString) && ( <>
handleSearchChange(e.target.value)} + searchString={searchString} + onSearchChange={handleInputChange} >
+ {(!list?.data?.search_apps?.length || + list?.data?.search_apps?.length <= 0) && + searchString && ( +
+ openCreateModalFun()} + /> +
+ )}
{list?.data.search_apps.map((x) => { @@ -111,8 +134,8 @@ export default function SearchList() { {list?.data.total && list?.data.total > 0 && (
diff --git a/web/src/pages/user-setting/data-source/component/confluence-token-field.tsx b/web/src/pages/user-setting/data-source/component/confluence-token-field.tsx index 5fe50b931..6c7e201d4 100644 --- a/web/src/pages/user-setting/data-source/component/confluence-token-field.tsx +++ b/web/src/pages/user-setting/data-source/component/confluence-token-field.tsx @@ -1,9 +1,10 @@ -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { ControllerRenderProps, useFormContext } from 'react-hook-form'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; +import { debounce } from 'lodash'; /* ---------------- Token Field ---------------- */ @@ -48,15 +49,15 @@ type ConfluenceIndexingMode = 'everything' | 'space' | 'page'; export type ConfluenceIndexingModeFieldProps = ControllerRenderProps; export const ConfluenceIndexingModeField = ( - fieldProps: ConfluenceIndexingModeFieldProps, + fieldProps: ControllerRenderProps, ) => { const { value, onChange, disabled } = fieldProps; + const [mode, setMode] = useState( + value || 'everything', + ); const { watch, setValue } = useFormContext(); - const mode = useMemo( - () => (value as ConfluenceIndexingMode) || 'everything', - [value], - ); + useEffect(() => setMode(value), [value]); const spaceValue = watch('config.space'); const pageIdValue = watch('config.page_id'); @@ -66,27 +67,40 @@ export const ConfluenceIndexingModeField = ( if (!value) onChange('everything'); }, [value, onChange]); - const handleModeChange = (nextMode?: string) => { - const normalized = (nextMode || 'everything') as ConfluenceIndexingMode; - onChange(normalized); + const handleModeChange = useCallback( + (nextMode?: string) => { + let normalized: ConfluenceIndexingMode = 'everything'; + if (nextMode) { + normalized = nextMode as ConfluenceIndexingMode; + setMode(normalized); + onChange(normalized); + } else { + setMode(mode); + normalized = mode; + onChange(mode); + // onChange(mode); + } + if (normalized === 'everything') { + setValue('config.space', ''); + setValue('config.page_id', ''); + setValue('config.index_recursively', false); + } else if (normalized === 'space') { + setValue('config.page_id', ''); + setValue('config.index_recursively', false); + } else if (normalized === 'page') { + setValue('config.space', ''); + } + }, + [mode, onChange, setValue], + ); - if (normalized === 'everything') { - setValue('config.space', '', { shouldDirty: true, shouldTouch: true }); - setValue('config.page_id', '', { shouldDirty: true, shouldTouch: true }); - setValue('config.index_recursively', false, { - shouldDirty: true, - shouldTouch: true, - }); - } else if (normalized === 'space') { - setValue('config.page_id', '', { shouldDirty: true, shouldTouch: true }); - setValue('config.index_recursively', false, { - shouldDirty: true, - shouldTouch: true, - }); - } else if (normalized === 'page') { - setValue('config.space', '', { shouldDirty: true, shouldTouch: true }); - } - }; + const debouncedHandleChange = useMemo( + () => + debounce(() => { + handleModeChange(); + }, 300), + [handleModeChange], + ); return (
@@ -127,12 +141,11 @@ export const ConfluenceIndexingModeField = ( - setValue('config.space', e.target.value, { - shouldDirty: true, - shouldTouch: true, - }) - } + onChange={(e) => { + const value = e.target.value; + setValue('config.space', value); + debouncedHandleChange(); + }} placeholder="e.g. KB" disabled={disabled} /> @@ -148,12 +161,10 @@ export const ConfluenceIndexingModeField = ( - setValue('config.page_id', e.target.value, { - shouldDirty: true, - shouldTouch: true, - }) - } + onChange={(e) => { + setValue('config.page_id', e.target.value); + debouncedHandleChange(); + }} placeholder="e.g. 123456" disabled={disabled} /> @@ -164,12 +175,10 @@ export const ConfluenceIndexingModeField = (
- setValue('config.index_recursively', Boolean(checked), { - shouldDirty: true, - shouldTouch: true, - }) - } + onCheckedChange={(checked) => { + setValue('config.index_recursively', Boolean(checked)); + debouncedHandleChange(); + }} disabled={disabled} /> diff --git a/web/src/pages/user-setting/data-source/contant.tsx b/web/src/pages/user-setting/data-source/contant.tsx index 34ced0ae2..b3e86e118 100644 --- a/web/src/pages/user-setting/data-source/contant.tsx +++ b/web/src/pages/user-setting/data-source/contant.tsx @@ -1,6 +1,7 @@ import { FormFieldType } from '@/components/dynamic-form'; import SvgIcon from '@/components/svg-icon'; import { t } from 'i18next'; +import { ControllerRenderProps } from 'react-hook-form'; import { ConfluenceIndexingModeField } from './component/confluence-token-field'; import GmailTokenField from './component/gmail-token-field'; import GoogleDriveTokenField from './component/google-drive-token-field'; @@ -237,7 +238,9 @@ export const DataSourceFormFields = { required: false, horizontal: true, labelClassName: 'self-start pt-4', - render: (fieldProps) => , + render: (fieldProps: ControllerRenderProps) => ( + + ), }, { label: 'Space Key', @@ -598,6 +601,7 @@ export const DataSourceFormDefaultValues = { confluence_username: '', confluence_access_token: '', }, + index_mode: 'everything', }, }, [DataSourceKey.GOOGLE_DRIVE]: { diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx index fe54dda64..f399fd21d 100644 --- a/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx +++ b/web/src/pages/user-setting/data-source/data-source-detail-page/index.tsx @@ -136,7 +136,7 @@ const SourceDetailPage = () => { ...customFields, ] as FormFieldConfig[]; - const neweFields = fields.map((field) => { + const newFields = fields.map((field) => { return { ...field, horizontal: true, @@ -145,7 +145,7 @@ const SourceDetailPage = () => { }, }; }); - setFields(neweFields); + setFields(newFields); const defultValueTemp = { ...(DataSourceFormDefaultValues[ diff --git a/web/src/pages/user-setting/setting-model/components/llm-header.tsx b/web/src/pages/user-setting/setting-model/components/llm-header.tsx new file mode 100644 index 000000000..0c90cf6b7 --- /dev/null +++ b/web/src/pages/user-setting/setting-model/components/llm-header.tsx @@ -0,0 +1,34 @@ +import { LlmIcon } from '@/components/svg-icon'; +import { Button } from '@/components/ui/button'; +import { APIMapUrl } from '@/constants/llm'; +import { t } from 'i18next'; +import { ArrowUpRight, Plus } from 'lucide-react'; + +export const LLMHeader = ({ name }: { name: string }) => { + return ( +
+ +
+
{name}
+ {!!APIMapUrl[name as keyof typeof APIMapUrl] && ( + + )} +
+ +
+ ); +}; diff --git a/web/src/pages/user-setting/setting-model/components/un-add-model.tsx b/web/src/pages/user-setting/setting-model/components/un-add-model.tsx index e73f32c95..f4592a796 100644 --- a/web/src/pages/user-setting/setting-model/components/un-add-model.tsx +++ b/web/src/pages/user-setting/setting-model/components/un-add-model.tsx @@ -2,9 +2,10 @@ import { LlmIcon } from '@/components/svg-icon'; import { Button } from '@/components/ui/button'; import { SearchInput } from '@/components/ui/input'; +import { APIMapUrl } from '@/constants/llm'; import { useTranslate } from '@/hooks/common-hooks'; import { useSelectLlmList } from '@/hooks/use-llm-request'; -import { Plus } from 'lucide-react'; +import { ArrowUpRight, Plus } from 'lucide-react'; import { FC, useMemo, useState } from 'react'; type TagType = @@ -128,10 +129,26 @@ export const AvailableModels: FC<{ >
-
+
{model.name}
+ {!!APIMapUrl[model.name as keyof typeof APIMapUrl] && ( + + )}