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

View File

@ -11,23 +11,33 @@ export enum EmptyCardType {
Dataset = 'dataset',
Chat = 'chat',
Search = 'search',
Memory = 'memory',
}
export const EmptyCardData = {
[EmptyCardType.Agent]: {
icon: <HomeIcon name="agents" width={'24'} />,
title: t('empty.agentTitle'),
notFound: t('empty.notFoundAgent'),
},
[EmptyCardType.Dataset]: {
icon: <HomeIcon name="datasets" width={'24'} />,
title: t('empty.datasetTitle'),
notFound: t('empty.notFoundDataset'),
},
[EmptyCardType.Chat]: {
icon: <HomeIcon name="chats" width={'24'} />,
title: t('empty.chatTitle'),
notFound: t('empty.notFoundChat'),
},
[EmptyCardType.Search]: {
icon: <HomeIcon name="searches" width={'24'} />,
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;
showIcon?: boolean;
className?: string;
isSearch?: boolean;
size?: 'small' | 'large';
}) => {
const { type, showIcon, className } = props;
const { type, showIcon, className, isSearch } = props;
let defaultClass = '';
let style = {};
switch (props.size) {
@ -95,19 +96,29 @@ export const EmptyAppCard = (props: {
break;
}
return (
<div className=" cursor-pointer " onClick={props.onClick}>
<div
className=" cursor-pointer "
onClick={isSearch ? undefined : props.onClick}
>
<EmptyCard
icon={showIcon ? EmptyCardData[type].icon : undefined}
title={EmptyCardData[type].title}
title={
isSearch ? EmptyCardData[type].notFound : EmptyCardData[type].title
}
className={className}
style={style}
// description={EmptyCardData[type].description}
>
<div
className={cn(defaultClass, 'flex items-center justify-start w-full')}
>
<Plus size={24} />
</div>
{!isSearch && (
<div
className={cn(
defaultClass,
'flex items-center justify-start w-full',
)}
>
<Plus size={24} />
</div>
)}
</EmptyCard>
</div>
);

View File

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

View File

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

View File

@ -126,3 +126,53 @@ export const IconMap = {
[LLMFactory.JiekouAI]: 'jiekouai',
[LLMFactory.Builtin]: 'builtin',
};
export const APIMapUrl = {
[LLMFactory.OpenAI]: 'https://platform.openai.com/api-keys',
[LLMFactory.Anthropic]: 'https://console.anthropic.com/settings/keys',
[LLMFactory.Gemini]: 'https://aistudio.google.com/app/apikey',
[LLMFactory.DeepSeek]: 'https://platform.deepseek.com/api_keys',
[LLMFactory.Moonshot]: 'https://platform.moonshot.cn/console/api-keys',
[LLMFactory.TongYiQianWen]: 'https://dashscope.console.aliyun.com/apiKey',
[LLMFactory.ZhipuAI]: 'https://open.bigmodel.cn/usercenter/apikeys',
[LLMFactory.XAI]: 'https://x.ai/api/',
[LLMFactory.HuggingFace]: 'https://huggingface.co/settings/tokens',
[LLMFactory.Mistral]: 'https://console.mistral.ai/api-keys/',
[LLMFactory.Cohere]: 'https://dashboard.cohere.com/api-keys',
[LLMFactory.BaiduYiYan]: 'https://wenxin.baidu.com/user/key',
[LLMFactory.Meituan]: 'https://longcat.chat/platform/api_keys',
[LLMFactory.Bedrock]:
'https://us-east-2.console.aws.amazon.com/bedrock/home#/api-keys',
[LLMFactory.AzureOpenAI]:
'https://portal.azure.com/#create/Microsoft.CognitiveServicesOpenAI',
[LLMFactory.OpenRouter]: 'https://openrouter.ai/keys',
[LLMFactory.XunFeiSpark]: 'https://console.xfyun.cn/services/cbm',
[LLMFactory.MiniMax]:
'https://platform.minimaxi.com/user-center/basic-information',
[LLMFactory.Groq]: 'https://console.groq.com/keys',
[LLMFactory.NVIDIA]: 'https://build.nvidia.com/settings/api-keys',
[LLMFactory.SILICONFLOW]: 'https://cloud.siliconflow.cn/account/ak',
[LLMFactory.Replicate]: 'https://replicate.com/account/api-tokens',
[LLMFactory.VolcEngine]: 'https://console.volcengine.com/ark',
[LLMFactory.Jina]: 'https://jina.ai/embeddings/',
[LLMFactory.TencentHunYuan]:
'https://console.cloud.tencent.com/hunyuan/api-key',
[LLMFactory.TencentCloud]: 'https://console.cloud.tencent.com/cam/capi',
[LLMFactory.ModelScope]: 'https://modelscope.cn/my/myaccesstoken',
[LLMFactory.GoogleCloud]: 'https://console.cloud.google.com/apis/credentials',
[LLMFactory.FishAudio]: 'https://fish.audio/app/api-keys/',
[LLMFactory.GiteeAI]:
'https://ai.gitee.com/hhxzgrjn/dashboard/settings/tokens',
[LLMFactory.StepFun]: 'https://platform.stepfun.com/interface-key',
[LLMFactory.BaiChuan]: 'https://platform.baichuan-ai.com/console/apikey',
[LLMFactory.PPIO]: 'https://ppio.com/settings/key-management',
[LLMFactory.VoyageAI]: 'https://dash.voyageai.com/api-keys',
[LLMFactory.TogetherAI]: 'https://api.together.xyz/settings/api-keys',
[LLMFactory.NovitaAI]: 'https://novita.ai/dashboard/key',
[LLMFactory.Upstage]: 'https://console.upstage.ai/api-keys',
[LLMFactory.CometAPI]: 'https://api.cometapi.com/console/token',
[LLMFactory.Ai302]: 'https://302.ai/apis/list',
[LLMFactory.DeerAPI]: 'https://api.deerapi.com/token',
[LLMFactory.TokenPony]: 'https://www.tokenpony.cn/#/user/keys',
[LLMFactory.DeepInfra]: 'https://deepinfra.com/dash/api_keys',
};

View File

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

View File

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

View File

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

View File

@ -99,6 +99,29 @@ export default {
search: 'Search',
welcome: 'Welcome to',
dataset: 'Dataset',
Memories: 'Memory',
},
memory: {
memory: 'Memory',
createMemory: 'Create Memory',
name: 'Name',
memoryNamePlaceholder: 'memory name',
memoryType: 'Memory type',
embeddingModel: 'Embedding model',
selectModel: 'Select model',
llm: 'LLM',
},
memoryDetail: {
messages: {
sessionId: 'Session ID',
agent: 'Agent',
type: 'Type',
validDate: 'Valid date',
forgetAt: 'Forget at',
source: 'Source',
enable: 'Enable',
action: 'Action',
},
},
knowledgeList: {
welcome: 'Welcome back',
@ -2044,14 +2067,21 @@ Important structured information may include: names, dates, locations, events, k
delFilesContent: 'Selected {{count}} files',
delChat: 'Delete chat',
delMember: 'Delete member',
delMemory: 'Delete memory',
},
empty: {
noMCP: 'No MCP servers available',
agentTitle: 'No agent app created yet',
notFoundAgent: 'Agent app not found',
datasetTitle: 'No dataset created yet',
notFoundDataset: 'Dataset not found',
chatTitle: 'No chat app created yet',
notFoundChat: 'Chat app not found',
searchTitle: 'No search app created yet',
notFoundSearch: 'Search app not found',
memoryTitle: 'No memory created yet',
notFoundMemory: 'Memory not found',
addNow: 'Add Now',
},

View File

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

View File

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

View File

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

View File

@ -70,18 +70,19 @@ export default function Datasets() {
return (
<>
<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)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Dataset}
onClick={() => showModal()}
/>
</div>
)}
{!!kbs?.length && (
{(!!kbs?.length || searchString) && (
<>
<ListFilterBar
title={t('header.dataset')}
@ -98,6 +99,18 @@ export default function Datasets() {
{t('knowledgeList.createKnowledgeBase')}
</Button>
</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">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{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 (
<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)]">
<EmptyAppCard
showIcon
size="large"
className="w-[480px] p-14"
isSearch={!!searchString}
type={EmptyCardType.Chat}
onClick={() => handleShowCreateModal()}
/>
</div>
)}
{data.dialogs?.length > 0 && (
{(data.dialogs?.length > 0 || searchString) && (
<>
<div className="px-8 pt-8">
<ListFilterBar
@ -76,6 +77,18 @@ export default function ChatList() {
</Button>
</ListFilterBar>
</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">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{data.dialogs.map((x) => {

View File

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

View File

@ -7,6 +7,7 @@ import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useTranslate } from '@/hooks/common-hooks';
import { pick } from 'lodash';
import { Plus } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { useSearchParams } from 'umi';
@ -19,10 +20,13 @@ export default function SearchList() {
// const [isEdit, setIsEdit] = useState(false);
const {
data: list,
searchParams,
setSearchListParams,
pagination,
searchString,
handleInputChange,
setPagination,
refetch: refetchList,
} = useFetchSearchList();
const {
openCreateModal,
showSearchRenameModal,
@ -32,9 +36,9 @@ export default function SearchList() {
initialSearchName,
} = useRenameSearch();
const handleSearchChange = (value: string) => {
console.log(value);
};
// const handleSearchChange = (value: string) => {
// console.log(value);
// };
const onSearchRenameConfirm = (name: string) => {
onSearchRenameOk(name, () => {
refetchList();
@ -44,10 +48,12 @@ export default function SearchList() {
// setIsEdit(false);
showSearchRenameModal();
}, [showSearchRenameModal]);
const handlePageChange = (page: number, pageSize: number) => {
// setIsEdit(false);
setSearchListParams({ ...searchParams, page, page_size: pageSize });
};
const handlePageChange = useCallback(
(page: number, pageSize?: number) => {
setPagination({ page, pageSize });
},
[setPagination],
);
const [searchUrl, setSearchUrl] = useSearchParams();
const isCreate = searchUrl.get('isCreate') === 'true';
@ -62,25 +68,28 @@ export default function SearchList() {
return (
<section className="w-full h-full flex flex-col">
{(!list?.data?.search_apps?.length ||
list?.data?.search_apps?.length <= 0) && (
<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}
onClick={() => openCreateModalFun()}
/>
</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>
)}
{(!!list?.data?.search_apps?.length || searchString) && (
<>
<div className="px-8 pt-8">
<ListFilterBar
icon="searches"
title={t('searchApps')}
showFilter={false}
onSearchChange={(e) => handleSearchChange(e.target.value)}
searchString={searchString}
onSearchChange={handleInputChange}
>
<Button
variant={'default'}
@ -93,6 +102,20 @@ export default function SearchList() {
</Button>
</ListFilterBar>
</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">
<CardContainer className="max-h-[calc(100dvh-280px)] overflow-auto px-8">
{list?.data.search_apps.map((x) => {
@ -111,8 +134,8 @@ export default function SearchList() {
{list?.data.total && list?.data.total > 0 && (
<div className="px-8 mb-4">
<RAGFlowPagination
current={searchParams.page}
pageSize={searchParams.page_size}
{...pick(pagination, 'current', 'pageSize')}
// total={pagination.total}
total={list?.data.total}
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 { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { debounce } from 'lodash';
/* ---------------- Token Field ---------------- */
@ -48,15 +49,15 @@ type ConfluenceIndexingMode = 'everything' | 'space' | 'page';
export type ConfluenceIndexingModeFieldProps = ControllerRenderProps;
export const ConfluenceIndexingModeField = (
fieldProps: ConfluenceIndexingModeFieldProps,
fieldProps: ControllerRenderProps,
) => {
const { value, onChange, disabled } = fieldProps;
const [mode, setMode] = useState<ConfluenceIndexingMode>(
value || 'everything',
);
const { watch, setValue } = useFormContext();
const mode = useMemo<ConfluenceIndexingMode>(
() => (value as ConfluenceIndexingMode) || 'everything',
[value],
);
useEffect(() => setMode(value), [value]);
const spaceValue = watch('config.space');
const pageIdValue = watch('config.page_id');
@ -66,27 +67,40 @@ export const ConfluenceIndexingModeField = (
if (!value) onChange('everything');
}, [value, onChange]);
const handleModeChange = (nextMode?: string) => {
const normalized = (nextMode || 'everything') as ConfluenceIndexingMode;
onChange(normalized);
const handleModeChange = useCallback(
(nextMode?: string) => {
let normalized: ConfluenceIndexingMode = 'everything';
if (nextMode) {
normalized = nextMode as ConfluenceIndexingMode;
setMode(normalized);
onChange(normalized);
} else {
setMode(mode);
normalized = mode;
onChange(mode);
// onChange(mode);
}
if (normalized === 'everything') {
setValue('config.space', '');
setValue('config.page_id', '');
setValue('config.index_recursively', false);
} else if (normalized === 'space') {
setValue('config.page_id', '');
setValue('config.index_recursively', false);
} else if (normalized === 'page') {
setValue('config.space', '');
}
},
[mode, onChange, setValue],
);
if (normalized === 'everything') {
setValue('config.space', '', { shouldDirty: true, shouldTouch: true });
setValue('config.page_id', '', { shouldDirty: true, shouldTouch: true });
setValue('config.index_recursively', false, {
shouldDirty: true,
shouldTouch: true,
});
} else if (normalized === 'space') {
setValue('config.page_id', '', { shouldDirty: true, shouldTouch: true });
setValue('config.index_recursively', false, {
shouldDirty: true,
shouldTouch: true,
});
} else if (normalized === 'page') {
setValue('config.space', '', { shouldDirty: true, shouldTouch: true });
}
};
const debouncedHandleChange = useMemo(
() =>
debounce(() => {
handleModeChange();
}, 300),
[handleModeChange],
);
return (
<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
className="w-full"
value={spaceValue ?? ''}
onChange={(e) =>
setValue('config.space', e.target.value, {
shouldDirty: true,
shouldTouch: true,
})
}
onChange={(e) => {
const value = e.target.value;
setValue('config.space', value);
debouncedHandleChange();
}}
placeholder="e.g. KB"
disabled={disabled}
/>
@ -148,12 +161,10 @@ export const ConfluenceIndexingModeField = (
<Input
className="w-full"
value={pageIdValue ?? ''}
onChange={(e) =>
setValue('config.page_id', e.target.value, {
shouldDirty: true,
shouldTouch: true,
})
}
onChange={(e) => {
setValue('config.page_id', e.target.value);
debouncedHandleChange();
}}
placeholder="e.g. 123456"
disabled={disabled}
/>
@ -164,12 +175,10 @@ export const ConfluenceIndexingModeField = (
<div className="flex items-center gap-2 pt-2">
<Checkbox
checked={Boolean(indexRecursively)}
onCheckedChange={(checked) =>
setValue('config.index_recursively', Boolean(checked), {
shouldDirty: true,
shouldTouch: true,
})
}
onCheckedChange={(checked) => {
setValue('config.index_recursively', Boolean(checked));
debouncedHandleChange();
}}
disabled={disabled}
/>
<span className="text-sm text-text-secondary">

View File

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

View File

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

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 { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import { APIMapUrl } from '@/constants/llm';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmList } from '@/hooks/use-llm-request';
import { Plus } from 'lucide-react';
import { ArrowUpRight, Plus } from 'lucide-react';
import { FC, useMemo, useState } from 'react';
type TagType =
@ -128,10 +129,26 @@ export const AvailableModels: FC<{
>
<div className="flex items-center space-x-3 mb-3">
<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">
{model.name}
</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>
<Button className=" px-2 items-center gap-0 text-xs h-6 rounded-md transition-colors hidden group-hover:flex">
<Plus size={12} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,10 @@ export enum Routes {
Agent = '/agent',
AgentTemplates = '/agent-templates',
Agents = '/agents',
Memories = '/memories',
Memory = '/memory',
MemoryMessage = '/memory-message',
MemorySetting = '/memory-setting',
AgentList = '/agent-list',
Searches = '/next-searches',
Search = '/next-search',
@ -89,6 +93,7 @@ const routes = [
path: Routes.AgentList,
component: `@/pages/${Routes.Agents}`,
},
{
path: '/document/:id',
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`,
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 registerServer from '@/utils/register-server';
import request from '@/utils/request';
import { registerNextServer } from '@/utils/register-server';
const {
createSearch,
@ -49,6 +48,6 @@ const methods = {
method: 'get',
},
} as const;
const searchService = registerServer<keyof typeof methods>(methods, request);
const searchService = registerNextServer<keyof typeof methods>(methods);
export default searchService;

View File

@ -226,6 +226,13 @@ export default {
getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`,
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
fetchDataflow: (id: string) => `${api_host}/dataflow/get/${id}`,
setDataflow: `${api_host}/dataflow/set`,