Features: Memory page rendering and other bug fixes (#11784)

### What problem does this PR solve?

Features: Memory page rendering and other bug fixes
- Rendering of the Memory list page
- Rendering of the message list page in Memory
- Fixed an issue where the empty state was incorrectly displayed when
search criteria were applied
- Added a web link for the API-Key
- modifying the index_mode attribute of the Confluence data source.

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-12-08 10:17:56 +08:00
committed by GitHub
parent 3285f09c92
commit 660fa8888b
55 changed files with 2047 additions and 218 deletions

View File

@ -0,0 +1,16 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="memory dark">
<g id="lucide/hard-drive">
<path id="Vector" d="M22 12H2M22 12V18C22 18.5304 21.7893 19.0391 21.4142 19.4142C21.0391 19.7893 20.5304 20 20 20H4C3.46957 20 2.96086 19.7893 2.58579 19.4142C2.21071 19.0391 2 18.5304 2 18V12M22 12L18.55 5.11C18.3844 4.77679 18.1292 4.49637 17.813 4.30028C17.4967 4.10419 17.1321 4.0002 16.76 4H7.24C6.86792 4.0002 6.50326 4.10419 6.18704 4.30028C5.87083 4.49637 5.61558 4.77679 5.45 5.11L2 12" stroke="url(#paint0_linear_1100_4836)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<g id="lucide/hard-drive_2">
<path id="Vector_2" d="M6 16H6.01M10 16H10.01" stroke="#00BEB4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_1100_4836" x1="12.5556" y1="4" x2="12.5556" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#666666"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,6 +1,13 @@
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react';
import { import {
forwardRef,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import {
ControllerRenderProps,
DefaultValues, DefaultValues,
FieldValues, FieldValues,
SubmitHandler, SubmitHandler,
@ -26,6 +33,7 @@ import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { t } from 'i18next'; import { t } from 'i18next';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { MultiSelect, MultiSelectOptionType } from './ui/multi-select';
// Field type enumeration // Field type enumeration
export enum FormFieldType { export enum FormFieldType {
@ -35,14 +43,17 @@ export enum FormFieldType {
Number = 'number', Number = 'number',
Textarea = 'textarea', Textarea = 'textarea',
Select = 'select', Select = 'select',
MultiSelect = 'multi-select',
Checkbox = 'checkbox', Checkbox = 'checkbox',
Tag = 'tag', Tag = 'tag',
Custom = 'custom',
} }
// Field configuration interface // Field configuration interface
export interface FormFieldConfig { export interface FormFieldConfig {
name: string; name: string;
label: string; label: string;
hideLabel?: boolean;
type: FormFieldType; type: FormFieldType;
hidden?: boolean; hidden?: boolean;
required?: boolean; required?: boolean;
@ -57,7 +68,7 @@ export interface FormFieldConfig {
max?: number; max?: number;
message?: string; message?: string;
}; };
render?: (fieldProps: any) => React.ReactNode; render?: (fieldProps: ControllerRenderProps) => React.ReactNode;
horizontal?: boolean; horizontal?: boolean;
onChange?: (value: any) => void; onChange?: (value: any) => void;
tooltip?: React.ReactNode; tooltip?: React.ReactNode;
@ -78,10 +89,10 @@ interface DynamicFormProps<T extends FieldValues> {
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
defaultValues?: DefaultValues<T>; defaultValues?: DefaultValues<T>;
onFieldUpdate?: ( // onFieldUpdate?: (
fieldName: string, // fieldName: string,
updatedField: Partial<FormFieldConfig>, // updatedField: Partial<FormFieldConfig>,
) => void; // ) => void;
labelClassName?: string; labelClassName?: string;
} }
@ -92,6 +103,10 @@ export interface DynamicFormRef {
reset: (values?: any) => void; reset: (values?: any) => void;
watch: (field: string, callback: (value: any) => void) => () => void; watch: (field: string, callback: (value: any) => void) => () => void;
updateFieldType: (fieldName: string, newType: FormFieldType) => void; updateFieldType: (fieldName: string, newType: FormFieldType) => void;
onFieldUpdate: (
fieldName: string,
newFieldProperties: Partial<FormFieldConfig>,
) => void;
} }
// Generate Zod validation schema based on field configurations // Generate Zod validation schema based on field configurations
@ -110,6 +125,14 @@ const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
case FormFieldType.Email: case FormFieldType.Email:
fieldSchema = z.string().email('Please enter a valid email address'); fieldSchema = z.string().email('Please enter a valid email address');
break; 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: case FormFieldType.Number:
fieldSchema = z.coerce.number(); fieldSchema = z.coerce.number();
if (field.validation?.min !== undefined) { if (field.validation?.min !== undefined) {
@ -275,7 +298,10 @@ const generateDefaultValues = <T extends FieldValues>(
defaultValues[field.name] = field.defaultValue; defaultValues[field.name] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) { } else if (field.type === FormFieldType.Checkbox) {
defaultValues[field.name] = false; defaultValues[field.name] = false;
} else if (field.type === FormFieldType.Tag) { } else if (
field.type === FormFieldType.Tag ||
field.type === FormFieldType.MultiSelect
) {
defaultValues[field.name] = []; defaultValues[field.name] = [];
} else { } else {
defaultValues[field.name] = ''; defaultValues[field.name] = '';
@ -291,17 +317,21 @@ const DynamicForm = {
Root: forwardRef( Root: forwardRef(
<T extends FieldValues>( <T extends FieldValues>(
{ {
fields, fields: originFields,
onSubmit, onSubmit,
className = '', className = '',
children, children,
defaultValues: formDefaultValues = {} as DefaultValues<T>, defaultValues: formDefaultValues = {} as DefaultValues<T>,
onFieldUpdate, // onFieldUpdate,
labelClassName, labelClassName,
}: DynamicFormProps<T>, }: DynamicFormProps<T>,
ref: React.Ref<any>, ref: React.Ref<any>,
) => { ) => {
// Generate validation schema and default values // Generate validation schema and default values
const [fields, setFields] = useState(originFields);
useMemo(() => {
setFields(originFields);
}, [originFields]);
const schema = useMemo(() => generateSchema(fields), [fields]); const schema = useMemo(() => generateSchema(fields), [fields]);
const defaultValues = useMemo(() => { const defaultValues = useMemo(() => {
@ -406,43 +436,54 @@ const DynamicForm = {
}, [fields, form]); }, [fields, form]);
// Expose form methods via ref // Expose form methods via ref
useImperativeHandle(ref, () => ({ useImperativeHandle(
submit: () => form.handleSubmit(onSubmit)(), ref,
getValues: () => form.getValues(), () => ({
reset: (values?: T) => { submit: () => form.handleSubmit(onSubmit)(),
if (values) { getValues: () => form.getValues(),
form.reset(values); reset: (values?: T) => {
} else { if (values) {
form.reset(); form.reset(values);
}
},
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<FormFieldConfig>,
) => {
setTimeout(() => {
if (onFieldUpdate) {
onFieldUpdate(fieldName, updatedField);
} else { } else {
console.warn( form.reset();
'onFieldUpdate prop is not provided. Cannot update field type.',
);
} }
}, 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<FormFieldConfig>,
) => {
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(() => { useEffect(() => {
if (formDefaultValues && Object.keys(formDefaultValues).length > 0) { if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
@ -459,6 +500,9 @@ const DynamicForm = {
// Render form fields // Render form fields
const renderField = (field: FormFieldConfig) => { const renderField = (field: FormFieldConfig) => {
if (field.render) { if (field.render) {
if (field.type === FormFieldType.Custom && field.hideLabel) {
return <div className="w-full">{field.render({})}</div>;
}
return ( return (
<RAGFlowFormItem <RAGFlowFormItem
name={field.name} name={field.name}
@ -549,6 +593,43 @@ const DynamicForm = {
</RAGFlowFormItem> </RAGFlowFormItem>
); );
case FormFieldType.MultiSelect:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
tooltip={field.tooltip}
labelClassName={labelClassName || field.labelClassName}
>
{(fieldProps) => {
console.log('multi select value', fieldProps);
const finalFieldProps = {
...fieldProps,
onValueChange: (value: string[]) => {
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
};
return (
<MultiSelect
variant="inverted"
maxCount={100}
{...finalFieldProps}
// onValueChange={(data) => {
// console.log(data);
// field.onChange?.(data);
// }}
options={field.options as MultiSelectOptionType[]}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Checkbox: case FormFieldType.Checkbox:
return ( return (
<FormField <FormField

View File

@ -11,23 +11,33 @@ export enum EmptyCardType {
Dataset = 'dataset', Dataset = 'dataset',
Chat = 'chat', Chat = 'chat',
Search = 'search', Search = 'search',
Memory = 'memory',
} }
export const EmptyCardData = { export const EmptyCardData = {
[EmptyCardType.Agent]: { [EmptyCardType.Agent]: {
icon: <HomeIcon name="agents" width={'24'} />, icon: <HomeIcon name="agents" width={'24'} />,
title: t('empty.agentTitle'), title: t('empty.agentTitle'),
notFound: t('empty.notFoundAgent'),
}, },
[EmptyCardType.Dataset]: { [EmptyCardType.Dataset]: {
icon: <HomeIcon name="datasets" width={'24'} />, icon: <HomeIcon name="datasets" width={'24'} />,
title: t('empty.datasetTitle'), title: t('empty.datasetTitle'),
notFound: t('empty.notFoundDataset'),
}, },
[EmptyCardType.Chat]: { [EmptyCardType.Chat]: {
icon: <HomeIcon name="chats" width={'24'} />, icon: <HomeIcon name="chats" width={'24'} />,
title: t('empty.chatTitle'), title: t('empty.chatTitle'),
notFound: t('empty.notFoundChat'),
}, },
[EmptyCardType.Search]: { [EmptyCardType.Search]: {
icon: <HomeIcon name="searches" width={'24'} />, icon: <HomeIcon name="searches" width={'24'} />,
title: t('empty.searchTitle'), title: t('empty.searchTitle'),
notFound: t('empty.notFoundSearch'),
},
[EmptyCardType.Memory]: {
icon: <HomeIcon name="memory" width={'24'} />,
title: t('empty.memoryTitle'),
notFound: t('empty.notFoundMemory'),
}, },
}; };

View File

@ -76,9 +76,10 @@ export const EmptyAppCard = (props: {
onClick?: () => void; onClick?: () => void;
showIcon?: boolean; showIcon?: boolean;
className?: string; className?: string;
isSearch?: boolean;
size?: 'small' | 'large'; size?: 'small' | 'large';
}) => { }) => {
const { type, showIcon, className } = props; const { type, showIcon, className, isSearch } = props;
let defaultClass = ''; let defaultClass = '';
let style = {}; let style = {};
switch (props.size) { switch (props.size) {
@ -95,19 +96,29 @@ export const EmptyAppCard = (props: {
break; break;
} }
return ( return (
<div className=" cursor-pointer " onClick={props.onClick}> <div
className=" cursor-pointer "
onClick={isSearch ? undefined : props.onClick}
>
<EmptyCard <EmptyCard
icon={showIcon ? EmptyCardData[type].icon : undefined} icon={showIcon ? EmptyCardData[type].icon : undefined}
title={EmptyCardData[type].title} title={
isSearch ? EmptyCardData[type].notFound : EmptyCardData[type].title
}
className={className} className={className}
style={style} style={style}
// description={EmptyCardData[type].description} // description={EmptyCardData[type].description}
> >
<div {!isSearch && (
className={cn(defaultClass, 'flex items-center justify-start w-full')} <div
> className={cn(
<Plus size={24} /> defaultClass,
</div> 'flex items-center justify-start w-full',
)}
>
<Plus size={24} />
</div>
)}
</EmptyCard> </EmptyCard>
</div> </div>
); );

View File

@ -9,13 +9,19 @@ export type LLMFormFieldProps = {
name?: string; name?: string;
}; };
export function LLMFormField({ options, name }: LLMFormFieldProps) { export const useModelOptions = () => {
const { t } = useTranslation();
const modelOptions = useComposeLlmOptionsByModelTypes([ const modelOptions = useComposeLlmOptionsByModelTypes([
LlmModelType.Chat, LlmModelType.Chat,
LlmModelType.Image2text, LlmModelType.Image2text,
]); ]);
return {
modelOptions,
};
};
export function LLMFormField({ options, name }: LLMFormFieldProps) {
const { t } = useTranslation();
const { modelOptions } = useModelOptions();
return ( return (
<RAGFlowFormItem name={name || 'llm_id'} label={t('chat.model')}> <RAGFlowFormItem name={name || 'llm_id'} label={t('chat.model')}>

View File

@ -53,14 +53,16 @@ export function RAGFlowFormItem({
{label} {label}
</FormLabel> </FormLabel>
)} )}
<FormControl> <div className="w-full flex flex-col">
{typeof children === 'function' <FormControl>
? children(field) {typeof children === 'function'
: isValidElement(children) ? children(field)
? cloneElement(children, { ...field }) : isValidElement(children)
: children} ? cloneElement(children, { ...field })
</FormControl> : children}
<FormMessage /> </FormControl>
<FormMessage />
</div>
</FormItem> </FormItem>
)} )}
/> />

View File

@ -126,3 +126,53 @@ export const IconMap = {
[LLMFactory.JiekouAI]: 'jiekouai', [LLMFactory.JiekouAI]: 'jiekouai',
[LLMFactory.Builtin]: 'builtin', [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',
};

View File

@ -1,6 +1,7 @@
import { Authorization } from '@/constants/authorization'; import { Authorization } from '@/constants/authorization';
import { MessageType } from '@/constants/chat'; import { MessageType } from '@/constants/chat';
import { LanguageTranslationMap } from '@/constants/common'; import { LanguageTranslationMap } from '@/constants/common';
import { Pagination } from '@/interfaces/common';
import { ResponseType } from '@/interfaces/database/base'; import { ResponseType } from '@/interfaces/database/base';
import { import {
IAnswer, IAnswer,
@ -12,7 +13,7 @@ import { IKnowledgeFile } from '@/interfaces/database/knowledge';
import api from '@/utils/api'; import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util'; import { getAuthorization } from '@/utils/authorization-util';
import { buildMessageUuid } from '@/utils/chat'; import { buildMessageUuid } from '@/utils/chat';
import { PaginationProps, message } from 'antd'; import { message } from 'antd';
import { FormInstance } from 'antd/lib'; import { FormInstance } from 'antd/lib';
import axios from 'axios'; import axios from 'axios';
import { EventSourceParserStream } from 'eventsource-parser/stream'; import { EventSourceParserStream } from 'eventsource-parser/stream';
@ -71,8 +72,8 @@ export const useGetPaginationWithRouter = () => {
size: pageSize, size: pageSize,
} = useSetPaginationParams(); } = useSetPaginationParams();
const onPageChange: PaginationProps['onChange'] = useCallback( const onPageChange: Pagination['onChange'] = useCallback(
(pageNumber: number, pageSize: number) => { (pageNumber: number, pageSize?: number) => {
setPaginationParams(pageNumber, pageSize); setPaginationParams(pageNumber, pageSize);
}, },
[setPaginationParams], [setPaginationParams],
@ -88,7 +89,7 @@ export const useGetPaginationWithRouter = () => {
[setPaginationParams, pageSize], [setPaginationParams, pageSize],
); );
const pagination: PaginationProps = useMemo(() => { const pagination: Pagination = useMemo(() => {
return { return {
showQuickJumper: true, showQuickJumper: true,
total: 0, total: 0,
@ -97,7 +98,7 @@ export const useGetPaginationWithRouter = () => {
pageSize: pageSize, pageSize: pageSize,
pageSizeOptions: [1, 2, 10, 20, 50, 100], pageSizeOptions: [1, 2, 10, 20, 50, 100],
onChange: onPageChange, onChange: onPageChange,
showTotal: (total) => `${t('total')} ${total}`, showTotal: (total: number) => `${t('total')} ${total}`,
}; };
}, [t, onPageChange, page, pageSize]); }, [t, onPageChange, page, pageSize]);
@ -109,7 +110,7 @@ export const useGetPaginationWithRouter = () => {
export const useHandleSearchChange = () => { export const useHandleSearchChange = () => {
const [searchString, setSearchString] = useState(''); const [searchString, setSearchString] = useState('');
const { setPagination } = useGetPaginationWithRouter(); const { pagination, setPagination } = useGetPaginationWithRouter();
const handleInputChange = useCallback( const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target.value; const value = e.target.value;
@ -119,21 +120,21 @@ export const useHandleSearchChange = () => {
[setPagination], [setPagination],
); );
return { handleInputChange, searchString }; return { handleInputChange, searchString, pagination, setPagination };
}; };
export const useGetPagination = () => { export const useGetPagination = () => {
const [pagination, setPagination] = useState({ page: 1, pageSize: 10 }); const [pagination, setPagination] = useState({ page: 1, pageSize: 10 });
const { t } = useTranslate('common'); const { t } = useTranslate('common');
const onPageChange: PaginationProps['onChange'] = useCallback( const onPageChange: Pagination['onChange'] = useCallback(
(pageNumber: number, pageSize: number) => { (pageNumber: number, pageSize: number) => {
setPagination({ page: pageNumber, pageSize }); setPagination({ page: pageNumber, pageSize });
}, },
[], [],
); );
const currentPagination: PaginationProps = useMemo(() => { const currentPagination: Pagination = useMemo(() => {
return { return {
showQuickJumper: true, showQuickJumper: true,
total: 0, total: 0,
@ -142,7 +143,7 @@ export const useGetPagination = () => {
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
pageSizeOptions: [1, 2, 10, 20, 50, 100], pageSizeOptions: [1, 2, 10, 20, 50, 100],
onChange: onPageChange, onChange: onPageChange,
showTotal: (total) => `${t('total')} ${total}`, showTotal: (total: number) => `${t('total')} ${total}`,
}; };
}, [t, onPageChange, pagination]); }, [t, onPageChange, pagination]);

View File

@ -25,6 +25,17 @@ export const useNavigatePage = () => {
[navigate], [navigate],
); );
const navigateToMemoryList = useCallback(
({ isCreate = false }: { isCreate?: boolean }) => {
if (isCreate) {
navigate(Routes.Memories + '?isCreate=true');
} else {
navigate(Routes.Memories);
}
},
[navigate],
);
const navigateToDataset = useCallback( const navigateToDataset = useCallback(
(id: string) => () => { (id: string) => () => {
// navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`); // navigate(`${Routes.DatasetBase}${Routes.DataSetOverview}/${id}`);
@ -105,6 +116,12 @@ export const useNavigatePage = () => {
}, },
[navigate], [navigate],
); );
const navigateToMemory = useCallback(
(id: string) => () => {
navigate(`${Routes.Memory}${Routes.MemoryMessage}/${id}`);
},
[navigate],
);
const navigateToChunkParsedResult = useCallback( const navigateToChunkParsedResult = useCallback(
(id: string, knowledgeId?: string) => () => { (id: string, knowledgeId?: string) => () => {
@ -196,5 +213,7 @@ export const useNavigatePage = () => {
navigateToDataflowResult, navigateToDataflowResult,
navigateToDataFile, navigateToDataFile,
navigateToDataSourceDetail, navigateToDataSourceDetail,
navigateToMemory,
navigateToMemoryList,
}; };
}; };

View File

@ -2,6 +2,7 @@ export interface Pagination {
current: number; current: number;
pageSize: number; pageSize: number;
total: number; total: number;
onChange?: (page: number, pageSize: number) => void;
} }
export interface BaseState { export interface BaseState {

View File

@ -99,6 +99,29 @@ export default {
search: 'Search', search: 'Search',
welcome: 'Welcome to', welcome: 'Welcome to',
dataset: 'Dataset', 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: { knowledgeList: {
welcome: 'Welcome back', welcome: 'Welcome back',
@ -2044,14 +2067,21 @@ Important structured information may include: names, dates, locations, events, k
delFilesContent: 'Selected {{count}} files', delFilesContent: 'Selected {{count}} files',
delChat: 'Delete chat', delChat: 'Delete chat',
delMember: 'Delete member', delMember: 'Delete member',
delMemory: 'Delete memory',
}, },
empty: { empty: {
noMCP: 'No MCP servers available', noMCP: 'No MCP servers available',
agentTitle: 'No agent app created yet', agentTitle: 'No agent app created yet',
notFoundAgent: 'Agent app not found',
datasetTitle: 'No dataset created yet', datasetTitle: 'No dataset created yet',
notFoundDataset: 'Dataset not found',
chatTitle: 'No chat app created yet', chatTitle: 'No chat app created yet',
notFoundChat: 'Chat app not found',
searchTitle: 'No search app created yet', searchTitle: 'No search app created yet',
notFoundSearch: 'Search app not found',
memoryTitle: 'No memory created yet',
notFoundMemory: 'Memory not found',
addNow: 'Add Now', addNow: 'Add Now',
}, },

View File

@ -1900,9 +1900,15 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
empty: { empty: {
noMCP: '暂无 MCP 服务器可用', noMCP: '暂无 MCP 服务器可用',
agentTitle: '尚未创建智能体', agentTitle: '尚未创建智能体',
notFoundAgent: '未查询到智能体',
datasetTitle: '尚未创建数据集', datasetTitle: '尚未创建数据集',
notFoundDataset: '未查询到数据集',
chatTitle: '尚未创建聊天应用', chatTitle: '尚未创建聊天应用',
notFoundChat: '未查询到聊天应用',
searchTitle: '尚未创建搜索应用', searchTitle: '尚未创建搜索应用',
notFoundSearch: '未查询到搜索应用',
memoryTitle: '尚未创建记忆',
notFoundMemory: '未查询到记忆',
addNow: '立即添加', addNow: '立即添加',
}, },
}, },

View File

@ -81,19 +81,20 @@ export default function Agents() {
}, [isCreate, showCreatingModal, searchUrl, setSearchUrl]); }, [isCreate, showCreatingModal, searchUrl, setSearchUrl]);
return ( return (
<> <>
{(!data?.length || data?.length <= 0) && ( {(!data?.length || data?.length <= 0) && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]"> <div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard <EmptyAppCard
showIcon showIcon
size="large" size="large"
className="w-[480px] p-14" className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Agent} type={EmptyCardType.Agent}
onClick={() => showCreatingModal()} onClick={() => showCreatingModal()}
/> />
</div> </div>
)} )}
<section className="flex flex-col w-full flex-1"> <section className="flex flex-col w-full flex-1">
{!!data?.length && ( {(!!data?.length || searchString) && (
<> <>
<div className="px-8 pt-8 "> <div className="px-8 pt-8 ">
<ListFilterBar <ListFilterBar
@ -138,6 +139,18 @@ export default function Agents() {
</DropdownMenu> </DropdownMenu>
</ListFilterBar> </ListFilterBar>
</div> </div>
{(!data?.length || data?.length <= 0) && searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Agent}
onClick={() => showCreatingModal()}
/>
</div>
)}
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8"> <CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{data.map((x) => { {data.map((x) => {

View File

@ -12,7 +12,7 @@ import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useFormContext } from 'react-hook-form'; import { FieldValues, useFormContext } from 'react-hook-form';
import { import {
useHandleKbEmbedding, useHandleKbEmbedding,
useHasParsedDocument, 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 { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext(); const form = useFormContext();
const embeddingModelOptions = useSelectEmbeddingModelOptions(); const embeddingModelOptions = useSelectEmbeddingModelOptions();
const { handleChange } = useHandleKbEmbedding(); const { handleChange } = useHandleKbEmbedding();
const disabled = useHasParsedDocument(isEdit); const disabled = useHasParsedDocument(isEdit);
const oldValue = useMemo(() => { const oldValue = useMemo(() => {
const embdStr = form.getValues('embd_id'); const embdStr = form.getValues(name || 'embd_id');
return embdStr || ''; return embdStr || '';
}, [form]); }, [form]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
return (
<Spin
spinning={loading}
className={cn(' rounded-lg after:bg-bg-base', {
'opacity-20': loading,
})}
>
<SelectWithSearch
onChange={async (value) => {
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')}
/>
</Spin>
);
};
export function EmbeddingModelItem({ line = 1, isEdit }: IProps) {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return ( return (
<> <>
<FormField <FormField
@ -102,33 +144,10 @@ export function EmbeddingModelItem({ line = 1, isEdit }: IProps) {
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })} className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
> >
<FormControl> <FormControl>
<Spin <EmbeddingSelect
spinning={loading} isEdit={!!isEdit}
className={cn(' rounded-lg after:bg-bg-base', { field={field}
'opacity-20': loading, ></EmbeddingSelect>
})}
>
<SelectWithSearch
onChange={async (value) => {
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"
/>
</Spin>
</FormControl> </FormControl>
</div> </div>
</div> </div>

View File

@ -70,18 +70,19 @@ export default function Datasets() {
return ( return (
<> <>
<section className="py-4 flex-1 flex flex-col"> <section className="py-4 flex-1 flex flex-col">
{(!kbs?.length || kbs?.length <= 0) && ( {(!kbs?.length || kbs?.length <= 0) && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]"> <div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard <EmptyAppCard
showIcon showIcon
size="large" size="large"
className="w-[480px] p-14" className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Dataset} type={EmptyCardType.Dataset}
onClick={() => showModal()} onClick={() => showModal()}
/> />
</div> </div>
)} )}
{!!kbs?.length && ( {(!!kbs?.length || searchString) && (
<> <>
<ListFilterBar <ListFilterBar
title={t('header.dataset')} title={t('header.dataset')}
@ -98,6 +99,18 @@ export default function Datasets() {
{t('knowledgeList.createKnowledgeBase')} {t('knowledgeList.createKnowledgeBase')}
</Button> </Button>
</ListFilterBar> </ListFilterBar>
{(!kbs?.length || kbs?.length <= 0) && searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Dataset}
onClick={() => showModal()}
/>
</div>
)}
<div className="flex-1"> <div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8"> <CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{kbs.map((dataset) => { {kbs.map((dataset) => {

View File

@ -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<FormFieldConfig[]>(createMemoryFields);
// const formRef = useRef<DynamicFormRef>(null);
const [formInstance, setFormInstance] = useState<DynamicFormRef | null>(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 (
<Modal
open={open}
onOpenChange={onClose}
className="!w-[480px]"
title={
<div className="flex flex-col">
<div>
<HomeIcon name="memory" width={'24'} />
</div>
{t('memory.createMemory')}
</div>
}
showfooter={false}
confirmLoading={props.loading}
>
<DynamicForm.Root
ref={formCallbackRef}
fields={createMemoryFields}
onSubmit={() => {}}
defaultValues={initialMemory}
>
<div className="flex justify-end gap-2 pb-5">
<DynamicForm.CancelButton handleCancel={onClose} />
<DynamicForm.SavingButton
submitLoading={false}
submitFunc={(data) => {
onSubmit?.(data);
}}
/>
</div>
</DynamicForm.Root>
</Modal>
);
};

View File

@ -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) => <EmbeddingSelect field={field} isEdit={false} />,
},
{
name: 'llm',
label: t('memory.llm'),
placeholder: t('memory.selectModel'),
required: true,
type: FormFieldType.Select,
},
] as FormFieldConfig[];

View File

@ -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<CreateMemoryResponse, Error, ICreateMemoryProps>({
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<MemoryDetailResponse, Error>({
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<DeleteMemoryResponse, Error, DeleteMemoryProps>({
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<any, Error, IMemoryAppDetailProps>({
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<IMemory>({} 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,
};
};

View File

@ -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 (
<section className="w-full h-full flex flex-col">
{(!list?.data?.memory_list?.length ||
list?.data?.memory_list?.length <= 0) &&
!searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
{(!!list?.data?.memory_list?.length || searchString) && (
<>
<div className="px-8 pt-8">
<ListFilterBar
icon="memory"
title={t('memory')}
showFilter={false}
onSearchChange={handleInputChange}
searchString={searchString}
>
<Button
variant={'default'}
onClick={() => {
openCreateModalFun();
}}
>
<Plus className="mr-2 h-4 w-4" />
{t('createMemory')}
</Button>
</ListFilterBar>
</div>
{(!list?.data?.memory_list?.length ||
list?.data?.memory_list?.length <= 0) &&
searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Memory}
onClick={() => openCreateModalFun()}
/>
</div>
)}
<div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{list?.data.memory_list.map((x) => {
return (
<MemoryCard
key={x.id}
data={x}
showMemoryRenameModal={() => {
showMemoryRenameModal(x);
}}
></MemoryCard>
);
})}
</CardContainer>
</div>
{list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
// total={pagination.total}
total={list?.data.total}
onChange={handlePageChange}
/>
</div>
)}
</>
)}
{/* {openCreateModal && (
<RenameDialog
hideModal={hideMemoryRenameModal}
onOk={onMemoryRenameConfirm}
initialName={initialMemoryName}
loading={searchRenameLoading}
title={<HomeIcon name="memory" width={'24'} />}
></RenameDialog>
)} */}
{openCreateModal && (
<AddOrEditModal
initialMemory={initialMemory}
open={openCreateModal}
loading={searchRenameLoading}
onClose={hideMemoryModal}
onSubmit={onMemoryConfirm}
/>
)}
</section>
);
}

View File

@ -0,0 +1,121 @@
export interface ICreateMemoryProps {
memory_name: string;
memory_type: Array<string>;
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<IMemory>;
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<IMemoryAppDetailProps, 'id'> & {
// id: string;
// };

View File

@ -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 (
<HomeCard
data={{
name: data?.name,
avatar: data?.avatar,
description: data?.description,
}}
moreDropdown={
<MemoryDropdown
dataset={data}
showMemoryRenameModal={showMemoryRenameModal}
>
<MoreButton></MoreButton>
</MemoryDropdown>
}
onClick={navigateToMemory(data?.id)}
/>
);
}

View File

@ -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<HTMLDivElement> =
useCallback(
(e) => {
e.stopPropagation();
showMemoryRenameModal(dataset);
},
[dataset, showMemoryRenameModal],
);
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
deleteMemory({ search_id: dataset.id });
}, [dataset.id, deleteMemory]);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={handleShowChatRenameModal}>
{t('common.rename')} <PenLine />
</DropdownMenuItem>
<DropdownMenuSeparator />
<ConfirmDeleteDialog
onOk={handleDelete}
title={t('deleteModal.delMemory')}
content={{
node: (
<ConfirmDeleteDialogNode
avatar={{ avatar: dataset.avatar, name: dataset.name }}
name={dataset.name}
/>
),
}}
>
<DropdownMenuItem
className="text-state-error"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
{t('common.delete')} <Trash2 />
</DropdownMenuItem>
</ConfirmDeleteDialog>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,3 @@
export enum MemoryApiAction {
FetchMemoryDetail = 'fetchMemoryDetail',
}

View File

@ -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<IMessageTableProps>({
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,
};
};

View File

@ -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<IMemory>({
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,
};
};

View File

@ -0,0 +1,17 @@
import Spotlight from '@/components/spotlight';
import { Outlet } from 'umi';
import { SideBar } from './sidebar';
export default function DatasetWrapper() {
return (
<section className="flex h-full flex-col w-full">
<div className="flex flex-1 min-h-0">
<SideBar></SideBar>
<div className=" relative flex-1 overflow-auto border-[0.5px] border-border-button p-5 rounded-md mr-5 mb-5">
<Spotlight />
<Outlet />
</div>
</div>
</section>
);
}

View File

@ -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 (
<div className="flex flex-col gap-2">
<ListFilterBar
title="Dataset"
onSearchChange={handleInputChange}
searchString={searchString}
// value={filterValue}
// onChange={handleFilterSubmit}
// onOpenChange={onOpenChange}
// filters={filters}
leftPanel={
<div className="items-start">
<div className="pb-1">{t('knowledgeDetails.subbarFiles')}</div>
<div className="text-text-secondary text-sm">
{t('knowledgeDetails.datasetDescription')}
</div>
</div>
}
></ListFilterBar>
<MemoryTable
messages={data?.messages?.message_list ?? []}
pagination={pagination}
setPagination={setPagination}
total={data?.messages?.total ?? 0}
// rowSelection={rowSelection}
// setRowSelection={setRowSelection}
// loading={loading}
></MemoryTable>
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full bg-text ">message</div>
</div>
</div>
);
}

View File

@ -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<IMessageInfo>; total: number };
storage_type: string;
}

View File

@ -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<IMessageInfo>;
total: number;
pagination: Pagination;
setPagination: (params: { page: number; pageSize: number }) => void;
};
export function MemoryTable({
messages,
total,
pagination,
setPagination,
}: MemoryTableProps) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
// Define columns for the memory table
const columns: ColumnDef<IMessageInfo>[] = useMemo(
() => [
{
accessorKey: 'session_id',
header: () => <span>{t('memoryDetail.messages.sessionId')}</span>,
cell: ({ row }) => (
<div className="text-sm font-medium ">
{row.getValue('session_id')}
</div>
),
},
{
accessorKey: 'agent_name',
header: () => <span>{t('memoryDetail.messages.agent')}</span>,
cell: ({ row }) => (
<div className="text-sm font-medium ">
{row.getValue('agent_name')}
</div>
),
},
{
accessorKey: 'message_type',
header: () => <span>{t('memoryDetail.messages.type')}</span>,
cell: ({ row }) => (
<div className="text-sm font-medium capitalize">
{row.getValue('message_type')}
</div>
),
},
{
accessorKey: 'valid_at',
header: () => <span>{t('memoryDetail.messages.validDate')}</span>,
cell: ({ row }) => (
<div className="text-sm ">{row.getValue('valid_at')}</div>
),
},
{
accessorKey: 'forget_at',
header: () => <span>{t('memoryDetail.messages.forgetAt')}</span>,
cell: ({ row }) => (
<div className="text-sm ">{row.getValue('forget_at')}</div>
),
},
{
accessorKey: 'source_id',
header: () => <span>{t('memoryDetail.messages.source')}</span>,
cell: ({ row }) => (
<div className="text-sm ">{row.getValue('source_id')}</div>
),
},
{
accessorKey: 'status',
header: () => <span>{t('memoryDetail.messages.enable')}</span>,
cell: ({ row }) => {
const isEnabled = row.getValue('status') as boolean;
return (
<div className="flex items-center">
<Switch defaultChecked={isEnabled} onChange={() => {}} />
</div>
);
},
},
{
accessorKey: 'action',
header: () => <span>{t('memoryDetail.messages.action')}</span>,
meta: {
cellClassName: 'w-12',
},
cell: () => (
<div className=" hidden group-hover:flex">
<Button variant={'ghost'} className="bg-transparent">
<TextSelect />
</Button>
<Button
variant={'delete'}
className="bg-transparent"
aria-label="Edit"
>
<Eraser />
</Button>
</div>
),
},
],
[],
);
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 (
<div className="w-full">
<Table rootClassName="max-h-[calc(100vh-222px)]">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody className="relative">
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
className="group"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
<Empty type={EmptyType.Data} />
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<div className="flex items-center justify-end py-4 absolute bottom-3 right-3">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={total}
onChange={(page, pageSize) => {
setPagination({ page, pageSize });
}}
/>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
export default function MemoryMessage() {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full bg-text-secondary">11</div>
<div className="h-4 w-4 rounded-full bg-text-secondary">11</div>
</div>
<div className="flex items-center gap-2">
<div className="h-4 w-4 rounded-full bg-text ">setting</div>
</div>
</div>
);
}

View File

@ -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 };
};

View File

@ -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: <Logs className="size-4" />,
label: t(`knowledgeDetails.overview`),
key: Routes.MemoryMessage,
},
{
icon: <Banknote className="size-4" />,
label: t(`knowledgeDetails.configuration`),
key: Routes.MemorySetting,
},
];
return list;
}, [t]);
return (
<aside className="relative p-5 space-y-8">
<div className="flex gap-2.5 max-w-[200px] items-center">
<RAGFlowAvatar
avatar={data.avatar}
name={data.name}
className="size-16"
></RAGFlowAvatar>
<div className=" text-text-secondary text-xs space-y-1 overflow-hidden">
<h3 className="text-lg font-semibold line-clamp-1 text-text-primary text-ellipsis overflow-hidden">
{data.name}
</h3>
<div className="flex justify-between">
<span>
{data.doc_num} {t('knowledgeDetails.files')}
</span>
<span>{formatBytes(data.size)}</span>
</div>
<div>
{t('knowledgeDetails.created')} {formatPureDate(data.create_time)}
</div>
</div>
</div>
<div className="w-[200px] flex flex-col gap-5">
{items.map((item, itemIdx) => {
const active = '/' + pathName === item.key;
return (
<Button
key={itemIdx}
variant={active ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-start gap-2.5 px-3 relative h-10 text-text-secondary',
{
'bg-bg-card': active,
'text-text-primary': active,
},
)}
onClick={handleMenuClick(item.key)}
>
{item.icon}
<span>{item.label}</span>
</Button>
);
})}
</div>
</aside>
);
}

View File

@ -50,18 +50,19 @@ export default function ChatList() {
return ( return (
<section className="flex flex-col w-full flex-1"> <section className="flex flex-col w-full flex-1">
{data.dialogs?.length <= 0 && ( {data.dialogs?.length <= 0 && !searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]"> <div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard <EmptyAppCard
showIcon showIcon
size="large" size="large"
className="w-[480px] p-14" className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Chat} type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()} onClick={() => handleShowCreateModal()}
/> />
</div> </div>
)} )}
{data.dialogs?.length > 0 && ( {(data.dialogs?.length > 0 || searchString) && (
<> <>
<div className="px-8 pt-8"> <div className="px-8 pt-8">
<ListFilterBar <ListFilterBar
@ -76,6 +77,18 @@ export default function ChatList() {
</Button> </Button>
</ListFilterBar> </ListFilterBar>
</div> </div>
{data.dialogs?.length <= 0 && searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
/>
</div>
)}
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8"> <CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{data.dialogs.map((x) => { {data.dialogs.map((x) => {

View File

@ -2,9 +2,11 @@
import message from '@/components/ui/message'; import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { useHandleSearchChange } from '@/hooks/logic-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import searchService from '@/services/search-service'; import searchService from '@/services/search-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useDebounce } from 'ahooks';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'umi'; import { useParams, useSearchParams } from 'umi';
@ -84,21 +86,34 @@ interface SearchListResponse {
message: string; message: string;
} }
export const useFetchSearchList = (params?: SearchListParams) => { export const useFetchSearchList = () => {
const [searchParams, setSearchParams] = useState<SearchListParams>({ const { handleInputChange, searchString, pagination, setPagination } =
page: 1, useHandleSearchChange();
page_size: 50, const debouncedSearchString = useDebounce(searchString, { wait: 500 });
...params,
});
const { data, isLoading, isError, refetch } = useQuery< const { data, isLoading, isError, refetch } = useQuery<
SearchListResponse, SearchListResponse,
Error Error
>({ >({
queryKey: ['searchList', searchParams], queryKey: [
'searchList',
{
debouncedSearchString,
...pagination,
},
],
queryFn: async () => { queryFn: async () => {
const { data: response } = const { data: response } = await searchService.getSearchList(
await searchService.getSearchList(searchParams); {
params: {
keywords: debouncedSearchString,
page_size: pagination.pageSize,
page: pagination.current,
},
data: {},
},
true,
);
if (response.code !== 0) { if (response.code !== 0) {
throw new Error(response.message || 'Failed to fetch search list'); 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 { return {
data, data,
isLoading, isLoading,
isError, isError,
searchParams, pagination,
setSearchListParams, searchString,
handleInputChange,
setPagination,
refetch, refetch,
}; };
}; };

View File

@ -7,6 +7,7 @@ import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination'; import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { pick } from 'lodash';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
@ -19,10 +20,13 @@ export default function SearchList() {
// const [isEdit, setIsEdit] = useState(false); // const [isEdit, setIsEdit] = useState(false);
const { const {
data: list, data: list,
searchParams, pagination,
setSearchListParams, searchString,
handleInputChange,
setPagination,
refetch: refetchList, refetch: refetchList,
} = useFetchSearchList(); } = useFetchSearchList();
const { const {
openCreateModal, openCreateModal,
showSearchRenameModal, showSearchRenameModal,
@ -32,9 +36,9 @@ export default function SearchList() {
initialSearchName, initialSearchName,
} = useRenameSearch(); } = useRenameSearch();
const handleSearchChange = (value: string) => { // const handleSearchChange = (value: string) => {
console.log(value); // console.log(value);
}; // };
const onSearchRenameConfirm = (name: string) => { const onSearchRenameConfirm = (name: string) => {
onSearchRenameOk(name, () => { onSearchRenameOk(name, () => {
refetchList(); refetchList();
@ -44,10 +48,12 @@ export default function SearchList() {
// setIsEdit(false); // setIsEdit(false);
showSearchRenameModal(); showSearchRenameModal();
}, [showSearchRenameModal]); }, [showSearchRenameModal]);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = useCallback(
// setIsEdit(false); (page: number, pageSize?: number) => {
setSearchListParams({ ...searchParams, page, page_size: pageSize }); setPagination({ page, pageSize });
}; },
[setPagination],
);
const [searchUrl, setSearchUrl] = useSearchParams(); const [searchUrl, setSearchUrl] = useSearchParams();
const isCreate = searchUrl.get('isCreate') === 'true'; const isCreate = searchUrl.get('isCreate') === 'true';
@ -62,25 +68,28 @@ export default function SearchList() {
return ( return (
<section className="w-full h-full flex flex-col"> <section className="w-full h-full flex flex-col">
{(!list?.data?.search_apps?.length || {(!list?.data?.search_apps?.length ||
list?.data?.search_apps?.length <= 0) && ( list?.data?.search_apps?.length <= 0) &&
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]"> !searchString && (
<EmptyAppCard <div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
showIcon <EmptyAppCard
size="large" showIcon
className="w-[480px] p-14" size="large"
type={EmptyCardType.Search} className="w-[480px] p-14"
onClick={() => openCreateModalFun()} type={EmptyCardType.Search}
/> isSearch={!!searchString}
</div> onClick={() => openCreateModalFun()}
)} />
{!!list?.data?.search_apps?.length && ( </div>
)}
{(!!list?.data?.search_apps?.length || searchString) && (
<> <>
<div className="px-8 pt-8"> <div className="px-8 pt-8">
<ListFilterBar <ListFilterBar
icon="searches" icon="searches"
title={t('searchApps')} title={t('searchApps')}
showFilter={false} showFilter={false}
onSearchChange={(e) => handleSearchChange(e.target.value)} searchString={searchString}
onSearchChange={handleInputChange}
> >
<Button <Button
variant={'default'} variant={'default'}
@ -93,6 +102,20 @@ export default function SearchList() {
</Button> </Button>
</ListFilterBar> </ListFilterBar>
</div> </div>
{(!list?.data?.search_apps?.length ||
list?.data?.search_apps?.length <= 0) &&
searchString && (
<div className="flex w-full items-center justify-center h-[calc(100vh-164px)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
type={EmptyCardType.Search}
isSearch={!!searchString}
onClick={() => openCreateModalFun()}
/>
</div>
)}
<div className="flex-1"> <div className="flex-1">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8"> <CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{list?.data.search_apps.map((x) => { {list?.data.search_apps.map((x) => {
@ -111,8 +134,8 @@ export default function SearchList() {
{list?.data.total && list?.data.total > 0 && ( {list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4"> <div className="px-8 mb-4">
<RAGFlowPagination <RAGFlowPagination
current={searchParams.page} {...pick(pagination, 'current', 'pageSize')}
pageSize={searchParams.page_size} // total={pagination.total}
total={list?.data.total} total={list?.data.total}
onChange={handlePageChange} onChange={handlePageChange}
/> />

View File

@ -1,9 +1,10 @@
import { useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { ControllerRenderProps, useFormContext } from 'react-hook-form'; import { ControllerRenderProps, useFormContext } from 'react-hook-form';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { debounce } from 'lodash';
/* ---------------- Token Field ---------------- */ /* ---------------- Token Field ---------------- */
@ -48,15 +49,15 @@ type ConfluenceIndexingMode = 'everything' | 'space' | 'page';
export type ConfluenceIndexingModeFieldProps = ControllerRenderProps; export type ConfluenceIndexingModeFieldProps = ControllerRenderProps;
export const ConfluenceIndexingModeField = ( export const ConfluenceIndexingModeField = (
fieldProps: ConfluenceIndexingModeFieldProps, fieldProps: ControllerRenderProps,
) => { ) => {
const { value, onChange, disabled } = fieldProps; const { value, onChange, disabled } = fieldProps;
const [mode, setMode] = useState<ConfluenceIndexingMode>(
value || 'everything',
);
const { watch, setValue } = useFormContext(); const { watch, setValue } = useFormContext();
const mode = useMemo<ConfluenceIndexingMode>( useEffect(() => setMode(value), [value]);
() => (value as ConfluenceIndexingMode) || 'everything',
[value],
);
const spaceValue = watch('config.space'); const spaceValue = watch('config.space');
const pageIdValue = watch('config.page_id'); const pageIdValue = watch('config.page_id');
@ -66,27 +67,40 @@ export const ConfluenceIndexingModeField = (
if (!value) onChange('everything'); if (!value) onChange('everything');
}, [value, onChange]); }, [value, onChange]);
const handleModeChange = (nextMode?: string) => { const handleModeChange = useCallback(
const normalized = (nextMode || 'everything') as ConfluenceIndexingMode; (nextMode?: string) => {
onChange(normalized); 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') { const debouncedHandleChange = useMemo(
setValue('config.space', '', { shouldDirty: true, shouldTouch: true }); () =>
setValue('config.page_id', '', { shouldDirty: true, shouldTouch: true }); debounce(() => {
setValue('config.index_recursively', false, { handleModeChange();
shouldDirty: true, }, 300),
shouldTouch: true, [handleModeChange],
}); );
} 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 });
}
};
return ( return (
<div className="w-full rounded-lg border border-border-button bg-bg-card p-4 space-y-4"> <div className="w-full rounded-lg border border-border-button bg-bg-card p-4 space-y-4">
@ -127,12 +141,11 @@ export const ConfluenceIndexingModeField = (
<Input <Input
className="w-full" className="w-full"
value={spaceValue ?? ''} value={spaceValue ?? ''}
onChange={(e) => onChange={(e) => {
setValue('config.space', e.target.value, { const value = e.target.value;
shouldDirty: true, setValue('config.space', value);
shouldTouch: true, debouncedHandleChange();
}) }}
}
placeholder="e.g. KB" placeholder="e.g. KB"
disabled={disabled} disabled={disabled}
/> />
@ -148,12 +161,10 @@ export const ConfluenceIndexingModeField = (
<Input <Input
className="w-full" className="w-full"
value={pageIdValue ?? ''} value={pageIdValue ?? ''}
onChange={(e) => onChange={(e) => {
setValue('config.page_id', e.target.value, { setValue('config.page_id', e.target.value);
shouldDirty: true, debouncedHandleChange();
shouldTouch: true, }}
})
}
placeholder="e.g. 123456" placeholder="e.g. 123456"
disabled={disabled} disabled={disabled}
/> />
@ -164,12 +175,10 @@ export const ConfluenceIndexingModeField = (
<div className="flex items-center gap-2 pt-2"> <div className="flex items-center gap-2 pt-2">
<Checkbox <Checkbox
checked={Boolean(indexRecursively)} checked={Boolean(indexRecursively)}
onCheckedChange={(checked) => onCheckedChange={(checked) => {
setValue('config.index_recursively', Boolean(checked), { setValue('config.index_recursively', Boolean(checked));
shouldDirty: true, debouncedHandleChange();
shouldTouch: true, }}
})
}
disabled={disabled} disabled={disabled}
/> />
<span className="text-sm text-text-secondary"> <span className="text-sm text-text-secondary">

View File

@ -1,6 +1,7 @@
import { FormFieldType } from '@/components/dynamic-form'; import { FormFieldType } from '@/components/dynamic-form';
import SvgIcon from '@/components/svg-icon'; import SvgIcon from '@/components/svg-icon';
import { t } from 'i18next'; import { t } from 'i18next';
import { ControllerRenderProps } from 'react-hook-form';
import { ConfluenceIndexingModeField } from './component/confluence-token-field'; import { ConfluenceIndexingModeField } from './component/confluence-token-field';
import GmailTokenField from './component/gmail-token-field'; import GmailTokenField from './component/gmail-token-field';
import GoogleDriveTokenField from './component/google-drive-token-field'; import GoogleDriveTokenField from './component/google-drive-token-field';
@ -237,7 +238,9 @@ export const DataSourceFormFields = {
required: false, required: false,
horizontal: true, horizontal: true,
labelClassName: 'self-start pt-4', labelClassName: 'self-start pt-4',
render: (fieldProps) => <ConfluenceIndexingModeField {...fieldProps} />, render: (fieldProps: ControllerRenderProps) => (
<ConfluenceIndexingModeField {...fieldProps} />
),
}, },
{ {
label: 'Space Key', label: 'Space Key',
@ -598,6 +601,7 @@ export const DataSourceFormDefaultValues = {
confluence_username: '', confluence_username: '',
confluence_access_token: '', confluence_access_token: '',
}, },
index_mode: 'everything',
}, },
}, },
[DataSourceKey.GOOGLE_DRIVE]: { [DataSourceKey.GOOGLE_DRIVE]: {

View File

@ -136,7 +136,7 @@ const SourceDetailPage = () => {
...customFields, ...customFields,
] as FormFieldConfig[]; ] as FormFieldConfig[];
const neweFields = fields.map((field) => { const newFields = fields.map((field) => {
return { return {
...field, ...field,
horizontal: true, horizontal: true,
@ -145,7 +145,7 @@ const SourceDetailPage = () => {
}, },
}; };
}); });
setFields(neweFields); setFields(newFields);
const defultValueTemp = { const defultValueTemp = {
...(DataSourceFormDefaultValues[ ...(DataSourceFormDefaultValues[

View File

@ -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 (
<div className="flex items-center space-x-3 mb-3">
<LlmIcon name={name} imgClass="h-8 w-8 text-text-primary" />
<div className="flex flex-1 gap-1 items-center">
<div className="font-normal text-base truncate">{name}</div>
{!!APIMapUrl[name as keyof typeof APIMapUrl] && (
<Button
variant={'ghost'}
className=" bg-transparent w-4 h-5"
onClick={(e) => {
e.stopPropagation();
window.open(APIMapUrl[name as keyof typeof APIMapUrl]);
}}
// target="_blank"
rel="noopener noreferrer"
>
<ArrowUpRight size={16} />
</Button>
)}
</div>
<Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex">
<Plus size={12} />
{t('addTheModel')}
</Button>
</div>
);
};

View File

@ -2,9 +2,10 @@
import { LlmIcon } from '@/components/svg-icon'; import { LlmIcon } from '@/components/svg-icon';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input'; import { SearchInput } from '@/components/ui/input';
import { APIMapUrl } from '@/constants/llm';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmList } from '@/hooks/use-llm-request'; import { useSelectLlmList } from '@/hooks/use-llm-request';
import { Plus } from 'lucide-react'; import { ArrowUpRight, Plus } from 'lucide-react';
import { FC, useMemo, useState } from 'react'; import { FC, useMemo, useState } from 'react';
type TagType = type TagType =
@ -128,10 +129,26 @@ export const AvailableModels: FC<{
> >
<div className="flex items-center space-x-3 mb-3"> <div className="flex items-center space-x-3 mb-3">
<LlmIcon name={model.name} imgClass="h-8 w-8 text-text-primary" /> <LlmIcon name={model.name} imgClass="h-8 w-8 text-text-primary" />
<div className="flex-1"> <div className="flex flex-1 gap-1 items-center">
<div className="font-normal text-base truncate"> <div className="font-normal text-base truncate">
{model.name} {model.name}
</div> </div>
{!!APIMapUrl[model.name as keyof typeof APIMapUrl] && (
<Button
variant={'ghost'}
className=" bg-transparent w-4 h-5"
onClick={(e) => {
e.stopPropagation();
window.open(
APIMapUrl[model.name as keyof typeof APIMapUrl],
);
}}
// target="_blank"
rel="noopener noreferrer"
>
<ArrowUpRight size={16} />
</Button>
)}
</div> </div>
<Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex"> <Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex">
<Plus size={12} /> <Plus size={12} />

View File

@ -14,6 +14,7 @@ import { useTranslate } from '@/hooks/common-hooks';
import { KeyboardEventHandler, useCallback, useEffect } from 'react'; import { KeyboardEventHandler, useCallback, useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { ApiKeyPostBody } from '../../../interface'; import { ApiKeyPostBody } from '../../../interface';
import { LLMHeader } from '../../components/llm-header';
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> { interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean; loading: boolean;
@ -70,7 +71,7 @@ const ApiKeyModal = ({
return ( return (
<Modal <Modal
title={t('configureModelTitle')} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOpenChange={(open) => !open && hideModal()} onOpenChange={(open) => !open && hideModal()}
onOk={handleOk} onOk={handleOk}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, InputNumber, Modal, Select, Switch } from 'antd'; import { Form, Input, InputNumber, Modal, Select, Switch } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
api_version: string; api_version: string;
@ -57,7 +58,7 @@ const AzureOpenAIModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd'; import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { LLMHeader } from '../../components/llm-header';
import { BedrockRegionList } from '../../constant'; import { BedrockRegionList } from '../../constant';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
@ -42,7 +43,7 @@ const BedrockModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd'; import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
fish_audio_ak: string; fish_audio_ak: string;
@ -39,7 +40,7 @@ const FishAudioModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -2,6 +2,7 @@ import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, InputNumber, Modal, Select } from 'antd'; import { Form, Input, InputNumber, Modal, Select } from 'antd';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
google_project_id: string; google_project_id: string;
@ -41,7 +42,7 @@ const GoogleModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, Modal } from 'antd'; import { Form, Input, Modal } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
vision: boolean; vision: boolean;
@ -46,7 +47,7 @@ const HunyuanModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, Modal, Select, Space } from 'antd'; import { Flex, Form, Input, Modal, Select, Space } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
TencentCloud_sid: string; TencentCloud_sid: string;
@ -45,7 +46,7 @@ const TencentCloudModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -14,6 +14,7 @@ import {
} from 'antd'; } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
vision: boolean; vision: boolean;
@ -147,11 +148,7 @@ const OllamaModal = ({
}; };
return ( return (
<Modal <Modal
title={ title={<LLMHeader name={llmFactory} />}
editMode
? t('editLlmTitle', { name: llmFactory })
: t('addLlmTitle', { name: llmFactory })
}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, InputNumber, Modal, Select } from 'antd'; import { Form, Input, InputNumber, Modal, Select } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
vision: boolean; vision: boolean;
@ -51,7 +52,7 @@ const SparkModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd'; import { Flex, Form, Input, InputNumber, Modal, Select, Space } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
vision: boolean; vision: boolean;
@ -45,7 +46,7 @@ const VolcEngineModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -3,6 +3,7 @@ import { IModalProps } from '@/interfaces/common';
import { IAddLlmRequestBody } from '@/interfaces/request/llm'; import { IAddLlmRequestBody } from '@/interfaces/request/llm';
import { Form, Input, InputNumber, Modal, Select } from 'antd'; import { Form, Input, InputNumber, Modal, Select } from 'antd';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { LLMHeader } from '../../components/llm-header';
type FieldType = IAddLlmRequestBody & { type FieldType = IAddLlmRequestBody & {
vision: boolean; vision: boolean;
@ -49,7 +50,7 @@ const YiyanModal = ({
return ( return (
<Modal <Modal
title={t('addLlmTitle', { name: llmFactory })} title={<LLMHeader name={llmFactory} />}
open={visible} open={visible}
onOk={handleOk} onOk={handleOk}
onCancel={hideModal} onCancel={hideModal}

View File

@ -11,6 +11,10 @@ export enum Routes {
Agent = '/agent', Agent = '/agent',
AgentTemplates = '/agent-templates', AgentTemplates = '/agent-templates',
Agents = '/agents', Agents = '/agents',
Memories = '/memories',
Memory = '/memory',
MemoryMessage = '/memory-message',
MemorySetting = '/memory-setting',
AgentList = '/agent-list', AgentList = '/agent-list',
Searches = '/next-searches', Searches = '/next-searches',
Search = '/next-search', Search = '/next-search',
@ -89,6 +93,7 @@ const routes = [
path: Routes.AgentList, path: Routes.AgentList,
component: `@/pages/${Routes.Agents}`, component: `@/pages/${Routes.Agents}`,
}, },
{ {
path: '/document/:id', path: '/document/:id',
component: '@/pages/document-viewer', component: '@/pages/document-viewer',
@ -149,6 +154,41 @@ const routes = [
}, },
], ],
}, },
{
path: Routes.Memories,
layout: false,
component: '@/layouts/next',
routes: [
{
path: Routes.Memories,
component: `@/pages${Routes.Memories}`,
},
],
},
{
path: `${Routes.Memory}`,
layout: false,
component: '@/layouts/next',
routes: [
{
path: `${Routes.Memory}`,
layout: false,
component: `@/pages${Routes.Memory}`,
routes: [
{
path: `${Routes.Memory}/${Routes.MemoryMessage}/:id`,
component: `@/pages${Routes.Memory}${Routes.MemoryMessage}`,
},
{
path: `${Routes.Memory}/${Routes.MemorySetting}/:id`,
component: `@/pages${Routes.Memory}${Routes.MemorySetting}`,
},
],
},
],
// component: `@/pages${Routes.DatasetBase}`,
// component: `@/pages${Routes.Memory}`,
},
{ {
path: `${Routes.Search}/:id`, path: `${Routes.Search}/:id`,
layout: false, layout: false,

View File

@ -0,0 +1,43 @@
import api from '@/utils/api';
import { registerNextServer } from '@/utils/register-server';
import request from '@/utils/request';
const {
createMemory,
getMemoryList,
deleteMemory,
getMemoryDetail,
updateMemorySetting,
// getMemoryDetailShare,
} = api;
const methods = {
createMemory: {
url: createMemory,
method: 'post',
},
getMemoryList: {
url: getMemoryList,
method: 'post',
},
deleteMemory: { url: deleteMemory, method: 'post' },
// getMemoryDetail: {
// url: getMemoryDetail,
// method: 'get',
// },
// updateMemorySetting: {
// url: updateMemorySetting,
// method: 'post',
// },
// getMemoryDetailShare: {
// url: getMemoryDetailShare,
// method: 'get',
// },
} as const;
const memoryService = registerNextServer<keyof typeof methods>(methods);
export const updateMemoryById = (id: string, data: any) => {
return request.post(updateMemorySetting(id), { data });
};
export const getMemoryDetailById = (id: string, data: any) => {
return request.post(getMemoryDetail(id), { data });
};
export default memoryService;

View File

@ -1,6 +1,5 @@
import api from '@/utils/api'; import api from '@/utils/api';
import registerServer from '@/utils/register-server'; import { registerNextServer } from '@/utils/register-server';
import request from '@/utils/request';
const { const {
createSearch, createSearch,
@ -49,6 +48,6 @@ const methods = {
method: 'get', method: 'get',
}, },
} as const; } as const;
const searchService = registerServer<keyof typeof methods>(methods, request); const searchService = registerNextServer<keyof typeof methods>(methods);
export default searchService; export default searchService;

View File

@ -226,6 +226,13 @@ export default {
getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`, getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`,
retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`, retrievalTestShare: `${ExternalApi}${api_host}/searchbots/retrieval_test`,
// memory
createMemory: `${api_host}/memory/create`,
getMemoryList: `${api_host}/memory/list`,
deleteMemory: (id: string) => `${api_host}/memory/rm/${id}`,
getMemoryDetail: (id: string) => `${api_host}/memory/detail/${id}`,
updateMemorySetting: (id: string) => `${api_host}/memory/update/${id}`,
// data pipeline // data pipeline
fetchDataflow: (id: string) => `${api_host}/dataflow/get/${id}`, fetchDataflow: (id: string) => `${api_host}/dataflow/get/${id}`,
setDataflow: `${api_host}/dataflow/set`, setDataflow: `${api_host}/dataflow/set`,