Feat: Replace antd with shadcn and delete the template node. #10427 (#11693)

### What problem does this PR solve?

Feat: Replace antd with shadcn and delete the template node. #10427
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-12-03 14:37:58 +08:00
committed by GitHub
parent e3f40db963
commit b44e65a12e
82 changed files with 181 additions and 4770 deletions

View File

@@ -1,48 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex, Form, InputNumber, Slider } from 'antd';
export const AutoKeywordsItem = () => {
const { t } = useTranslate('knowledgeDetails');
return (
<Form.Item label={t('autoKeywords')} tooltip={t('autoKeywordsTip')}>
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['parser_config', 'auto_keywords']}
noStyle
initialValue={0}
>
<Slider max={30} style={{ width: '100%' }} />
</Form.Item>
</Flex>
<Form.Item name={['parser_config', 'auto_keywords']} noStyle>
<InputNumber max={30} min={0} />
</Form.Item>
</Flex>
</Form.Item>
);
};
export const AutoQuestionsItem = () => {
const { t } = useTranslate('knowledgeDetails');
return (
<Form.Item label={t('autoQuestions')} tooltip={t('autoQuestionsTip')}>
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['parser_config', 'auto_questions']}
noStyle
initialValue={0}
>
<Slider max={10} style={{ width: '100%' }} />
</Form.Item>
</Flex>
<Form.Item name={['parser_config', 'auto_questions']} noStyle>
<InputNumber max={10} min={0} />
</Form.Item>
</Flex>
</Form.Item>
);
};

View File

@@ -1,161 +0,0 @@
import { DocumentParserType } from '@/constants/knowledge';
import { useHandleChunkMethodSelectChange } from '@/hooks/logic-hooks';
import { useSelectParserList } from '@/hooks/use-user-setting-request';
import { FormInstance } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react';
const ParserListMap = new Map([
[
['pdf'],
[
DocumentParserType.Naive,
DocumentParserType.Resume,
DocumentParserType.Manual,
DocumentParserType.Paper,
DocumentParserType.Book,
DocumentParserType.Laws,
DocumentParserType.Presentation,
DocumentParserType.One,
DocumentParserType.Qa,
DocumentParserType.KnowledgeGraph,
],
],
[
['doc', 'docx'],
[
DocumentParserType.Naive,
DocumentParserType.Resume,
DocumentParserType.Book,
DocumentParserType.Laws,
DocumentParserType.One,
DocumentParserType.Qa,
DocumentParserType.Manual,
DocumentParserType.KnowledgeGraph,
],
],
[
['xlsx', 'xls'],
[
DocumentParserType.Naive,
DocumentParserType.Qa,
DocumentParserType.Table,
DocumentParserType.One,
DocumentParserType.KnowledgeGraph,
],
],
[['ppt', 'pptx'], [DocumentParserType.Presentation]],
[
['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tif', 'tiff', 'webp', 'svg', 'ico'],
[DocumentParserType.Picture],
],
[
['txt'],
[
DocumentParserType.Naive,
DocumentParserType.Resume,
DocumentParserType.Book,
DocumentParserType.Laws,
DocumentParserType.One,
DocumentParserType.Qa,
DocumentParserType.Table,
DocumentParserType.KnowledgeGraph,
],
],
[
['csv'],
[
DocumentParserType.Naive,
DocumentParserType.Resume,
DocumentParserType.Book,
DocumentParserType.Laws,
DocumentParserType.One,
DocumentParserType.Qa,
DocumentParserType.Table,
DocumentParserType.KnowledgeGraph,
],
],
[
['md'],
[
DocumentParserType.Naive,
DocumentParserType.Qa,
DocumentParserType.KnowledgeGraph,
],
],
[['json'], [DocumentParserType.Naive, DocumentParserType.KnowledgeGraph]],
[['eml'], [DocumentParserType.Email]],
]);
const getParserList = (
values: string[],
parserList: Array<{
value: string;
label: string;
}>,
) => {
return parserList.filter((x) => values?.some((y) => y === x.value));
};
export const useFetchParserListOnMount = (
documentId: string,
parserId: DocumentParserType,
documentExtension: string,
form: FormInstance,
) => {
const [selectedTag, setSelectedTag] = useState<DocumentParserType>();
const parserList = useSelectParserList();
const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form);
const nextParserList = useMemo(() => {
const key = [...ParserListMap.keys()].find((x) =>
x.some((y) => y === documentExtension),
);
if (key) {
const values = ParserListMap.get(key);
return getParserList(values ?? [], parserList);
}
return getParserList(
[
DocumentParserType.Naive,
DocumentParserType.Resume,
DocumentParserType.Book,
DocumentParserType.Laws,
DocumentParserType.One,
DocumentParserType.Qa,
DocumentParserType.Table,
],
parserList,
);
}, [parserList, documentExtension]);
useEffect(() => {
setSelectedTag(parserId);
}, [parserId, documentId]);
const handleChange = (tag: string) => {
handleChunkMethodSelectChange(tag);
setSelectedTag(tag as DocumentParserType);
};
return { parserList: nextParserList, handleChange, selectedTag };
};
const hideAutoKeywords = [
DocumentParserType.Qa,
DocumentParserType.Table,
DocumentParserType.Resume,
DocumentParserType.KnowledgeGraph,
DocumentParserType.Tag,
];
export const useShowAutoKeywords = () => {
const showAutoKeywords = useCallback(
(selectedTag: DocumentParserType | undefined) => {
return hideAutoKeywords.every((x) => selectedTag !== x);
},
[],
);
return showAutoKeywords;
};

View File

@@ -1,14 +0,0 @@
.pageInputNumber {
width: 220px;
}
.questionIcon {
margin-inline-start: 4px;
color: rgba(0, 0, 0, 0.45);
cursor: help;
writing-mode: horizontal-tb;
}
.chunkMethod {
margin-bottom: 0;
}

View File

@@ -1,350 +0,0 @@
import MaxTokenNumber from '@/components/max-token-number';
import { IModalManagerChildrenProps } from '@/components/modal-manager';
import {
MinusCircleOutlined,
PlusOutlined,
QuestionCircleOutlined,
} from '@ant-design/icons';
import {
Button,
Divider,
Form,
InputNumber,
Modal,
Select,
Space,
Tooltip,
} from 'antd';
import omit from 'lodash/omit';
import React, { useEffect, useMemo } from 'react';
import { useFetchParserListOnMount, useShowAutoKeywords } from './hooks';
import { DocumentParserType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { IParserConfig } from '@/interfaces/database/document';
import { IChangeParserConfigRequestBody } from '@/interfaces/request/document';
import { get } from 'lodash';
import { AutoKeywordsItem, AutoQuestionsItem } from '../auto-keywords-item';
import { DatasetConfigurationContainer } from '../dataset-configuration-container';
import Delimiter from '../delimiter';
import EntityTypesItem from '../entity-types-item';
import ExcelToHtml from '../excel-to-html';
import LayoutRecognize from '../layout-recognize';
import ParseConfiguration, {
showRaptorParseConfiguration,
} from '../parse-configuration';
import {
UseGraphRagItem,
showGraphRagItems,
} from '../parse-configuration/graph-rag-items';
import styles from './index.less';
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
onOk: (
parserId: DocumentParserType | undefined,
parserConfig: IChangeParserConfigRequestBody,
) => void;
showModal?(): void;
parserId: DocumentParserType;
parserConfig: IParserConfig;
documentExtension: string;
documentId: string;
}
const hidePagesChunkMethods = [
DocumentParserType.Qa,
DocumentParserType.Table,
DocumentParserType.Picture,
DocumentParserType.Resume,
DocumentParserType.One,
DocumentParserType.KnowledgeGraph,
];
const ChunkMethodModal: React.FC<IProps> = ({
documentId,
parserId,
onOk,
hideModal,
visible,
documentExtension,
parserConfig,
loading,
}) => {
const [form] = Form.useForm();
const { parserList, handleChange, selectedTag } = useFetchParserListOnMount(
documentId,
parserId,
documentExtension,
form,
);
const { t } = useTranslate('knowledgeDetails');
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
const useGraphRag = useMemo(() => {
return knowledgeDetails.parser_config?.graphrag?.use_graphrag;
}, [knowledgeDetails.parser_config?.graphrag?.use_graphrag]);
const handleOk = async () => {
const values = await form.validateFields();
const parser_config = {
...values.parser_config,
pages: values.pages?.map((x: any) => [x.from, x.to]) ?? [],
};
onOk(selectedTag, parser_config);
};
const isPdf = documentExtension === 'pdf';
const showPages = useMemo(() => {
return isPdf && hidePagesChunkMethods.every((x) => x !== selectedTag);
}, [selectedTag, isPdf]);
const showOne = useMemo(() => {
return (
isPdf &&
hidePagesChunkMethods
.filter((x) => x !== DocumentParserType.One)
.every((x) => x !== selectedTag)
);
}, [selectedTag, isPdf]);
const showMaxTokenNumber =
selectedTag === DocumentParserType.Naive ||
selectedTag === DocumentParserType.KnowledgeGraph;
const showEntityTypes = selectedTag === DocumentParserType.KnowledgeGraph;
const showExcelToHtml =
selectedTag === DocumentParserType.Naive && documentExtension === 'xlsx';
const showAutoKeywords = useShowAutoKeywords();
const afterClose = () => {
form.resetFields();
};
useEffect(() => {
if (visible) {
const pages =
parserConfig?.pages?.map((x) => ({ from: x[0], to: x[1] })) ?? [];
form.setFieldsValue({
pages: pages.length > 0 ? pages : [{ from: 1, to: 1024 }],
parser_config: {
...omit(parserConfig, 'pages'),
graphrag: {
use_graphrag: get(
parserConfig,
'graphrag.use_graphrag',
useGraphRag,
),
},
},
});
}
}, [
form,
knowledgeDetails.parser_config,
parserConfig,
useGraphRag,
visible,
]);
return (
<Modal
title={t('chunkMethod')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
afterClose={afterClose}
confirmLoading={loading}
width={700}
>
<Space size={[0, 8]} wrap>
<Form.Item label={t('chunkMethod')} className={styles.chunkMethod}>
<Select
style={{ width: 160 }}
onChange={handleChange}
value={selectedTag}
options={parserList}
/>
</Form.Item>
</Space>
<Divider></Divider>
<Form
name="dynamic_form_nest_item"
autoComplete="off"
form={form}
className="space-y-4"
>
{showPages && (
<>
<Space>
<p>{t('pageRanges')}:</p>
<Tooltip title={t('pageRangesTip')}>
<QuestionCircleOutlined
className={styles.questionIcon}
></QuestionCircleOutlined>
</Tooltip>
</Space>
<Form.List name="pages">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Space
key={key}
style={{
display: 'flex',
}}
align="baseline"
>
<Form.Item
{...restField}
name={[name, 'from']}
dependencies={name > 0 ? [name - 1, 'to'] : []}
rules={[
{
required: true,
message: t('fromMessage'),
},
({ getFieldValue }) => ({
validator(_, value) {
if (
name === 0 ||
!value ||
getFieldValue(['pages', name - 1, 'to']) < value
) {
return Promise.resolve();
}
return Promise.reject(
new Error(t('greaterThanPrevious')),
);
},
}),
]}
>
<InputNumber
placeholder={t('fromPlaceholder')}
min={0}
precision={0}
className={styles.pageInputNumber}
/>
</Form.Item>
<Form.Item
{...restField}
name={[name, 'to']}
dependencies={[name, 'from']}
rules={[
{
required: true,
message: t('toMessage'),
},
({ getFieldValue }) => ({
validator(_, value) {
if (
!value ||
getFieldValue(['pages', name, 'from']) < value
) {
return Promise.resolve();
}
return Promise.reject(
new Error(t('greaterThan')),
);
},
}),
]}
>
<InputNumber
placeholder={t('toPlaceholder')}
min={0}
precision={0}
className={styles.pageInputNumber}
/>
</Form.Item>
{name > 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
>
{t('addPage')}
</Button>
</Form.Item>
</>
)}
</Form.List>
</>
)}
{showPages && (
<Form.Item
noStyle
dependencies={[['parser_config', 'layout_recognize']]}
>
{({ getFieldValue }) =>
getFieldValue(['parser_config', 'layout_recognize']) && (
<Form.Item
name={['parser_config', 'task_page_size']}
label={t('taskPageSize')}
tooltip={t('taskPageSizeTip')}
initialValue={12}
rules={[
{
required: true,
message: t('taskPageSizeMessage'),
},
]}
>
<InputNumber min={1} max={128} />
</Form.Item>
)
}
</Form.Item>
)}
<DatasetConfigurationContainer show={showOne || showMaxTokenNumber}>
{showOne && <LayoutRecognize></LayoutRecognize>}
{showMaxTokenNumber && (
<>
<MaxTokenNumber
max={
selectedTag === DocumentParserType.KnowledgeGraph
? 8192 * 2
: 2048
}
></MaxTokenNumber>
<Delimiter></Delimiter>
</>
)}
</DatasetConfigurationContainer>
<DatasetConfigurationContainer
show={showAutoKeywords(selectedTag) || showExcelToHtml}
>
{showAutoKeywords(selectedTag) && (
<>
<AutoKeywordsItem></AutoKeywordsItem>
<AutoQuestionsItem></AutoQuestionsItem>
</>
)}
{showExcelToHtml && <ExcelToHtml></ExcelToHtml>}
</DatasetConfigurationContainer>
{showRaptorParseConfiguration(selectedTag) && (
<DatasetConfigurationContainer>
<ParseConfiguration></ParseConfiguration>
</DatasetConfigurationContainer>
)}
{showGraphRagItems(selectedTag) && useGraphRag && (
<UseGraphRagItem></UseGraphRagItem>
)}
{showEntityTypes && <EntityTypesItem></EntityTypesItem>}
</Form>
</Modal>
);
};
export default ChunkMethodModal;

View File

@@ -1,40 +0,0 @@
import { Select as AntSelect, Form } from 'antd';
import { useTranslation } from 'react-i18next';
const Languages = [
'English',
'Chinese',
'Spanish',
'French',
'German',
'Japanese',
'Korean',
'Vietnamese',
];
const options = Languages.map((x) => ({ label: x, value: x }));
type CrossLanguageItemProps = {
name?: string | Array<string>;
};
export const CrossLanguageItem = ({
name = ['prompt_config', 'cross_languages'],
}: CrossLanguageItemProps) => {
const { t } = useTranslation();
return (
<Form.Item
label={t('chat.crossLanguage')}
name={name}
tooltip={t('chat.crossLanguageTip')}
>
<AntSelect
options={options}
allowClear
placeholder={t('common.languagePlaceholder')}
mode="multiple"
/>
</Form.Item>
);
};

View File

@@ -1,42 +0,0 @@
import { Form, Input } from 'antd';
import { useTranslation } from 'react-i18next';
interface IProps {
value?: string | undefined;
onChange?: (val: string | undefined) => void;
maxLength?: number;
}
export const DelimiterInput = ({ value, onChange, maxLength }: IProps) => {
const nextValue = value?.replaceAll('\n', '\\n');
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
const nextValue = val.replaceAll('\\n', '\n');
onChange?.(nextValue);
};
return (
<Input
value={nextValue}
onChange={handleInputChange}
maxLength={maxLength}
></Input>
);
};
const Delimiter = () => {
const { t } = useTranslation();
return (
<Form.Item
name={['parser_config', 'delimiter']}
label={t('knowledgeDetails.delimiter')}
initialValue={`\n`}
rules={[{ required: true }]}
tooltip={t('knowledgeDetails.delimiterTip')}
>
<DelimiterInput />
</Form.Item>
);
};
export default Delimiter;

View File

@@ -1,33 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form } from 'antd';
import EditTag from './edit-tag';
const initialEntityTypes = [
'organization',
'person',
'geo',
'event',
'category',
];
type IProps = {
field?: string[];
};
const EntityTypesItem = ({
field = ['parser_config', 'entity_types'],
}: IProps) => {
const { t } = useTranslate('knowledgeConfiguration');
return (
<Form.Item
name={field}
label={t('entityTypes')}
rules={[{ required: true }]}
initialValue={initialEntityTypes}
>
<EditTag />
</Form.Item>
);
};
export default EntityTypesItem;

View File

@@ -1,19 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Switch } from 'antd';
const ExcelToHtml = () => {
const { t } = useTranslate('knowledgeDetails');
return (
<Form.Item
name={['parser_config', 'html4excel']}
label={t('html4excel')}
initialValue={false}
valuePropName="checked"
tooltip={t('html4excelTip')}
>
<Switch />
</Form.Item>
);
};
export default ExcelToHtml;

View File

@@ -0,0 +1,77 @@
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { RAGFlowFormItem } from './ragflow-form';
import { ButtonLoading } from './ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from './ui/dialog';
import { Form } from './ui/form';
import { Textarea } from './ui/textarea';
const FormId = 'feedback-dialog';
const FeedbackDialog = ({
visible,
hideModal,
onOk,
loading,
}: IModalProps<IFeedbackRequestBody>) => {
const { t } = useTranslation();
const FormSchema = z.object({
feedback: z
.string()
.min(1, {
message: t('common.namePlaceholder'),
})
.trim(),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: { feedback: '' },
});
const handleOk = useCallback(
async (data: z.infer<typeof FormSchema>) => {
return onOk?.({ thumbup: false, feedback: data.feedback });
},
[onOk],
);
return (
<Dialog open={visible} onOpenChange={hideModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Feedback</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleOk)}
className="space-y-6"
id={FormId}
>
<RAGFlowFormItem name="feedback">
<Textarea> </Textarea>
</RAGFlowFormItem>
</form>
</Form>
<DialogFooter>
<ButtonLoading type="submit" form={FormId} loading={loading}>
{t('common.save')}
</ButtonLoading>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default FeedbackDialog;

View File

@@ -1,13 +0,0 @@
.uploader {
:global {
.ant-upload-list {
max-height: 40vh;
overflow-y: auto;
}
}
}
.uploadLimit {
color: red;
font-size: 12px;
}

View File

@@ -1,191 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { InboxOutlined } from '@ant-design/icons';
import {
Checkbox,
Flex,
Modal,
Progress,
Segmented,
Tabs,
TabsProps,
Upload,
UploadFile,
UploadProps,
} from 'antd';
import { Dispatch, SetStateAction, useState } from 'react';
import styles from './index.less';
const { Dragger } = Upload;
const FileUpload = ({
directory,
fileList,
setFileList,
uploadProgress,
}: {
directory: boolean;
fileList: UploadFile[];
setFileList: Dispatch<SetStateAction<UploadFile[]>>;
uploadProgress?: number;
}) => {
const { t } = useTranslate('fileManager');
const props: UploadProps = {
multiple: true,
onRemove: (file) => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
setFileList(newFileList);
},
beforeUpload: (file: UploadFile) => {
setFileList((pre) => {
return [...pre, file];
});
return false;
},
directory,
fileList,
progress: {
strokeWidth: 2,
},
};
return (
<>
<Progress percent={uploadProgress} showInfo={false} />
<Dragger {...props} className={styles.uploader}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">{t('uploadTitle')}</p>
<p className="ant-upload-hint">{t('uploadDescription')}</p>
{false && <p className={styles.uploadLimit}>{t('uploadLimit')}</p>}
</Dragger>
</>
);
};
interface IFileUploadModalProps
extends IModalProps<
{ parseOnCreation: boolean; directoryFileList: UploadFile[] } | UploadFile[]
> {
uploadFileList?: UploadFile[];
setUploadFileList?: Dispatch<SetStateAction<UploadFile[]>>;
uploadProgress?: number;
setUploadProgress?: Dispatch<SetStateAction<number>>;
}
const FileUploadModal = ({
visible,
hideModal,
loading,
onOk: onFileUploadOk,
uploadFileList: fileList,
setUploadFileList: setFileList,
uploadProgress,
setUploadProgress,
}: IFileUploadModalProps) => {
const { t } = useTranslate('fileManager');
const [value, setValue] = useState<string | number>('local');
const [parseOnCreation, setParseOnCreation] = useState(false);
const [currentFileList, setCurrentFileList] = useState<UploadFile[]>([]);
const [directoryFileList, setDirectoryFileList] = useState<UploadFile[]>([]);
const clearFileList = () => {
if (setFileList) {
setFileList([]);
setUploadProgress?.(0);
} else {
setCurrentFileList([]);
}
setDirectoryFileList([]);
};
const onOk = async () => {
if (uploadProgress === 100) {
hideModal?.();
return;
}
const ret = await onFileUploadOk?.(
fileList
? { parseOnCreation, directoryFileList }
: [...currentFileList, ...directoryFileList],
);
return ret;
};
const afterClose = () => {
clearFileList();
};
const items: TabsProps['items'] = [
{
key: '1',
label: t('file'),
children: (
<FileUpload
directory={false}
fileList={fileList ? fileList : currentFileList}
setFileList={setFileList ? setFileList : setCurrentFileList}
uploadProgress={uploadProgress}
></FileUpload>
),
},
{
key: '2',
label: t('directory'),
children: (
<FileUpload
directory
fileList={directoryFileList}
setFileList={setDirectoryFileList}
uploadProgress={uploadProgress}
></FileUpload>
),
},
];
return (
<>
<Modal
title={t('uploadFile')}
open={visible}
onOk={onOk}
onCancel={hideModal}
confirmLoading={loading}
afterClose={afterClose}
>
<Flex gap={'large'} vertical>
<Segmented
options={[
{ label: t('local'), value: 'local' },
{ label: t('s3'), value: 's3' },
]}
block
value={value}
onChange={setValue}
/>
{value === 'local' ? (
<>
<Checkbox
checked={parseOnCreation}
onChange={(e) => setParseOnCreation(e.target.checked)}
>
{t('parseOnCreation')}
</Checkbox>
<Tabs defaultActiveKey="1" items={items} />
</>
) : (
t('comingSoon', { keyPrefix: 'common' })
)}
</Flex>
</Modal>
</>
);
};
export default FileUploadModal;

View File

@@ -1,55 +0,0 @@
import { LlmModelType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/use-llm-request';
import { Form, Select } from 'antd';
import { camelCase } from 'lodash';
import { useMemo } from 'react';
const enum DocumentType {
DeepDOC = 'DeepDOC',
PlainText = 'Plain Text',
}
const LayoutRecognize = () => {
const { t } = useTranslate('knowledgeDetails');
const allOptions = useSelectLlmOptionsByModelType();
const options = useMemo(() => {
const list = [DocumentType.DeepDOC, DocumentType.PlainText].map((x) => ({
label: x === DocumentType.PlainText ? t(camelCase(x)) : 'DeepDoc',
value: x,
}));
const image2TextList = allOptions[LlmModelType.Image2text].map((x) => {
return {
...x,
options: x.options.map((y) => {
return {
...y,
label: (
<div className="flex justify-between items-center gap-2">
{y.label}
<span className="text-red-500 text-sm">Experimental</span>
</div>
),
};
}),
};
});
return [...list, ...image2TextList];
}, [allOptions, t]);
return (
<Form.Item
name={['parser_config', 'layout_recognize']}
label={t('layoutRecognize')}
initialValue={DocumentType.DeepDOC}
tooltip={t('layoutRecognizeTip')}
>
<Select options={options} popupMatchSelectWidth={false} />
</Form.Item>
);
};
export default LayoutRecognize;

View File

@@ -1,51 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { useLlmToolsList } from '@/hooks/plugin-hooks';
import { Select, Space } from 'antd';
interface IProps {
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
}
const LLMToolsSelect = ({ value, onChange, disabled }: IProps) => {
const { t } = useTranslate("llmTools");
const tools = useLlmToolsList();
function wrapTranslation(text: string): string {
if (!text) {
return text;
}
if (text.startsWith("$t:")) {
return t(text.substring(3));
}
return text;
}
const toolOptions = tools.map(t => ({
label: wrapTranslation(t.displayName),
description: wrapTranslation(t.displayDescription),
value: t.name,
title: wrapTranslation(t.displayDescription),
}));
return (
<Select
mode="multiple"
options={toolOptions}
optionRender={option => (
<Space size="large">
{option.label}
{option.data.description}
</Space>
)}
onChange={onChange}
value={value}
disabled={disabled}
></Select>
);
};
export default LLMToolsSelect;

View File

@@ -1,37 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex, Form, InputNumber, Slider } from 'antd';
interface IProps {
initialValue?: number;
max?: number;
}
const MaxTokenNumber = ({ initialValue = 512, max = 2048 }: IProps) => {
const { t } = useTranslate('knowledgeConfiguration');
return (
<Form.Item label={t('chunkTokenNumber')} tooltip={t('chunkTokenNumberTip')}>
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['parser_config', 'chunk_token_num']}
noStyle
initialValue={initialValue}
rules={[{ required: true, message: t('chunkTokenNumberMessage') }]}
>
<Slider max={max} style={{ width: '100%' }} />
</Form.Item>
</Flex>
<Form.Item
name={['parser_config', 'chunk_token_num']}
noStyle
rules={[{ required: true, message: t('chunkTokenNumberMessage') }]}
>
<InputNumber max={max} min={0} />
</Form.Item>
</Flex>
</Form.Item>
);
};
export default MaxTokenNumber;

View File

@@ -1,4 +1,3 @@
import { Form, InputNumber } from 'antd';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
@@ -10,27 +9,6 @@ import {
} from './ui/form';
import { NumberInput } from './ui/input';
const MessageHistoryWindowSizeItem = ({
initialValue,
}: {
initialValue: number;
}) => {
const { t } = useTranslation();
return (
<Form.Item
name={'message_history_window_size'}
label={t('flow.messageHistoryWindowSize')}
initialValue={initialValue}
tooltip={t('flow.messageHistoryWindowSizeTip')}
>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
);
};
export default MessageHistoryWindowSizeItem;
export function MessageHistoryWindowSizeFormField() {
const form = useFormContext();
const { t } = useTranslation();

View File

@@ -1,51 +0,0 @@
import { Form, Input, Modal } from 'antd';
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { useCallback } from 'react';
type FieldType = {
feedback?: string;
};
const FeedbackModal = ({
visible,
hideModal,
onOk,
loading,
}: IModalProps<IFeedbackRequestBody>) => {
const [form] = Form.useForm();
const handleOk = useCallback(async () => {
const ret = await form.validateFields();
return onOk?.({ thumbup: false, feedback: ret.feedback });
}, [onOk, form]);
return (
<Modal
title="Feedback"
open={visible}
onOk={handleOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Form
name="basic"
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
style={{ maxWidth: 600 }}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
name="feedback"
rules={[{ required: true, message: 'Please input your feedback!' }]}
>
<Input.TextArea rows={8} placeholder="Please input your feedback!" />
</Form.Item>
</Form>
</Modal>
);
};
export default FeedbackModal;

View File

@@ -13,9 +13,9 @@ import {
import { Radio, Tooltip } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import FeedbackModal from './feedback-modal';
import FeedbackDialog from '../feedback-dialog';
import { PromptDialog } from '../prompt-dialog';
import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
import PromptModal from './prompt-modal';
interface IProps {
messageId: string;
@@ -79,19 +79,19 @@ export const AssistantGroupButton = ({
)}
</Radio.Group>
{visible && (
<FeedbackModal
<FeedbackDialog
visible={visible}
hideModal={hideModal}
onOk={onFeedbackOk}
loading={loading}
></FeedbackModal>
></FeedbackDialog>
)}
{promptVisible && (
<PromptModal
<PromptDialog
visible={promptVisible}
hideModal={hidePromptModal}
prompt={prompt}
></PromptModal>
></PromptDialog>
)}
</>
);

View File

@@ -1,30 +0,0 @@
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { Modal, Space } from 'antd';
import HightLightMarkdown from '../highlight-markdown';
import SvgIcon from '../svg-icon';
const PromptModal = ({
visible,
hideModal,
prompt,
}: IModalProps<IFeedbackRequestBody> & { prompt?: string }) => {
return (
<Modal
title={
<Space>
<SvgIcon name={`prompt`} width={18}></SvgIcon>
Prompt
</Space>
}
width={'80%'}
open={visible}
onCancel={hideModal}
footer={null}
>
<HightLightMarkdown>{prompt}</HightLightMarkdown>
</Modal>
);
};
export default PromptModal;

View File

@@ -1,51 +0,0 @@
import { Form, Input, Modal } from 'antd';
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { useCallback } from 'react';
type FieldType = {
feedback?: string;
};
const FeedbackModal = ({
visible,
hideModal,
onOk,
loading,
}: IModalProps<IFeedbackRequestBody>) => {
const [form] = Form.useForm();
const handleOk = useCallback(async () => {
const ret = await form.validateFields();
return onOk?.({ thumbup: false, feedback: ret.feedback });
}, [onOk, form]);
return (
<Modal
title="Feedback"
open={visible}
onOk={handleOk}
onCancel={hideModal}
confirmLoading={loading}
>
<Form
name="basic"
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
style={{ maxWidth: 600 }}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
name="feedback"
rules={[{ required: true, message: 'Please input your feedback!' }]}
>
<Input.TextArea rows={8} placeholder="Please input your feedback!" />
</Form.Item>
</Form>
</Modal>
);
};
export default FeedbackModal;

View File

@@ -17,10 +17,10 @@ import { Radio, Tooltip } from 'antd';
import { Download, NotebookText } from 'lucide-react';
import { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import FeedbackDialog from '../feedback-dialog';
import { PromptDialog } from '../prompt-dialog';
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
import FeedbackModal from './feedback-modal';
import { useRemoveMessage, useSendFeedback, useSpeech } from './hooks';
import PromptModal from './prompt-modal';
interface IProps {
messageId: string;
@@ -129,19 +129,19 @@ export const AssistantGroupButton = ({
)}
</ToggleGroup>
{visible && (
<FeedbackModal
<FeedbackDialog
visible={visible}
hideModal={hideModal}
onOk={onFeedbackOk}
loading={loading}
></FeedbackModal>
></FeedbackDialog>
)}
{promptVisible && (
<PromptModal
<PromptDialog
visible={promptVisible}
hideModal={hidePromptModal}
prompt={prompt}
></PromptModal>
></PromptDialog>
)}
</>
);

View File

@@ -1,30 +0,0 @@
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import { Modal, Space } from 'antd';
import HightLightMarkdown from '../highlight-markdown';
import SvgIcon from '../svg-icon';
const PromptModal = ({
visible,
hideModal,
prompt,
}: IModalProps<IFeedbackRequestBody> & { prompt?: string }) => {
return (
<Modal
title={
<Space>
<SvgIcon name={`prompt`} width={18}></SvgIcon>
Prompt
</Space>
}
width={'80%'}
open={visible}
onCancel={hideModal}
footer={null}
>
<HightLightMarkdown>{prompt}</HightLightMarkdown>
</Modal>
);
};
export default PromptModal;

View File

@@ -1,4 +0,0 @@
.delete {
// height: 24px;
display: inline-block;
}

View File

@@ -1,90 +0,0 @@
import { useShowDeleteConfirm } from '@/hooks/common-hooks';
import { DeleteOutlined, MoreOutlined } from '@ant-design/icons';
import { Dropdown, MenuProps, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import React, { useMemo } from 'react';
import styles from './index.less';
interface IProps {
deleteItem: () => Promise<any> | void;
iconFontSize?: number;
iconFontColor?: string;
items?: MenuProps['items'];
height?: number;
needsDeletionValidation?: boolean;
showDeleteItems?: boolean;
}
const OperateDropdown = ({
deleteItem,
children,
iconFontSize = 30,
iconFontColor = 'gray',
items: otherItems = [],
height = 24,
needsDeletionValidation = true,
showDeleteItems = true,
}: React.PropsWithChildren<IProps>) => {
const { t } = useTranslation();
const showDeleteConfirm = useShowDeleteConfirm();
const handleDelete = () => {
if (needsDeletionValidation) {
showDeleteConfirm({ onOk: deleteItem });
} else {
deleteItem();
}
};
const handleDropdownMenuClick: MenuProps['onClick'] = ({ domEvent, key }) => {
domEvent.preventDefault();
domEvent.stopPropagation();
if (key === '1') {
handleDelete();
}
};
const items: MenuProps['items'] = useMemo(() => {
const items = [];
if (showDeleteItems) {
items.push({
key: '1',
label: (
<Space>
{t('common.delete')}
<DeleteOutlined />
</Space>
),
});
}
return [...items, ...otherItems];
}, [showDeleteItems, otherItems, t]);
return (
<Dropdown
menu={{
items,
onClick: handleDropdownMenuClick,
}}
>
{children || (
<span className={styles.delete}>
<MoreOutlined
rotate={90}
style={{
fontSize: iconFontSize,
color: iconFontColor,
cursor: 'pointer',
height,
}}
/>
</span>
)}
</Dropdown>
);
};
export default OperateDropdown;

View File

@@ -1,28 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Flex, Form, InputNumber, Slider } from 'antd';
const PageRank = () => {
const { t } = useTranslate('knowledgeConfiguration');
return (
<Form.Item label={t('pageRank')} tooltip={t('pageRankTip')}>
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['pagerank']}
noStyle
initialValue={0}
rules={[{ required: true }]}
>
<Slider max={100} style={{ width: '100%' }} />
</Form.Item>
</Flex>
<Form.Item name={['pagerank']} noStyle rules={[{ required: true }]}>
<InputNumber max={100} min={0} />
</Form.Item>
</Flex>
</Form.Item>
);
};
export default PageRank;

View File

@@ -1,138 +0,0 @@
import { DocumentParserType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { Form, Select, Switch } from 'antd';
import { upperFirst } from 'lodash';
import { useCallback, useMemo } from 'react';
import { DatasetConfigurationContainer } from '../dataset-configuration-container';
import EntityTypesItem from '../entity-types-item';
const excludedTagParseMethods = [
DocumentParserType.Table,
DocumentParserType.KnowledgeGraph,
DocumentParserType.Tag,
];
export const showTagItems = (parserId: DocumentParserType) => {
return !excludedTagParseMethods.includes(parserId);
};
const enum MethodValue {
General = 'general',
Light = 'light',
}
export const excludedParseMethods = [
DocumentParserType.Table,
DocumentParserType.Resume,
DocumentParserType.Picture,
DocumentParserType.KnowledgeGraph,
DocumentParserType.Qa,
DocumentParserType.Tag,
];
export const showGraphRagItems = (parserId: DocumentParserType | undefined) => {
return !excludedParseMethods.some((x) => x === parserId);
};
type GraphRagItemsProps = {
marginBottom?: boolean;
};
export function UseGraphRagItem() {
const { t } = useTranslate('knowledgeConfiguration');
return (
<Form.Item
name={['parser_config', 'graphrag', 'use_graphrag']}
label={t('useGraphRag')}
initialValue={false}
valuePropName="checked"
tooltip={t('useGraphRagTip')}
>
<Switch />
</Form.Item>
);
}
// The three types "table", "resume" and "one" do not display this configuration.
const GraphRagItems = ({ marginBottom = false }: GraphRagItemsProps) => {
const { t } = useTranslate('knowledgeConfiguration');
const methodOptions = useMemo(() => {
return [MethodValue.Light, MethodValue.General].map((x) => ({
value: x,
label: upperFirst(x),
}));
}, []);
const renderWideTooltip = useCallback(
(title: React.ReactNode | string) => {
return {
title: typeof title === 'string' ? t(title) : title,
overlayInnerStyle: { width: '32vw' },
};
},
[t],
);
return (
<DatasetConfigurationContainer className={cn({ 'mb-4': marginBottom })}>
<UseGraphRagItem></UseGraphRagItem>
<Form.Item
shouldUpdate={(prevValues, curValues) =>
prevValues.parser_config.graphrag.use_graphrag !==
curValues.parser_config.graphrag.use_graphrag
}
>
{({ getFieldValue }) => {
const useRaptor = getFieldValue([
'parser_config',
'graphrag',
'use_graphrag',
]);
return (
useRaptor && (
<>
<EntityTypesItem
field={['parser_config', 'graphrag', 'entity_types']}
></EntityTypesItem>
<Form.Item
name={['parser_config', 'graphrag', 'method']}
label={t('graphRagMethod')}
tooltip={renderWideTooltip(
<div
dangerouslySetInnerHTML={{
__html: t('graphRagMethodTip'),
}}
></div>,
)}
initialValue={MethodValue.Light}
>
<Select options={methodOptions} />
</Form.Item>
<Form.Item
name={['parser_config', 'graphrag', 'resolution']}
label={t('resolution')}
tooltip={renderWideTooltip('resolutionTip')}
>
<Switch />
</Form.Item>
<Form.Item
name={['parser_config', 'graphrag', 'community']}
label={t('community')}
tooltip={renderWideTooltip('communityTip')}
>
<Switch />
</Form.Item>
</>
)
);
}}
</Form.Item>
</DatasetConfigurationContainer>
);
};
export default GraphRagItems;

View File

@@ -0,0 +1,33 @@
import { IModalProps } from '@/interfaces/common';
import { IFeedbackRequestBody } from '@/interfaces/request/chat';
import HightLightMarkdown from './highlight-markdown';
import SvgIcon from './svg-icon';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
type PromptDialogProps = IModalProps<IFeedbackRequestBody> & {
prompt?: string;
};
export function PromptDialog({
visible,
hideModal,
prompt,
}: PromptDialogProps) {
return (
<Dialog open={visible} onOpenChange={hideModal}>
<DialogContent className="max-w-[80vw]">
<DialogHeader>
<DialogTitle>
<div className="space-x-2">
<SvgIcon name={`prompt`} width={18}></SvgIcon>
<span> Prompt</span>
</div>
</DialogTitle>
</DialogHeader>
<section className="max-h-[80vh] overflow-auto">
<HightLightMarkdown>{prompt}</HightLightMarkdown>
</section>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,82 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Modal } from 'antd';
import { useEffect } from 'react';
import { IModalManagerChildrenProps } from '../modal-manager';
interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
loading: boolean;
initialName: string;
onOk: (name: string) => void;
showModal?(): void;
}
const RenameModal = ({
visible,
hideModal,
loading,
initialName,
onOk,
}: IProps) => {
const [form] = Form.useForm();
const { t } = useTranslate('common');
type FieldType = {
name?: string;
};
const handleOk = async () => {
const ret = await form.validateFields();
return onOk(ret.name);
};
const handleCancel = () => {
hideModal();
};
const onFinish = (values: any) => {
console.log('Success:', values);
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
useEffect(() => {
if (visible) {
form.setFieldValue('name', initialName);
}
}, [initialName, form, visible]);
return (
<Modal
title={t('rename')}
open={visible}
onOk={handleOk}
onCancel={handleCancel}
okButtonProps={{ loading }}
confirmLoading={loading}
>
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
style={{ maxWidth: 600 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
label={t('name')}
name="name"
rules={[{ required: true, message: t('namePlaceholder') }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default RenameModal;

View File

@@ -1,8 +1,6 @@
import { LlmModelType } from '@/constants/knowledge';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/use-llm-request';
import { Select as AntSelect, Form, message, Slider } from 'antd';
import { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { SelectWithSearch } from './originui/select-with-search';
@@ -15,47 +13,6 @@ import {
FormMessage,
} from './ui/form';
type FieldType = {
rerank_id?: string;
top_k?: number;
};
export const RerankItem = () => {
const { t } = useTranslate('knowledgeDetails');
const allOptions = useSelectLlmOptionsByModelType();
const [messageApi, contextHolder] = message.useMessage();
const handleChange = useCallback(
(val: string) => {
if (val) {
messageApi.open({
type: 'warning',
content: t('reRankModelWaring'),
});
}
},
[messageApi, t],
);
return (
<>
{contextHolder}
<Form.Item
label={t('rerankModel')}
name={'rerank_id'}
tooltip={t('rerankTip')}
>
<AntSelect
options={allOptions[LlmModelType.Rerank]}
allowClear
placeholder={t('rerankPlaceholder')}
onChange={handleChange}
/>
</Form.Item>
</>
);
};
export const topKSchema = {
top_k: z.number().optional(),
};
@@ -64,35 +21,6 @@ export const initialTopKValue = {
top_k: 1024,
};
const Rerank = () => {
const { t } = useTranslate('knowledgeDetails');
return (
<>
<RerankItem></RerankItem>
<Form.Item noStyle dependencies={['rerank_id']}>
{({ getFieldValue }) => {
const rerankId = getFieldValue('rerank_id');
return (
rerankId && (
<Form.Item<FieldType>
label={t('topK')}
name={'top_k'}
initialValue={1024}
tooltip={t('topKTip')}
>
<Slider max={2048} min={1} />
</Form.Item>
)
);
}}
</Form.Item>
</>
);
};
export default Rerank;
const RerankId = 'rerank_id';
function RerankFormField() {

View File

@@ -1,11 +0,0 @@
.selectFilesCollapse {
:global(.ant-collapse-header) {
padding-left: 22px;
}
margin-bottom: 32px;
overflow-y: auto;
}
.selectFilesTitle {
padding-right: 10px;
}

View File

@@ -1,66 +0,0 @@
import { ReactComponent as SelectedFilesCollapseIcon } from '@/assets/svg/selected-files-collapse.svg';
import { Collapse, Flex, Space } from 'antd';
import SelectFiles from './select-files';
import {
useAllTestingResult,
useSelectTestingResult,
} from '@/hooks/use-knowledge-request';
import { useTranslation } from 'react-i18next';
import styles from './index.less';
interface IProps {
onTesting(documentIds: string[]): void;
setSelectedDocumentIds(documentIds: string[]): void;
selectedDocumentIds: string[];
}
const RetrievalDocuments = ({
onTesting,
selectedDocumentIds,
setSelectedDocumentIds,
}: IProps) => {
const { t } = useTranslation();
const { documents: documentsAll } = useAllTestingResult();
const { documents } = useSelectTestingResult();
const { documents: useDocuments } = {
documents:
documentsAll?.length > documents?.length ? documentsAll : documents,
};
return (
<Collapse
expandIcon={() => <SelectedFilesCollapseIcon></SelectedFilesCollapseIcon>}
className={styles.selectFilesCollapse}
items={[
{
key: '1',
label: (
<Flex
justify={'space-between'}
align="center"
className={styles.selectFilesTitle}
>
<Space>
<span>
{selectedDocumentIds?.length ?? 0}/{useDocuments?.length ?? 0}
</span>
{t('knowledgeDetails.filesSelected')}
</Space>
</Flex>
),
children: (
<div>
<SelectFiles
setSelectedDocumentIds={setSelectedDocumentIds}
handleTesting={onTesting}
></SelectFiles>
</div>
),
},
]}
/>
);
};
export default RetrievalDocuments;

View File

@@ -1,79 +0,0 @@
import NewDocumentLink from '@/components/new-document-link';
import { useTranslate } from '@/hooks/common-hooks';
import {
useAllTestingResult,
useSelectTestingResult,
} from '@/hooks/use-knowledge-request';
import { ITestingDocument } from '@/interfaces/database/knowledge';
import { EyeOutlined } from '@ant-design/icons';
import { Button, Table, TableProps, Tooltip } from 'antd';
interface IProps {
handleTesting: (ids: string[]) => void;
setSelectedDocumentIds: (ids: string[]) => void;
}
const SelectFiles = ({ setSelectedDocumentIds, handleTesting }: IProps) => {
const { documents } = useSelectTestingResult();
const { documents: documentsAll } = useAllTestingResult();
const useDocuments =
documentsAll?.length > documents?.length ? documentsAll : documents;
const { t } = useTranslate('fileManager');
const columns: TableProps<ITestingDocument>['columns'] = [
{
title: 'Name',
dataIndex: 'doc_name',
key: 'doc_name',
render: (text) => <p>{text}</p>,
},
{
title: 'Hits',
dataIndex: 'count',
key: 'count',
width: 80,
},
{
title: 'View',
key: 'view',
width: 50,
render: (_, { doc_id, doc_name }) => (
<NewDocumentLink
documentName={doc_name}
documentId={doc_id}
prefix="document"
>
<Tooltip title={t('preview')}>
<Button type="text">
<EyeOutlined size={20} />
</Button>
</Tooltip>
</NewDocumentLink>
),
},
];
const rowSelection = {
onChange: (selectedRowKeys: React.Key[]) => {
handleTesting(selectedRowKeys as string[]);
setSelectedDocumentIds(selectedRowKeys as string[]);
},
getCheckboxProps: (record: ITestingDocument) => ({
disabled: record.doc_name === 'Disabled User', // Column configuration not to be checked
name: record.doc_name,
}),
};
return (
<Table
columns={columns}
dataSource={useDocuments}
showHeader={false}
rowSelection={rowSelection}
rowKey={'doc_id'}
/>
);
};
export default SelectFiles;

View File

@@ -1,7 +1,6 @@
import { FormLayout } from '@/constants/form';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { Form, Slider } from 'antd';
import { useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { SliderInputFormField } from '../slider-input-form-field';
@@ -15,46 +14,6 @@ import {
} from '../ui/form';
import { NumberInput } from '../ui/input';
type FieldType = {
similarity_threshold?: number;
// vector_similarity_weight?: number;
};
interface IProps {
isTooltipShown?: boolean;
vectorSimilarityWeightName?: string;
}
const SimilaritySlider = ({
isTooltipShown = false,
vectorSimilarityWeightName = 'vector_similarity_weight',
}: IProps) => {
const { t } = useTranslate('knowledgeDetails');
return (
<>
<Form.Item<FieldType>
label={t('similarityThreshold')}
name={'similarity_threshold'}
tooltip={isTooltipShown && t('similarityThresholdTip')}
initialValue={0.2}
>
<Slider max={1} step={0.01} />
</Form.Item>
<Form.Item
label={t('vectorSimilarityWeight')}
name={vectorSimilarityWeightName}
initialValue={1 - 0.3}
tooltip={isTooltipShown && t('vectorSimilarityWeightTip')}
>
<Slider max={1} step={0.01} />
</Form.Item>
</>
);
};
export default SimilaritySlider;
interface SimilaritySliderFormFieldProps {
similarityName?: string;
vectorSimilarityWeightName?: string;

View File

@@ -1,28 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Typography } from 'antd';
interface IProps {
name?: string | string[];
}
export function TavilyItem({
name = ['prompt_config', 'tavily_api_key'],
}: IProps) {
const { t } = useTranslate('chat');
return (
<Form.Item label={'Tavily API Key'} tooltip={t('tavilyApiKeyTip')}>
<div className="flex flex-col gap-1">
<Form.Item name={name} noStyle>
<Input.Password
placeholder={t('tavilyApiKeyMessage')}
autoComplete="new-password"
/>
</Form.Item>
<Typography.Link href="https://app.tavily.com/home" target={'_blank'}>
{t('tavilyApiKeyHelp')}
</Typography.Link>
</div>
</Form.Item>
);
}

View File

@@ -1,35 +1,8 @@
import { FormLayout } from '@/constants/form';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Slider } from 'antd';
import { z } from 'zod';
import { SliderInputFormField } from './slider-input-form-field';
type FieldType = {
top_n?: number;
};
interface IProps {
initialValue?: number;
max?: number;
}
const TopNItem = ({ initialValue = 8, max = 30 }: IProps) => {
const { t } = useTranslate('chat');
return (
<Form.Item<FieldType>
label={t('topN')}
name={'top_n'}
initialValue={initialValue}
tooltip={t('topNTip')}
>
<Slider max={max} />
</Form.Item>
);
};
export default TopNItem;
interface SimilaritySliderFormFieldProps {
max?: number;
}

View File

@@ -1,26 +1,6 @@
import { Form, Switch } from 'antd';
import { useTranslation } from 'react-i18next';
import { SwitchFormField } from './switch-fom-field';
type IProps = {
filedName: string[] | string;
};
export function UseKnowledgeGraphItem({ filedName }: IProps) {
const { t } = useTranslation();
return (
<Form.Item
label={t('chat.useKnowledgeGraph')}
tooltip={t('chat.useKnowledgeGraphTip')}
name={filedName}
initialValue={false}
>
<Switch></Switch>
</Form.Item>
);
}
interface UseKnowledgeGraphFormFieldProps {
name: string;
}

View File

@@ -73,7 +73,6 @@ export enum Operator {
Retrieval = 'Retrieval',
Categorize = 'Categorize',
Message = 'Message',
Relevant = 'Relevant',
RewriteQuestion = 'RewriteQuestion',
DuckDuckGo = 'DuckDuckGo',
Wikipedia = 'Wikipedia',

View File

@@ -856,7 +856,6 @@ export default {
generate: 'Generieren',
answer: 'Interagieren',
categorize: 'Kategorisieren',
relevant: 'Relevant',
rewriteQuestion: 'Umschreiben',
rewrite: 'Umschreiben',
begin: 'Beginn',

View File

@@ -1217,7 +1217,6 @@ Example: Virtual Hosted Style`,
generate: 'Generate',
answer: 'Interact',
categorize: 'Categorize',
relevant: 'Relevant',
rewriteQuestion: 'Rewrite',
rewrite: 'Rewrite',
begin: 'Begin',

View File

@@ -59,7 +59,6 @@ import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { ExitLoopNode } from './node/exit-loop-node';
import { ExtractorNode } from './node/extractor-node';
import { FileNode } from './node/file-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
import { ListOperationsNode } from './node/list-operations-node';
@@ -68,12 +67,10 @@ import { MessageNode } from './node/message-node';
import NoteNode from './node/note-node';
import ParserNode from './node/parser-node';
import { PlaceholderNode } from './node/placeholder-node';
import { RelevantNode } from './node/relevant-node';
import { RetrievalNode } from './node/retrieval-node';
import { RewriteNode } from './node/rewrite-node';
import { SplitterNode } from './node/splitter-node';
import { SwitchNode } from './node/switch-node';
import { TemplateNode } from './node/template-node';
import TokenizerNode from './node/tokenizer-node';
import { ToolNode } from './node/tool-node';
import { VariableAggregatorNode } from './node/variable-aggregator-node';
@@ -84,15 +81,12 @@ export const nodeTypes: NodeTypes = {
categorizeNode: CategorizeNode,
beginNode: BeginNode,
placeholderNode: PlaceholderNode,
relevantNode: RelevantNode,
noteNode: NoteNode,
switchNode: SwitchNode,
retrievalNode: RetrievalNode,
messageNode: MessageNode,
rewriteNode: RewriteNode,
keywordNode: KeywordNode,
invokeNode: InvokeNode,
templateNode: TemplateNode,
// emailNode: EmailNode,
group: IterationNode,
iterationStartNode: IterationStartNode,

View File

@@ -1,80 +0,0 @@
import { IEmailNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { memo, useState } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
export function InnerEmailNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IEmailNode>) {
const [showDetails, setShowDetails] = useState(false);
return (
<section
className={classNames(styles.ragNode, {
[styles.selectedNode]: selected,
})}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
<Flex vertical gap={8} className={styles.emailNodeContainer}>
<div
className={styles.emailConfig}
onClick={() => setShowDetails(!showDetails)}
>
<div className={styles.configItem}>
<span className={styles.configLabel}>SMTP:</span>
<span className={styles.configValue}>{data.form?.smtp_server}</span>
</div>
<div className={styles.configItem}>
<span className={styles.configLabel}>Port:</span>
<span className={styles.configValue}>{data.form?.smtp_port}</span>
</div>
<div className={styles.configItem}>
<span className={styles.configLabel}>From:</span>
<span className={styles.configValue}>{data.form?.email}</span>
</div>
<div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div>
</div>
{showDetails && (
<div className={styles.jsonExample}>
<div className={styles.jsonTitle}>Expected Input JSON:</div>
<pre className={styles.jsonContent}>
{`{
"to_email": "...",
"cc_email": "...",
"subject": "...",
"content": "..."
}`}
</pre>
</div>
)}
</Flex>
</section>
);
}
export const EmailNode = memo(InnerEmailNode);

View File

@@ -1,62 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { IInvokeNode } from '@/interfaces/database/flow';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
function InnerInvokeNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<IInvokeNode>) {
const { t } = useTranslation();
const { theme } = useTheme();
const url = get(data, 'form.url');
return (
<section
className={classNames(
styles.ragNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
id="b"
style={RightHandleStyle}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical>
<div>{t('flow.url')}</div>
<div className={styles.nodeText}>{url}</div>
</Flex>
</section>
);
}
export const InvokeNode = memo(InnerInvokeNode);

View File

@@ -1,9 +1,11 @@
import { NodeCollapsible } from '@/components/collapse';
import { IMessageNode } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils';
import { NodeProps } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { memo } from 'react';
import { LabelCard } from './card';
import { LeftEndHandle } from './handle';
import styles from './index.less';
import NodeHeader from './node-header';
@@ -11,20 +13,11 @@ import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
function InnerMessageNode({ id, data, selected }: NodeProps<IMessageNode>) {
const messages: string[] = get(data, 'form.messages', []);
const messages: string[] = get(data, 'form.content', []);
return (
<ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}>
<LeftEndHandle></LeftEndHandle>
{/* <CommonHandle
type="source"
position={Position.Right}
isConnectable={isConnectable}
style={RightHandleStyle}
id={NodeHandleId.Start}
nodeId={id}
isConnectableEnd={false}
></CommonHandle> */}
<NodeHeader
id={id}
name={data.name}
@@ -33,15 +26,17 @@ function InnerMessageNode({ id, data, selected }: NodeProps<IMessageNode>) {
[styles.nodeHeader]: messages.length > 0,
})}
></NodeHeader>
<Flex vertical gap={8} className={styles.messageNodeContainer}>
{messages.map((message, idx) => {
return (
<div className={styles.nodeText} key={idx}>
{message}
</div>
);
})}
</Flex>
<section
className={cn('flex flex-col gap-2', styles.messageNodeContainer)}
>
<NodeCollapsible items={messages}>
{(x, idx) => (
<LabelCard key={idx} className="truncate">
{x}
</LabelCard>
)}
</NodeCollapsible>
</section>
</NodeWrapper>
</ToolBar>
);

View File

@@ -1,5 +1,5 @@
import { Skeleton } from '@/components/ui/skeleton';
import { NodeProps, Position } from '@xyflow/react';
import { Skeleton } from 'antd';
import { memo } from 'react';
import { NodeHandleId } from '../../constant';
import { CommonHandle } from './handle';
@@ -17,19 +17,10 @@ function InnerPlaceholderNode({ id, selected }: NodeProps) {
nodeId={id}
id={NodeHandleId.End}
></CommonHandle>
<section className="flex items-center gap-2">
<Skeleton.Avatar
active
size={24}
shape="square"
style={{ backgroundColor: 'rgba(255,255,255,0.05)' }}
/>
</section>
<section className={'flex gap-2 flex-col'} style={{ marginTop: 10 }}>
<Skeleton.Input active style={{ width: '100%', height: 30 }} />
</section>
<div className="space-y-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-6 w-full" />
</div>
</NodeWrapper>
);
}

View File

@@ -1,73 +0,0 @@
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { RightHandleStyle } from './handle-icon';
import { useTheme } from '@/components/theme-provider';
import { IRelevantNode } from '@/interfaces/database/flow';
import { get } from 'lodash';
import { memo } from 'react';
import { useReplaceIdWithName } from '../../hooks';
import styles from './index.less';
import NodeHeader from './node-header';
function InnerRelevantNode({ id, data, selected }: NodeProps<IRelevantNode>) {
const yes = get(data, 'form.yes');
const no = get(data, 'form.no');
const replaceIdWithName = useReplaceIdWithName();
const { theme } = useTheme();
return (
<section
className={classNames(
styles.logicNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
type="target"
position={Position.Left}
isConnectable
className={styles.handle}
id={'a'}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'yes'}
style={{ ...RightHandleStyle, top: 57 + 20 }}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable
className={styles.handle}
id={'no'}
style={{ ...RightHandleStyle, top: 115 + 20 }}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex vertical gap={10}>
<Flex vertical>
<div className={styles.relevantLabel}>Yes</div>
<div className={styles.nodeText}>{replaceIdWithName(yes)}</div>
</Flex>
<Flex vertical>
<div className={styles.relevantLabel}>No</div>
<div className={styles.nodeText}>{replaceIdWithName(no)}</div>
</Flex>
</Flex>
</section>
);
}
export const RelevantNode = memo(InnerRelevantNode);

View File

@@ -1,78 +0,0 @@
import { useTheme } from '@/components/theme-provider';
import { Handle, NodeProps, Position } from '@xyflow/react';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { IGenerateParameter } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { ITemplateNode } from '@/interfaces/database/flow';
import { memo } from 'react';
import styles from './index.less';
function InnerTemplateNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<ITemplateNode>) {
const parameters: IGenerateParameter[] = get(data, 'form.parameters', []);
const getLabel = useGetComponentLabelByValue(id);
const { theme } = useTheme();
return (
<section
className={classNames(
styles.logicNode,
theme === 'dark' ? styles.dark : '',
{
[styles.selectedNode]: selected,
},
)}
>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
id="b"
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
className={styles.nodeHeader}
></NodeHeader>
<Flex gap={8} vertical className={styles.generateParameters}>
{parameters.map((x) => (
<Flex
key={x.id}
align="center"
gap={6}
className={styles.conditionBlock}
>
<label htmlFor="">{x.key}</label>
<span className={styles.parameterValue}>
{getLabel(x.component_id)}
</span>
</Flex>
))}
</Flex>
</section>
);
}
export const TemplateNode = memo(InnerTemplateNode);

View File

@@ -613,7 +613,7 @@ export const CategorizeAnchorPointPositions = [
// key is the source of the edge, value is the target of the edge
// no connection lines are allowed between key and value
export const RestrictedUpstreamMap = {
[Operator.Begin]: [Operator.Relevant],
[Operator.Begin]: [Operator.Begin],
[Operator.Categorize]: [Operator.Begin, Operator.Categorize],
[Operator.Retrieval]: [Operator.Begin, Operator.Retrieval],
[Operator.Message]: [
@@ -623,12 +623,10 @@ export const RestrictedUpstreamMap = {
Operator.RewriteQuestion,
Operator.Categorize,
],
[Operator.Relevant]: [Operator.Begin],
[Operator.RewriteQuestion]: [
Operator.Begin,
Operator.Message,
Operator.RewriteQuestion,
Operator.Relevant,
],
[Operator.DuckDuckGo]: [Operator.Begin, Operator.Retrieval],
[Operator.Wikipedia]: [Operator.Begin, Operator.Retrieval],
@@ -678,7 +676,6 @@ export const NodeMap = {
[Operator.Categorize]: 'categorizeNode',
[Operator.Retrieval]: 'retrievalNode',
[Operator.Message]: 'messageNode',
[Operator.Relevant]: 'relevantNode',
[Operator.RewriteQuestion]: 'rewriteNode',
[Operator.DuckDuckGo]: 'ragNode',
[Operator.Wikipedia]: 'ragNode',

View File

@@ -23,7 +23,6 @@ import LoopForm from '../form/loop-form';
import MessageForm from '../form/message-form';
import ParserForm from '../form/parser-form';
import PubMedForm from '../form/pubmed-form';
import RelevantForm from '../form/relevant-form';
import RetrievalForm from '../form/retrieval-form/next';
import RewriteQuestionForm from '../form/rewrite-question-form';
import SearXNGForm from '../form/searxng-form';
@@ -54,9 +53,6 @@ export const FormConfigMap = {
[Operator.Message]: {
component: MessageForm,
},
[Operator.Relevant]: {
component: RelevantForm,
},
[Operator.RewriteQuestion]: {
component: RewriteQuestionForm,
},

View File

@@ -1,41 +0,0 @@
import pick from 'lodash/pick';
import { useCallback, useEffect } from 'react';
import { IOperatorForm } from '../../interface';
import useGraphStore from '../../store';
export const useBuildRelevantOptions = () => {
const nodes = useGraphStore((state) => state.nodes);
const buildRelevantOptions = useCallback(
(toList: string[]) => {
return nodes
.filter(
(x) => !toList.some((y) => y === x.id), // filter out selected values in other to fields from the current drop-down box options
)
.map((x) => ({ label: x.data.name, value: x.id }));
},
[nodes],
);
return buildRelevantOptions;
};
/**
* monitor changes in the connection and synchronize the target to the yes and no fields of the form
* similar to the categorize-form's useHandleFormValuesChange method
* @param param0
*/
export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => {
const getNode = useGraphStore((state) => state.getNode);
const node = getNode(nodeId);
const watchFormChanges = useCallback(() => {
if (node) {
form?.setFieldsValue(pick(node, ['yes', 'no']));
}
}, [node, form]);
useEffect(() => {
watchFormChanges();
}, [watchFormChanges]);
};

View File

@@ -1,49 +0,0 @@
import LLMSelect from '@/components/llm-select';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import { useWatchConnectionChanges } from './hooks';
const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const buildRelevantOptions = useBuildFormSelectOptions(
Operator.Relevant,
node?.id,
);
useWatchConnectionChanges({ nodeId: node?.id, form });
return (
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onValuesChange={onValuesChange}
autoComplete="off"
form={form}
>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<Form.Item label={t('yes')} name={'yes'}>
<Select
allowClear
options={buildRelevantOptions([form?.getFieldValue('no')])}
/>
</Form.Item>
<Form.Item label={t('no')} name={'no'}>
<Select
allowClear
options={buildRelevantOptions([form?.getFieldValue('yes')])}
/>
</Form.Item>
</Form>
);
};
export default RelevantForm;

View File

@@ -34,7 +34,6 @@ import {
initialNoteValues,
initialParserValues,
initialPubMedValues,
initialRelevantValues,
initialRetrievalValues,
initialRewriteQuestionValues,
initialSearXNGValues,
@@ -129,7 +128,6 @@ export const useInitializeOperatorParams = () => {
[Operator.Begin]: initialBeginValues,
[Operator.Retrieval]: initialRetrievalValues,
[Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId },
[Operator.Relevant]: { ...initialRelevantValues, llm_id: llmId },
[Operator.RewriteQuestion]: {
...initialRewriteQuestionValues,
llm_id: llmId,

View File

@@ -125,12 +125,7 @@ export function useBuildParentOutputOptions(parentId?: string) {
}
// exclude nodes with branches
const ExcludedNodes = [
Operator.Categorize,
Operator.Relevant,
Operator.Begin,
Operator.Note,
];
const ExcludedNodes = [Operator.Categorize, Operator.Begin, Operator.Note];
const StringList = [
BeginQueryType.Line,

View File

@@ -301,12 +301,7 @@ const useGraphStore = create<RFState>()(
});
},
deleteEdgeById: (id: string) => {
const {
edges,
updateNodeForm,
getOperatorTypeFromId,
updateSwitchFormData,
} = get();
const { edges, getOperatorTypeFromId, updateSwitchFormData } = get();
const currentEdge = edges.find((x) => x.id === id);
if (currentEdge) {
@@ -314,11 +309,6 @@ const useGraphStore = create<RFState>()(
const operatorType = getOperatorTypeFromId(source);
// After deleting the edge, set the corresponding field in the node's form field to undefined
switch (operatorType) {
case Operator.Relevant:
updateNodeForm(source, {
[sourceHandle as string]: undefined,
});
break;
// case Operator.Categorize:
// if (sourceHandle)
// updateNodeForm(source, undefined, [

View File

@@ -569,12 +569,6 @@ export const duplicateNodeForm = (nodeData?: RAGFlowNodeType['data']) => {
}, {});
}
// Delete the downstream nodes corresponding to the yes and no fields of the Relevant operator
if (nodeData?.label === Operator.Relevant) {
form.yes = undefined;
form.no = undefined;
}
return {
...(nodeData ?? { label: '' }),
form,

View File

@@ -1,6 +1,5 @@
import { Images } from '@/constants/common';
import { api_host } from '@/utils/api';
// import { Flex } from 'antd';
import { useParams, useSearchParams } from 'umi';
// import Docx from './docx';
// import Excel from './excel';

View File

@@ -1,55 +0,0 @@
import { Authorization } from '@/constants/authorization';
import { getAuthorization } from '@/utils/authorization-util';
import { Skeleton } from 'antd';
import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter';
import FileError from '../file-error';
import { useCatchError } from '../hooks';
type PdfLoaderProps = React.ComponentProps<typeof PdfLoader> & {
httpHeaders?: Record<string, string>;
};
const Loader = PdfLoader as React.ComponentType<PdfLoaderProps>;
interface IProps {
url: string;
}
const PdfPreviewer = ({ url }: IProps) => {
const { error } = useCatchError(url);
const resetHash = () => {};
const httpHeaders = {
[Authorization]: getAuthorization(),
};
return (
<div style={{ width: '100%', height: '100%' }}>
<Loader
url={url}
httpHeaders={httpHeaders}
beforeLoad={<Skeleton active />}
workerSrc="/pdfjs-dist/pdf.worker.min.js"
errorMessage={<FileError>{error}</FileError>}
onError={(e) => {
console.warn(e);
}}
>
{(pdfDocument) => {
return (
<PdfHighlighter
pdfDocument={pdfDocument}
enableAreaSelection={(event) => event.altKey}
onScrollChange={resetHash}
scrollRef={() => {}}
onSelectionFinished={() => null}
highlightTransform={() => {
return <div></div>;
}}
highlights={[]}
/>
);
}}
</Loader>
</div>
);
};
export default PdfPreviewer;

View File

@@ -1,4 +1,5 @@
import HightLightMarkdown from '@/components/highlight-markdown';
import message from '@/components/ui/message';
import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
@@ -7,7 +8,6 @@ import {
LanguageAbbreviationMap,
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { message } from 'antd';
import { useCallback, useMemo, useState } from 'react';
type IEmbedAppModalProps = {

View File

@@ -1,24 +0,0 @@
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { PropsWithChildren } from 'react';
export function Title({ children }: PropsWithChildren) {
return <span className="font-bold text-xl">{children}</span>;
}
type ProfileSettingWrapperCardProps = {
header: React.ReactNode;
} & PropsWithChildren;
export function ProfileSettingWrapperCard({
header,
children,
}: ProfileSettingWrapperCardProps) {
return (
<Card className="w-full border-border-button bg-transparent relative">
<CardHeader className="border-b border-border-button p-5">
{header}
</CardHeader>
<CardContent className="p-5">{children}</CardContent>
</Card>
);
}

View File

@@ -1,20 +0,0 @@
import { ProfileSettingRouteKey } from '@/constants/setting';
import { useSecondPathName } from '@/hooks/route-hook';
export const useGetPageTitle = (): string => {
const pathName = useSecondPathName();
const LabelMap = {
[ProfileSettingRouteKey.Profile]: 'User profile',
[ProfileSettingRouteKey.Plan]: 'Plan & balance',
[ProfileSettingRouteKey.Model]: 'Model management',
[ProfileSettingRouteKey.System]: 'System',
[ProfileSettingRouteKey.Api]: 'Api',
[ProfileSettingRouteKey.Team]: 'Team management',
[ProfileSettingRouteKey.Prompt]: 'Prompt management',
[ProfileSettingRouteKey.Chunk]: 'Chunking method',
[ProfileSettingRouteKey.Logout]: 'Logout',
};
return LabelMap[pathName as ProfileSettingRouteKey];
};

View File

@@ -1,47 +0,0 @@
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { House } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Outlet } from 'umi';
import { SideBar } from './sidebar';
export default function ProfileSetting() {
const { navigateToHome } = useNavigatePage();
const { t } = useTranslation();
return (
<div className="flex flex-col w-full h-screen bg-background text-foreground">
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToHome}>
<House className="size-4" />
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{t('setting.profile')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="flex flex-1 bg-muted/50">
<SideBar></SideBar>
<main className="flex-1 ">
<Outlet></Outlet>
</main>
</div>
</div>
);
}

View File

@@ -1,178 +0,0 @@
import { Collapse } from '@/components/collapse';
import { Button, ButtonLoading } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { useGetMcpServer, useTestMcpServer } from '@/hooks/use-mcp-request';
import { IModalProps } from '@/interfaces/common';
import { IMCPTool, IMCPToolObject } from '@/interfaces/database/mcp';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { isEmpty, omit, pick } from 'lodash';
import { RefreshCw } from 'lucide-react';
import {
MouseEventHandler,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
EditMcpForm,
FormId,
ServerType,
useBuildFormSchema,
} from './edit-mcp-form';
import { McpToolCard } from './tool-card';
function transferToolToArray(tools: IMCPToolObject) {
return Object.entries(tools).reduce<IMCPTool[]>((pre, [name, tool]) => {
pre.push({ ...tool, name });
return pre;
}, []);
}
const DefaultValues = {
name: '',
server_type: ServerType.SSE,
url: '',
};
export function EditMcpDialog({
hideModal,
loading,
onOk,
id,
}: IModalProps<any> & { id: string }) {
const { t } = useTranslation();
const {
testMcpServer,
data: testData,
loading: testLoading,
} = useTestMcpServer();
const [isTriggeredBySaving, setIsTriggeredBySaving] = useState(false);
const FormSchema = useBuildFormSchema();
const [collapseOpen, setCollapseOpen] = useState(true);
const { data } = useGetMcpServer(id);
const [fieldChanged, setFieldChanged] = useState(false);
const tools = useMemo(() => {
return testData?.data || [];
}, [testData?.data]);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: DefaultValues,
});
const handleTest: MouseEventHandler<HTMLButtonElement> = useCallback((e) => {
e.stopPropagation();
setIsTriggeredBySaving(false);
}, []);
const handleSave: MouseEventHandler<HTMLButtonElement> = useCallback(() => {
setIsTriggeredBySaving(true);
}, []);
const handleOk = async (values: z.infer<typeof FormSchema>) => {
const nextValues = {
...omit(values, 'authorization_token'),
variables: { authorization_token: values.authorization_token },
headers: { Authorization: 'Bearer ${authorization_token}' },
};
if (isTriggeredBySaving) {
onOk?.(nextValues);
} else {
const ret = await testMcpServer(nextValues);
if (ret.code === 0) {
setFieldChanged(false);
}
}
};
useEffect(() => {
if (!isEmpty(data)) {
form.reset(pick(data, ['name', 'server_type', 'url']));
}
}, [data, form]);
const nextTools = useMemo(() => {
return isEmpty(tools)
? transferToolToArray(data.variables?.tools || {})
: tools;
}, [data.variables?.tools, tools]);
const disabled = !!!tools?.length || testLoading || fieldChanged;
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{id ? t('mcp.editMCP') : t('mcp.addMCP')}</DialogTitle>
</DialogHeader>
<EditMcpForm
onOk={handleOk}
form={form}
setFieldChanged={setFieldChanged}
></EditMcpForm>
<Card className="bg-transparent">
<CardContent className="p-3">
<Collapse
title={
<div>
{nextTools?.length || 0} {t('mcp.toolsAvailable')}
</div>
}
open={collapseOpen}
onOpenChange={setCollapseOpen}
rightContent={
<Button
variant={'transparent'}
form={FormId}
type="submit"
onClick={handleTest}
className="border-none p-0 hover:bg-transparent"
>
<RefreshCw
className={cn('text-text-secondary', {
'animate-spin': testLoading,
})}
/>
</Button>
}
>
<div className="overflow-auto max-h-80 divide-y bg-bg-card rounded-md px-2.5">
{nextTools?.map((x) => (
<McpToolCard key={x.name} data={x}></McpToolCard>
))}
</div>
</Collapse>
</CardContent>
</Card>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">{t('common.cancel')}</Button>
</DialogClose>
<ButtonLoading
type="submit"
form={FormId}
loading={loading}
onClick={handleSave}
disabled={disabled}
>
{t('common.save')}
</ButtonLoading>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,171 +0,0 @@
'use client';
import { UseFormReturn } from 'react-hook-form';
import { z } from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { IModalProps } from '@/interfaces/common';
import { buildOptions } from '@/utils/form';
import { loader } from '@monaco-editor/react';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
loader.config({ paths: { vs: '/vs' } });
export const FormId = 'EditMcpForm';
export enum ServerType {
SSE = 'sse',
StreamableHttp = 'streamable-http',
}
const ServerTypeOptions = buildOptions(ServerType);
export function useBuildFormSchema() {
const { t } = useTranslation();
const FormSchema = z.object({
name: z
.string()
.min(1, {
message: t('common.mcp.namePlaceholder'),
})
.regex(/^[a-zA-Z0-9_-]{1,64}$/, {
message: t('common.mcp.nameRequired'),
})
.trim(),
url: z
.string()
.url()
.min(1, {
message: t('common.mcp.urlPlaceholder'),
})
.trim(),
server_type: z
.string()
.min(1, {
message: t('common.pleaseSelect'),
})
.trim(),
authorization_token: z.string().optional(),
});
return FormSchema;
}
export function EditMcpForm({
form,
onOk,
setFieldChanged,
}: IModalProps<any> & {
form: UseFormReturn<any>;
setFieldChanged: Dispatch<SetStateAction<boolean>>;
}) {
const { t } = useTranslation();
const FormSchema = useBuildFormSchema();
function onSubmit(data: z.infer<typeof FormSchema>) {
onOk?.(data);
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
id={FormId}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('common.name')}</FormLabel>
<FormControl>
<Input
placeholder={t('common.mcp.namePlaceholder')}
{...field}
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('mcp.url')}</FormLabel>
<FormControl>
<Input
placeholder={t('common.mcp.urlPlaceholder')}
{...field}
autoComplete="off"
onChange={(e) => {
field.onChange(e.target.value.trim());
setFieldChanged(true);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="server_type"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('mcp.serverType')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
autoComplete="off"
options={ServerTypeOptions}
onChange={(value) => {
field.onChange(value);
setFieldChanged(true);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="authorization_token"
render={({ field }) => (
<FormItem>
<FormLabel>Authorization Token</FormLabel>
<FormControl>
<Input
placeholder={t('common.mcp.tokenPlaceholder')}
{...field}
autoComplete="off"
type="password"
onChange={(e) => {
field.onChange(e.target.value.trim());
setFieldChanged(true);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}

View File

@@ -1,72 +0,0 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { FileUploader } from '@/components/file-uploader';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { FileMimeType, Platform } from '@/constants/common';
import { TagRenameId } from '@/constants/knowledge';
import { IModalProps } from '@/interfaces/common';
import { useTranslation } from 'react-i18next';
export function ImportMcpForm({ hideModal, onOk }: IModalProps<any>) {
const { t } = useTranslation();
const FormSchema = z.object({
platform: z
.string()
.min(1, {
message: t('common.namePlaceholder'),
})
.trim(),
fileList: z.array(z.instanceof(File)),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: { platform: Platform.RAGFlow },
});
async function onSubmit(data: z.infer<typeof FormSchema>) {
const ret = await onOk?.(data);
if (ret) {
hideModal?.();
}
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6"
id={TagRenameId}
>
<FormField
control={form.control}
name="fileList"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.name')}</FormLabel>
<FormControl>
<FileUploader
value={field.value}
onValueChange={field.onChange}
accept={{ '*.json': [FileMimeType.Json] }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}

View File

@@ -1,36 +0,0 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { LoadingButton } from '@/components/ui/loading-button';
import { TagRenameId } from '@/constants/knowledge';
import { IModalProps } from '@/interfaces/common';
import { useTranslation } from 'react-i18next';
import { ImportMcpForm } from './import-mcp-form';
export function ImportMcpDialog({
hideModal,
onOk,
loading,
}: IModalProps<any>) {
const { t } = useTranslation();
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('mcp.import')}</DialogTitle>
</DialogHeader>
<ImportMcpForm hideModal={hideModal} onOk={onOk}></ImportMcpForm>
<DialogFooter>
<LoadingButton type="submit" form={TagRenameId} loading={loading}>
{t('common.save')}
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,157 +0,0 @@
import { CardContainer } from '@/components/card-container';
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import Spotlight from '@/components/spotlight';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { SearchInput } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import { useListMcpServer } from '@/hooks/use-mcp-request';
import { pick } from 'lodash';
import {
Download,
LayoutList,
ListChecks,
Plus,
Trash2,
Upload,
} from 'lucide-react';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ProfileSettingWrapperCard } from '../components';
import { EditMcpDialog } from './edit-mcp-dialog';
import { ImportMcpDialog } from './import-mcp-dialog';
import { McpCard } from './mcp-card';
import { useBulkOperateMCP } from './use-bulk-operate-mcp';
import { useEditMcp } from './use-edit-mcp';
import { useImportMcp } from './use-import-mcp';
export default function McpServer() {
const { data, setPagination, searchString, handleInputChange, pagination } =
useListMcpServer();
const { editVisible, showEditModal, hideEditModal, handleOk, id, loading } =
useEditMcp();
const {
selectedList,
handleSelectChange,
handleDelete,
handleExportMcp,
handleSelectAll,
} = useBulkOperateMCP(data.mcp_servers);
const { t } = useTranslation();
const { importVisible, showImportModal, hideImportModal, onImportOk } =
useImportMcp();
const [isSelectionMode, setSelectionMode] = useState(false);
const handlePageChange = useCallback(
(page: number, pageSize?: number) => {
setPagination({ page, pageSize });
},
[setPagination],
);
const switchSelectionMode = useCallback(() => {
setSelectionMode((prev) => !prev);
}, []);
return (
<ProfileSettingWrapperCard
header={
<>
<div className="text-text-primary text-2xl font-semibold">
{t('mcp.mcpServers')}
</div>
<section className="flex items-center justify-between">
<div className="text-text-secondary">
{t('mcp.customizeTheListOfMcpServers')}
</div>
<div className="flex gap-5">
<SearchInput
className="w-40"
value={searchString}
onChange={handleInputChange}
></SearchInput>
<Button variant={'secondary'} onClick={switchSelectionMode}>
{isSelectionMode ? (
<ListChecks className="size-3.5" />
) : (
<LayoutList className="size-3.5" />
)}
{t(`mcp.${isSelectionMode ? 'exitBulkManage' : 'bulkManage'}`)}
</Button>
<Button variant={'secondary'} onClick={showImportModal}>
<Download className="size-3.5" />
{t('mcp.import')}
</Button>
<Button onClick={showEditModal('')}>
<Plus className="size-3.5" /> {t('mcp.addMCP')}
</Button>
</div>
</section>
</>
}
>
{isSelectionMode && (
<section className="pb-5 flex items-center">
<Checkbox id="all" onCheckedChange={handleSelectAll} />
<Label
className="pl-2 text-text-primary cursor-pointer"
htmlFor="all"
>
{t('common.selectAll')}
</Label>
<span className="text-text-secondary pr-10 pl-5">
{t('mcp.selected')} {selectedList.length}
</span>
<div className="flex gap-10 items-center">
<Button variant={'secondary'} onClick={handleExportMcp}>
<Upload className="size-3.5"></Upload>
{t('mcp.export')}
</Button>
<ConfirmDeleteDialog onOk={handleDelete}>
<Button variant={'danger'}>
<Trash2 className="size-3.5 cursor-pointer" />
{t('common.delete')}
</Button>
</ConfirmDeleteDialog>
</div>
</section>
)}
<CardContainer>
{data.mcp_servers.map((item) => (
<McpCard
key={item.id}
data={item}
selectedList={selectedList}
handleSelectChange={handleSelectChange}
showEditModal={showEditModal}
isSelectionMode={isSelectionMode}
></McpCard>
))}
</CardContainer>
<div className="mt-8">
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total || 0}
onChange={handlePageChange}
></RAGFlowPagination>
</div>
{editVisible && (
<EditMcpDialog
hideModal={hideEditModal}
onOk={handleOk}
id={id}
loading={loading}
></EditMcpDialog>
)}
{importVisible && (
<ImportMcpDialog
hideModal={hideImportModal}
onOk={onImportOk}
></ImportMcpDialog>
)}
<Spotlight />
</ProfileSettingWrapperCard>
);
}

View File

@@ -1,74 +0,0 @@
import { Card, CardContent } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { IMcpServer } from '@/interfaces/database/mcp';
import { formatDate } from '@/utils/date';
import { isPlainObject } from 'lodash';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { McpOperation } from './mcp-operation';
import { UseBulkOperateMCPReturnType } from './use-bulk-operate-mcp';
import { UseEditMcpReturnType } from './use-edit-mcp';
export type DatasetCardProps = {
data: IMcpServer;
isSelectionMode: boolean;
} & Pick<UseBulkOperateMCPReturnType, 'handleSelectChange' | 'selectedList'> &
Pick<UseEditMcpReturnType, 'showEditModal'>;
export function McpCard({
data,
selectedList,
handleSelectChange,
showEditModal,
isSelectionMode,
}: DatasetCardProps) {
const { t } = useTranslation();
const toolLength = useMemo(() => {
const tools = data.variables?.tools;
if (isPlainObject(tools)) {
return Object.keys(tools || {}).length;
}
return 0;
}, [data.variables?.tools]);
const onCheckedChange = (checked: boolean) => {
if (typeof checked === 'boolean') {
handleSelectChange(data.id, checked);
}
};
return (
<Card key={data.id}>
<CardContent className="p-2.5 pt-2 group">
<section className="flex justify-between pb-2">
<h3 className="text-base font-normal truncate flex-1 text-text-primary">
{data.name}
</h3>
<div className="space-x-4">
{isSelectionMode ? (
<Checkbox
checked={selectedList.includes(data.id)}
onCheckedChange={onCheckedChange}
onClick={(e) => {
e.stopPropagation();
}}
/>
) : (
<McpOperation
mcpId={data.id}
showEditModal={showEditModal}
></McpOperation>
)}
</div>
</section>
<div className="flex justify-between items-end text-xs text-text-secondary">
<div className="w-full">
<div className="line-clamp-1 pb-1">
{toolLength} {t('mcp.cachedTools')}
</div>
<p>{formatDate(data.update_date)}</p>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,43 +0,0 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { RAGFlowTooltip } from '@/components/ui/tooltip';
import { useDeleteMcpServer } from '@/hooks/use-mcp-request';
import { PenLine, Trash2, Upload } from 'lucide-react';
import { MouseEventHandler, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { UseEditMcpReturnType } from './use-edit-mcp';
import { useExportMcp } from './use-export-mcp';
export function McpOperation({
mcpId,
showEditModal,
}: { mcpId: string } & Pick<UseEditMcpReturnType, 'showEditModal'>) {
const { t } = useTranslation();
const { deleteMcpServer } = useDeleteMcpServer();
const { handleExportMcpJson } = useExportMcp();
const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => {
deleteMcpServer([mcpId]);
}, [deleteMcpServer, mcpId]);
return (
<div className="hidden gap-3 group-hover:flex text-text-secondary">
<RAGFlowTooltip tooltip={t('mcp.export')}>
<Upload
className="size-3 cursor-pointer"
onClick={handleExportMcpJson([mcpId])}
/>
</RAGFlowTooltip>
<RAGFlowTooltip tooltip={t('common.edit')}>
<PenLine
className="size-3 cursor-pointer"
onClick={showEditModal(mcpId)}
/>
</RAGFlowTooltip>
<RAGFlowTooltip tooltip={t('common.delete')}>
<ConfirmDeleteDialog onOk={handleDelete}>
<Trash2 className="size-3 cursor-pointer" />
</ConfirmDeleteDialog>
</RAGFlowTooltip>
</div>
);
}

View File

@@ -1,16 +0,0 @@
import { IMCPTool } from '@/interfaces/database/mcp';
export type McpToolCardProps = {
data: IMCPTool;
};
export function McpToolCard({ data }: McpToolCardProps) {
return (
<section className="group py-2.5">
<h3 className="text-sm font-semibold line-clamp-1 pb-2">{data.name}</h3>
<div className="text-xs font-normal text-text-secondary">
{data.description}
</div>
</section>
);
}

View File

@@ -1,56 +0,0 @@
import { useDeleteMcpServer } from '@/hooks/use-mcp-request';
import { IMcpServer } from '@/interfaces/database/mcp';
import { Trash2, Upload } from 'lucide-react';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useExportMcp } from './use-export-mcp';
export function useBulkOperateMCP(mcpList: IMcpServer[]) {
const { t } = useTranslation();
const [selectedList, setSelectedList] = useState<Array<string>>([]);
const { deleteMcpServer } = useDeleteMcpServer();
const { handleExportMcpJson } = useExportMcp();
const handleDelete = useCallback(() => {
deleteMcpServer(selectedList);
}, [deleteMcpServer, selectedList]);
const handleSelectChange = useCallback((id: string, checked: boolean) => {
setSelectedList((list) => {
return checked ? [...list, id] : list.filter((item) => item !== id);
});
}, []);
const handleSelectAll = useCallback(
(checked: boolean) => {
setSelectedList(() => (checked ? mcpList.map((item) => item.id) : []));
},
[mcpList],
);
const list = [
{
id: 'export',
label: t('mcp.export'),
icon: <Upload />,
onClick: handleExportMcpJson(selectedList),
},
{
id: 'delete',
label: t('common.delete'),
icon: <Trash2 />,
onClick: handleDelete,
},
];
return {
list,
selectedList,
handleSelectChange,
handleDelete,
handleExportMcp: handleExportMcpJson(selectedList),
handleSelectAll,
};
}
export type UseBulkOperateMCPReturnType = ReturnType<typeof useBulkOperateMCP>;

View File

@@ -1,53 +0,0 @@
import { useSetModalState } from '@/hooks/common-hooks';
import {
useCreateMcpServer,
useUpdateMcpServer,
} from '@/hooks/use-mcp-request';
import { useCallback, useState } from 'react';
export const useEditMcp = () => {
const {
visible: editVisible,
hideModal: hideEditModal,
showModal: showEditModal,
} = useSetModalState();
const { createMcpServer, loading } = useCreateMcpServer();
const [id, setId] = useState('');
const { updateMcpServer, loading: updateLoading } = useUpdateMcpServer();
const handleShowModal = useCallback(
(id: string) => () => {
setId(id);
showEditModal();
},
[setId, showEditModal],
);
const handleOk = useCallback(
async (values: any) => {
let code;
if (id) {
code = await updateMcpServer({ ...values, mcp_id: id });
} else {
code = await createMcpServer(values);
}
if (code === 0) {
hideEditModal();
}
},
[createMcpServer, hideEditModal, id, updateMcpServer],
);
return {
editVisible,
hideEditModal,
showEditModal: handleShowModal,
loading: loading || updateLoading,
createMcpServer,
handleOk,
id,
};
};
export type UseEditMcpReturnType = ReturnType<typeof useEditMcp>;

View File

@@ -1,21 +0,0 @@
import { useExportMcpServer } from '@/hooks/use-mcp-request';
import { downloadJsonFile } from '@/utils/file-util';
import { useCallback } from 'react';
export function useExportMcp() {
const { exportMcpServer } = useExportMcpServer();
const handleExportMcpJson = useCallback(
(ids: string[]) => async () => {
const data = await exportMcpServer(ids);
if (data.code === 0) {
downloadJsonFile(data.data, `mcp.json`);
}
},
[exportMcpServer],
);
return {
handleExportMcpJson,
};
}

View File

@@ -1,73 +0,0 @@
import message from '@/components/ui/message';
import { FileMimeType } from '@/constants/common';
import { useSetModalState } from '@/hooks/common-hooks';
import { useImportMcpServer } from '@/hooks/use-mcp-request';
import { isEmpty } from 'lodash';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
const ServerEntrySchema = z.object({
authorization_token: z.string().optional(),
name: z.string().optional(),
tool_configuration: z.object({}).passthrough().optional(),
type: z.string(),
url: z.string().url(),
});
const McpConfigSchema = z.object({
mcpServers: z.record(ServerEntrySchema),
});
export const useImportMcp = () => {
const {
visible: importVisible,
hideModal: hideImportModal,
showModal: showImportModal,
} = useSetModalState();
const { t } = useTranslation();
const { importMcpServer, loading } = useImportMcpServer();
const onImportOk = useCallback(
async ({ fileList }: { fileList: File[] }) => {
if (fileList.length > 0) {
const file = fileList[0];
if (file.type !== FileMimeType.Json) {
message.error(t('flow.jsonUploadTypeErrorMessage'));
return;
}
const mcpStr = await file.text();
const errorMessage = t('flow.jsonUploadContentErrorMessage');
try {
const mcp = JSON.parse(mcpStr);
try {
McpConfigSchema.parse(mcp);
} catch (error) {
message.error('Incorrect data format');
return;
}
if (mcpStr && !isEmpty(mcp)) {
const ret = await importMcpServer(mcp);
if (ret.code === 0) {
hideImportModal();
}
} else {
message.error(errorMessage);
}
} catch (error) {
message.error(errorMessage);
}
}
},
[hideImportModal, importMcpServer, t],
);
return {
importVisible,
showImportModal,
hideImportModal,
onImportOk,
loading,
};
};

View File

@@ -1,44 +0,0 @@
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
AddModelCard,
ModelLibraryCard,
SystemModelSetting,
} from './model-card';
const addedModelList = new Array(4).fill(1);
const modelLibraryList = new Array(4).fill(1);
export default function ModelManagement() {
return (
<section className="p-8 space-y-8 ">
<div className="flex justify-between items-center ">
<h1 className="text-4xl font-bold">Team management</h1>
<Button>Unfinished</Button>
</div>
<div className="max-h-[84vh] overflow-auto">
<SystemModelSetting></SystemModelSetting>
<section className="mt-6">
<h2 className="text-2xl font-semibold mb-3">Added model</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4 gap-4">
{addedModelList.map((x, idx) => (
<AddModelCard key={idx}></AddModelCard>
))}
</div>
</section>
<section className="mt-6">
<div className="flex justify-between items-center mb-3">
<h2 className="text-2xl font-semibold ">Model library</h2>
<Input placeholder="search" className="w-1/5"></Input>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 gap-4">
{modelLibraryList.map((x, idx) => (
<ModelLibraryCard key={idx}></ModelLibraryCard>
))}
</div>
</section>
</div>
</section>
);
}

View File

@@ -1,136 +0,0 @@
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Key, MoreVertical, Plus, Trash2 } from 'lucide-react';
import { PropsWithChildren } from 'react';
const settings = [
{
title: 'GPT Model',
description:
'The default chat LLM all the newly created knowledgebase will use.',
model: 'DeepseekChat',
},
{
title: 'Embedding Model',
description:
'The default embedding model all the newly created knowledgebase will use.',
model: 'DeepseekChat',
},
{
title: 'Image Model',
description:
'The default multi-capable model all the newly created knowledgebase will use. It can generate a picture or video.',
model: 'DeepseekChat',
},
{
title: 'Speech2TXT Model',
description:
'The default ASR model all the newly created knowledgebase will use. Use this model to translate voices to text something text.',
model: 'DeepseekChat',
},
{
title: 'TTS Model',
description:
'The default text to speech model all the newly created knowledgebase will use.',
model: 'DeepseekChat',
},
];
function Title({ children }: PropsWithChildren) {
return <span className="font-bold text-xl">{children}</span>;
}
export function SystemModelSetting() {
return (
<Card>
<CardContent className="p-4 space-y-6">
{settings.map((x, idx) => (
<div key={idx} className="flex items-center">
<div className="flex-1 flex flex-col">
<span className="font-semibold text-base">{x.title}</span>
<span className="text-colors-text-neutral-standard">
{x.description}
</span>
</div>
<div className="flex-1">
<Select defaultValue="english">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="english">English</SelectItem>
</SelectContent>
</Select>
</div>
</div>
))}
</CardContent>
</Card>
);
}
export function AddModelCard() {
return (
<Card className="pt-4">
<CardContent className="space-y-4">
<div className="flex justify-between space-y-4">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Button variant={'outline'}>Sub models</Button>
</div>
<Title>Deep seek</Title>
<p>LLM,TEXT EMBEDDING, SPEECH2TEXT, MODERATION</p>
<Card>
<CardContent className="p-3 flex gap-2">
<Button variant={'secondary'}>
deepseek-chat <Trash2 />
</Button>
<Button variant={'secondary'}>
deepseek-code <Trash2 />
</Button>
</CardContent>
</Card>
<div className="flex justify-end gap-2">
<Button variant="secondary" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
<Button>
<Key /> API
</Button>
</div>
</CardContent>
</Card>
);
}
export function ModelLibraryCard() {
return (
<Card className="pt-4">
<CardContent className="space-y-4">
<Avatar className="mb-4">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Title>Deep seek</Title>
<p>LLM,TEXT EMBEDDING, SPEECH2TEXT, MODERATION</p>
<div className="text-right">
<Button>
<Plus /> Add
</Button>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,121 +0,0 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Segmented, SegmentedValue } from '@/components/ui/segmented';
import { CircleCheckBig, LogOut } from 'lucide-react';
import { useMemo, useState } from 'react';
import { PricingCard } from './pricing-card';
const pricingData = [
{
title: 'Free',
price: '$0',
description: 'Meh, just looking',
features: [
{ name: 'Project', value: '1 project' },
{ name: 'Storage', value: '1 Gb' },
{ name: 'Team', value: '2 members' },
{ name: 'Features', value: 'Basic features' },
],
buttonText: 'Current plan',
buttonVariant: 'outline' as const,
},
{
title: 'Pro',
price: '$16.00',
description: 'For professional use.',
features: [
{ name: 'Project', value: 'Unlimited projects' },
{ name: 'Storage', value: '100 Gb' },
{ name: 'Team', value: 'Unlimited members' },
{ name: 'Features', value: 'Basic features All advanced features' },
],
buttonText: 'Upgrade',
buttonVariant: 'default' as const,
isPro: true,
},
{
title: 'Enterprise',
price: 'Customed',
description:
'Get full capabilities and support for large-scale mission-critical systems.',
features: [
{ name: 'Project', value: 'Unlimited projects' },
{ name: 'Storage', value: '100 Gb' },
{ name: 'Team', value: 'Unlimited members' },
{ name: 'Features', value: 'Basic features All advanced features' },
],
buttonText: 'Contact us',
buttonVariant: 'secondary' as const,
isEnterprise: true,
},
];
export default function Plan() {
const [val, setVal] = useState('monthly');
const options = useMemo(() => {
return [
{
label: 'Monthly',
value: 'monthly',
},
{
label: 'Yearly',
value: 'yearly',
},
];
}, []);
const handleChange = (path: SegmentedValue) => {
setVal(path as string);
};
const list = [
'Full access to pro features',
'Exclusive analyze models',
'Create more teams',
'Invite more collaborators',
];
return (
<section className="p-8">
<h1 className="text-3xl font-bold mb-6">Plan & balance</h1>
<Card className="border-0 p-6 mb-6 divide-y divide-colors-outline-neutral-strong">
<div className="pb-2 flex justify-between text-xl">
<span className="font-bold ">Balance</span>
<span className="font-medium">$ 100.00</span>
</div>
<div className="flex items-center justify-between pt-3">
<span>The value equals to 1,000 tokens or 10.00 GBs of storage</span>
<Button size={'sm'}>
<LogOut />
Recharge
</Button>
</div>
</Card>
<Card className="pt-6 ">
<CardContent className="space-y-4">
<div className="font-bold text-xl">Upgrade to access</div>
<section className="grid grid-cols-2 gap-3">
{list.map((x, idx) => (
<div key={idx} className="flex items-center gap-2">
<CircleCheckBig className="size-4" />
<span>{x}</span>
</div>
))}
</section>
<Segmented
options={options}
value={val}
onChange={handleChange}
className="inline-flex"
></Segmented>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{pricingData.map((plan, index) => (
<PricingCard key={index} {...plan} />
))}
</div>
</CardContent>
</Card>
</section>
);
}

View File

@@ -1,86 +0,0 @@
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { Mail, Zap } from 'lucide-react';
interface PricingFeature {
name: string;
value: string;
tooltip?: string;
}
interface PricingCardProps {
title: string;
price: string;
description: string;
features: PricingFeature[];
buttonText: string;
buttonVariant?: 'default' | 'outline' | 'secondary';
badge?: string;
isPro?: boolean;
isEnterprise?: boolean;
}
export function PricingCard({
title,
price,
description,
features,
buttonText,
isPro,
isEnterprise,
}: PricingCardProps) {
const isFree = title === 'Free';
return (
<Card className="flex flex-col bg-colors-background-neutral-weak divide-y divide-colors-outline-neutral-strong p-4">
<CardHeader className=" justify-between p-0 pb-3 h-52">
<section>
<div className="flex items-center justify-between mb-2">
<Badge className="text-xs">
{isPro && <Zap className="mr-2 h-4 w-4" />}
{isEnterprise && <Mail className="mr-2 h-4 w-4" />}
{title}
</Badge>
</div>
<p className="text-sm text-colors-text-neutral-standard">
{description}
</p>
</section>
<section>
<div className="flex items-baseline text-3xl font-bold pb-3">
{price}
{price !== 'Customed' && (
<span className="text-sm font-normal">/mo</span>
)}
</div>
<Button
className={cn('w-full', {
'bg-colors-text-core-standard': !isFree,
})}
size={'sm'}
>
{isPro && <Zap className="mr-2 h-4 w-4" />}
{isEnterprise && <Mail />}
{buttonText}
</Button>
</section>
</CardHeader>
<CardContent className=" p-0 pt-3">
<ul className="space-y-2">
{features.map((feature, index) => (
<li key={index} className="">
<div className="text-colors-text-core-standard">
{feature.name}
</div>
<span className="text-sm">
<span className="font-medium">{feature.value}</span>
</span>
</li>
))}
</ul>
</CardContent>
</Card>
);
}

View File

@@ -1,151 +0,0 @@
// src/hooks/useProfile.ts
import {
useFetchUserInfo,
useSaveSetting,
} from '@/hooks/use-user-setting-request';
import { rsaPsw } from '@/utils';
import { useCallback, useEffect, useState } from 'react';
interface ProfileData {
userName: string;
timeZone: string;
currPasswd?: string;
newPasswd?: string;
avatar: string;
email: string;
confirmPasswd?: string;
}
export const EditType = {
editName: 'editName',
editTimeZone: 'editTimeZone',
editPassword: 'editPassword',
} as const;
export type IEditType = keyof typeof EditType;
export const modalTitle = {
[EditType.editName]: 'Edit Name',
[EditType.editTimeZone]: 'Edit Time Zone',
[EditType.editPassword]: 'Edit Password',
} as const;
export const useProfile = () => {
const { data: userInfo } = useFetchUserInfo();
const [profile, setProfile] = useState<ProfileData>({
userName: '',
avatar: '',
timeZone: '',
email: '',
currPasswd: '',
});
const [editType, setEditType] = useState<IEditType>(EditType.editName);
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState<Partial<ProfileData>>({});
const {
saveSetting,
loading: submitLoading,
data: saveSettingData,
} = useSaveSetting();
useEffect(() => {
// form.setValue('currPasswd', ''); // current password
const profile = {
userName: userInfo.nickname,
timeZone: userInfo.timezone,
avatar: userInfo.avatar || '',
email: userInfo.email,
currPasswd: userInfo.password,
};
setProfile(profile);
}, [userInfo, setProfile]);
useEffect(() => {
if (saveSettingData === 0) {
setIsEditing(false);
setEditForm({});
}
}, [saveSettingData]);
const onSubmit = (newProfile: ProfileData) => {
const payload: Partial<{
nickname: string;
password: string;
new_password: string;
avatar: string;
timezone: string;
}> = {
nickname: newProfile.userName,
avatar: newProfile.avatar,
timezone: newProfile.timeZone,
};
if (
'currPasswd' in newProfile &&
'newPasswd' in newProfile &&
newProfile.currPasswd &&
newProfile.newPasswd
) {
payload.password = rsaPsw(newProfile.currPasswd!) as string;
payload.new_password = rsaPsw(newProfile.newPasswd!) as string;
}
console.log('payload', payload);
if (editType === EditType.editName && payload.nickname) {
saveSetting({ nickname: payload.nickname });
setProfile(newProfile);
}
if (editType === EditType.editTimeZone && payload.timezone) {
saveSetting({ timezone: payload.timezone });
setProfile(newProfile);
}
if (editType === EditType.editPassword && payload.password) {
saveSetting({
password: payload.password,
new_password: payload.new_password,
});
setProfile(newProfile);
}
// saveSetting(payload);
};
const handleEditClick = useCallback(
(type: IEditType) => {
setEditForm(profile);
setEditType(type);
setIsEditing(true);
},
[profile],
);
const handleCancel = useCallback(() => {
setIsEditing(false);
setEditForm({});
}, []);
const handleSave = (data: ProfileData) => {
console.log('handleSave', data);
const newProfile = { ...profile, ...data };
onSubmit(newProfile);
// setIsEditing(false);
// setEditForm({});
};
const handleAvatarUpload = (avatar: string) => {
setProfile((prev) => ({ ...prev, avatar }));
saveSetting({ avatar });
};
return {
profile,
setProfile,
submitLoading: submitLoading,
isEditing,
editType,
editForm,
handleEditClick,
handleCancel,
handleSave,
handleAvatarUpload,
};
};

View File

@@ -1,412 +0,0 @@
// src/components/ProfilePage.tsx
import { AvatarUpload } from '@/components/avatar-upload';
import PasswordInput from '@/components/originui/password-input';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useTranslate } from '@/hooks/common-hooks';
import { TimezoneList } from '@/pages/user-setting/constants';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { Loader2Icon, PenLine } from 'lucide-react';
import { FC, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
const baseSchema = z.object({
userName: z
.string()
.min(1, { message: t('setting.usernameMessage') })
.trim(),
timeZone: z
.string()
.trim()
.min(1, { message: t('setting.timezonePlaceholder') }),
});
const nameSchema = baseSchema.extend({
currPasswd: z.string().optional(),
newPasswd: z.string().optional(),
confirmPasswd: z.string().optional(),
});
const passwordSchema = baseSchema
.extend({
currPasswd: z
.string({
required_error: t('setting.currentPasswordMessage'),
})
.trim(),
newPasswd: z
.string({
required_error: t('setting.newPasswordMessage'),
})
.trim()
.min(8, { message: t('setting.newPasswordDescription') }),
confirmPasswd: z
.string({
required_error: t('setting.confirmPasswordMessage'),
})
.trim()
.min(8, { message: t('setting.newPasswordDescription') }),
})
.superRefine((data, ctx) => {
if (
data.newPasswd &&
data.confirmPasswd &&
data.newPasswd !== data.confirmPasswd
) {
ctx.addIssue({
path: ['confirmPasswd'],
message: t('setting.confirmPasswordNonMatchMessage'),
code: z.ZodIssueCode.custom,
});
}
});
const ProfilePage: FC = () => {
const { t } = useTranslate('setting');
const {
profile,
editType,
isEditing,
submitLoading,
editForm,
handleEditClick,
handleCancel,
handleSave,
handleAvatarUpload,
} = useProfile();
const form = useForm<z.infer<typeof baseSchema | typeof passwordSchema>>({
resolver: zodResolver(
editType === EditType.editPassword ? passwordSchema : nameSchema,
),
defaultValues: {
userName: '',
timeZone: '',
},
// shouldUnregister: true,
});
useEffect(() => {
form.reset({ ...editForm, currPasswd: undefined });
}, [editForm, form]);
// const ModalContent: FC = () => {
// // let content = null;
// // if (editType === EditType.editName) {
// // content = editName();
// // }
// return (
// <>
// </>
// );
// };
return (
<div className="h-full bg-bg-base text-text-secondary p-5">
{/* Header */}
<header className="flex flex-col gap-1 justify-between items-start mb-6">
<h1 className="text-2xl font-bold text-text-primary">{t('profile')}</h1>
<div className="text-sm text-text-secondary mb-6">
{t('profileDescription')}
</div>
</header>
{/* Main Content */}
<div className="max-w-3xl space-y-11 w-3/4">
{/* Name */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">
{t('username')}
</label>
<div className="flex-1 flex items-center gap-4 min-w-60">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.userName}
</div>
<Button
variant={'secondary'}
type="button"
onClick={() => handleEditClick(EditType.editName)}
className="text-sm text-text-secondary flex gap-1 px-1"
>
<PenLine size={12} /> Edit
</Button>
</div>
</div>
{/* Avatar */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">{t('avatar')}</label>
<div className="flex items-center gap-4">
<AvatarUpload
value={profile.avatar}
onChange={handleAvatarUpload}
tips={'This will be displayed on your profile.'}
/>
</div>
</div>
{/* Time Zone */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">
{t('timezone')}
</label>
<div className="flex-1 flex items-center gap-4">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.timeZone}
</div>
<Button
variant={'secondary'}
type="button"
onClick={() => handleEditClick(EditType.editTimeZone)}
className="text-sm text-text-secondary flex gap-1 px-1"
>
<PenLine size={12} /> Edit
</Button>
</div>
</div>
{/* Email Address */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium"> {t('email')}</label>
<div className="flex-1 flex flex-col items-start gap-2">
<div className="text-sm text-text-primary flex-1 rounded-md py-1.5 ">
{profile.email}
</div>
<span className="text-text-secondary text-xs">
{t('emailDescription')}
</span>
</div>
</div>
{/* Password */}
<div className="flex items-start gap-4">
<label className="w-[190px] text-sm font-medium">
{t('password')}
</label>
<div className="flex-1 flex items-center gap-4">
<div className="text-sm text-text-primary border border-border-button flex-1 rounded-md py-1.5 px-2">
{profile.currPasswd ? '********' : ''}
</div>
<Button
variant={'secondary'}
type="button"
onClick={() => handleEditClick(EditType.editPassword)}
className="text-sm text-text-secondary flex gap-1 px-1"
>
<PenLine size={12} /> Edit
</Button>
</div>
</div>
</div>
{editType && (
<Modal
title={modalTitle[editType]}
open={isEditing}
showfooter={false}
onOpenChange={(open) => {
if (!open) {
handleCancel();
}
}}
className="!w-[480px]"
>
{/* <ModalContent /> */}
<Form {...form}>
<form
onSubmit={form.handleSubmit((data) => handleSave(data as any))}
className="flex flex-col mt-6 mb-8 ml-2 space-y-6 "
>
{editType === EditType.editName && (
<FormField
control={form.control}
name="userName"
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex flex-col w-full gap-2">
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
{t('username')}
</FormLabel>
<FormControl className="w-full">
<Input
placeholder=""
{...field}
className="bg-bg-input border-border-default"
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
)}
{editType === EditType.editTimeZone && (
<FormField
control={form.control}
name="timeZone"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel className="text-sm text-text-secondary whitespace-nowrap">
{t('timezone')}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl className="w-full bg-bg-input border-border-default">
<SelectTrigger>
<SelectValue placeholder="Select a timeZone" />
</SelectTrigger>
</FormControl>
<SelectContent>
{TimezoneList.map((timeStr) => (
<SelectItem key={timeStr} value={timeStr}>
{timeStr}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex w-full pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
)}
{editType === EditType.editPassword && (
<>
<FormField
control={form.control}
name="currPasswd"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel
required
className="text-sm flex justify-between text-text-secondary whitespace-nowrap"
>
{t('currentPassword')}
</FormLabel>
<FormControl className="w-full">
<PasswordInput
{...field}
autoComplete="current-password"
className="bg-bg-input border-border-default"
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPasswd"
render={({ field }) => (
<FormItem className=" items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel
required
className="text-sm text-text-secondary whitespace-nowrap"
>
{t('newPassword')}
</FormLabel>
<FormControl className="w-full">
<PasswordInput
{...field}
autoComplete="new-password"
className="bg-bg-input border-border-default"
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPasswd"
render={({ field }) => (
<FormItem className=" items-center space-y-0">
<div className="flex flex-col w-full gap-2">
<FormLabel
required
className="text-sm text-text-secondary whitespace-nowrap"
>
{t('confirmPassword')}
</FormLabel>
<FormControl className="w-full">
<PasswordInput
{...field}
className="bg-bg-input border-border-default"
autoComplete="new-password"
onBlur={() => {
form.trigger('confirmPasswd');
}}
onChange={(ev) => {
form.setValue(
'confirmPasswd',
ev.target.value.trim(),
);
}}
/>
</FormControl>
</div>
<div className="flex w-full pt-1">
<FormMessage />
</div>
</FormItem>
)}
/>
</>
)}
<div className="w-full text-right space-x-4 !mt-11">
<Button type="reset" variant="secondary">
{t('cancel')}
</Button>
<Button type="submit" disabled={submitLoading}>
{submitLoading && <Loader2Icon className="animate-spin" />}
{t('save', { keyPrefix: 'common' })}
</Button>
</div>
</form>
</Form>
</Modal>
)}
</div>
);
};
export default ProfilePage;

View File

@@ -1,48 +0,0 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Plus, Trash2 } from 'lucide-react';
import { Title } from '../components';
const text = `You are an intelligent assistant. Please summarize the content of the knowledge base to answer the question. Please list the data in the knowledge base and answer in detail. When all knowledge base content is irrelevant to the question, your answer must include the sentence "The answer you are looking for is not found in the knowledge base!" Answers need to consider chat history.
Here is the knowledge base:
{knowledge}
The above is the knowledge base.`;
const PromptManagement = () => {
const modelLibraryList = new Array(8).fill(1);
return (
<div className="p-8 ">
<div className="mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-4xl font-bold">Prompt templates</h1>
<Button size={'sm'}>
<Plus className="mr-2 h-4 w-4" />
Create template
</Button>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-6 gap-4">
{modelLibraryList.map((x, idx) => (
<Card className="p-0" key={idx}>
<CardContent className="space-y-4 p-4">
<Title>Prompt name</Title>
<p className="line-clamp-3">{text}</p>
<div className="flex justify-end gap-2">
<Button size={'sm'} variant={'secondary'}>
<Trash2 />
</Button>
<Button variant={'outline'} size={'sm'}>
Edit
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default PromptManagement;

View File

@@ -1,24 +0,0 @@
import { useLogout } from '@/hooks/use-login-request';
import { Routes } from '@/routes';
import { useCallback, useState } from 'react';
import { useNavigate } from 'umi';
export const useHandleMenuClick = () => {
const navigate = useNavigate();
const [active, setActive] = useState<Routes>();
const { logout } = useLogout();
const handleMenuClick = useCallback(
(key: Routes) => () => {
if (key === Routes.Logout) {
logout();
} else {
setActive(key);
navigate(`${Routes.ProfileSetting}${key}`);
}
},
[logout, navigate],
);
return { handleMenuClick, active };
};

View File

@@ -1,113 +0,0 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import ThemeToggle from '@/components/theme-toggle';
import { Button } from '@/components/ui/button';
import { useSecondPathName } from '@/hooks/route-hook';
import { useLogout } from '@/hooks/use-login-request';
import { useFetchUserInfo } from '@/hooks/use-user-setting-request';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import {
AlignEndVertical,
Banknote,
Box,
FileCog,
User,
Users,
} from 'lucide-react';
import { useHandleMenuClick } from './hooks';
const menuItems = [
{
section: 'Account & collaboration',
items: [
{ icon: User, label: 'Profile', key: Routes.Profile },
{ icon: Users, label: 'Team', key: Routes.Team },
{ icon: Banknote, label: 'Plan', key: Routes.Plan },
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
],
},
{
section: 'System configurations',
items: [
{
icon: Box,
label: 'Model management',
key: Routes.Model,
},
{
icon: FileCog,
label: 'Prompt management',
key: Routes.Prompt,
},
{
icon: AlignEndVertical,
label: 'Chunking method',
key: Routes.Chunk,
},
],
},
];
export function SideBar() {
const pathName = useSecondPathName();
const { data: userInfo } = useFetchUserInfo();
const { handleMenuClick, active } = useHandleMenuClick();
const { logout } = useLogout();
return (
<aside className="w-[303px] bg-bg-base flex flex-col">
<div className="px-6 flex gap-2 items-center">
<RAGFlowAvatar
avatar={userInfo?.avatar}
name={userInfo?.nickname}
isPerson
/>
<p className="text-sm text-text-primary">{userInfo?.email}</p>
</div>
<div className="flex-1 overflow-auto">
{menuItems.map((section, idx) => (
<div key={idx}>
{/* <h2 className="p-6 text-sm font-semibold">{section.section}</h2> */}
{section.items.map((item, itemIdx) => {
const hoverKey = pathName === item.key;
return (
<div key={itemIdx} className="mx-6 my-5 ">
<Button
variant={hoverKey ? 'secondary' : 'ghost'}
className={cn('w-full justify-start gap-2.5 p-3 relative', {
'bg-bg-card text-text-primary': active === item.key,
'bg-bg-base text-text-secondary': active !== item.key,
})}
onClick={handleMenuClick(item.key)}
>
<item.icon className="w-6 h-6" />
<span>{item.label}</span>
{/* {active && (
<div className="absolute right-0 w-[5px] h-[66px] bg-primary rounded-l-xl shadow-[0_0_5.94px_#7561ff,0_0_11.88px_#7561ff,0_0_41.58px_#7561ff,0_0_83.16px_#7561ff,0_0_142.56px_#7561ff,0_0_249.48px_#7561ff]" />
)} */}
</Button>
</div>
);
})}
</div>
))}
</div>
<div className="p-6 mt-auto ">
<div className="flex items-center gap-2 mb-6 justify-end">
<ThemeToggle />
</div>
<Button
variant="outline"
className="w-full gap-3 !bg-bg-base border !border-border-button !text-text-secondary"
onClick={() => {
logout();
}}
>
Log Out
</Button>
</div>
</aside>
);
}

View File

@@ -1,107 +0,0 @@
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
import { ChevronDown, MoreVertical, Plus, UserPlus } from 'lucide-react';
interface TeamMember {
email: string;
name: string;
role: string;
}
const TeamManagement = () => {
const teamMembers: TeamMember[] = [
{ email: 'yifanwu92@gmail.com', name: 'Yifan Wu', role: 'Admin' },
{ email: 'yifanwu92@gmail.com', name: 'Yifan Wu', role: 'Admin' },
];
const stats = {
project: 1,
token: '1,000',
storage: '1GB',
};
return (
<div className="p-8 ">
<div className=" mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-4xl font-bold">Team management</h1>
<Button size={'sm'}>
<Plus className="mr-2 h-4 w-4" />
Create team
</Button>
</div>
<div className="mb-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-semibold">Yifan's team</h2>
<Button variant="secondary" size="icon">
<ChevronDown className="h-4 w-4" />
</Button>
</div>
<Card className="border-0 p-6 mb-6">
<div className="grid grid-cols-3 gap-8">
<div>
<p className="text-sm text-gray-400 mb-2">Project</p>
<p className="text-2xl font-semibold">{stats.project}</p>
</div>
<div>
<p className="text-sm text-gray-400 mb-2">Token</p>
<p className="text-2xl font-semibold">{stats.token}</p>
</div>
<div>
<p className="text-sm text-gray-400 mb-2">Storage</p>
<p className="text-2xl font-semibold">{stats.storage}</p>
</div>
</div>
</Card>
<Card className="border-0 p-6">
<Table>
<TableBody>
{teamMembers.map((member, idx) => (
<TableRow key={idx}>
<TableCell>{member.email}</TableCell>
<TableCell>{member.name}</TableCell>
<TableCell className="flex items-center justify-end">
<span className="text-colors-text-core-standard">
{member.role}
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Button variant="outline" className="mt-6 ">
<UserPlus className="mr-2 h-4 w-4" />
Invite member
</Button>
</Card>
</div>
</div>
</div>
);
};
export default TeamManagement;

View File

@@ -268,41 +268,41 @@ const routes = [
layout: false,
component: `@/pages${Routes.Chunk}`,
},
{
path: Routes.ProfileSetting,
layout: false,
component: `@/pages${Routes.ProfileSetting}`,
routes: [
{
path: Routes.ProfileSetting,
redirect: `${Routes.ProfileProfile}`,
},
{
path: `${Routes.ProfileProfile}`,
component: `@/pages${Routes.ProfileProfile}`,
},
{
path: `${Routes.ProfileTeam}`,
component: `@/pages${Routes.ProfileTeam}`,
},
{
path: `${Routes.ProfilePlan}`,
component: `@/pages${Routes.ProfilePlan}`,
},
{
path: `${Routes.ProfileModel}`,
component: `@/pages${Routes.ProfileModel}`,
},
{
path: `${Routes.ProfilePrompt}`,
component: `@/pages${Routes.ProfilePrompt}`,
},
{
path: Routes.ProfileMcp,
component: `@/pages${Routes.ProfileMcp}`,
},
],
},
// {
// path: Routes.ProfileSetting,
// layout: false,
// component: `@/pages${Routes.ProfileSetting}`,
// routes: [
// {
// path: Routes.ProfileSetting,
// redirect: `${Routes.ProfileProfile}`,
// },
// {
// path: `${Routes.ProfileProfile}`,
// component: `@/pages${Routes.ProfileProfile}`,
// },
// {
// path: `${Routes.ProfileTeam}`,
// component: `@/pages${Routes.ProfileTeam}`,
// },
// {
// path: `${Routes.ProfilePlan}`,
// component: `@/pages${Routes.ProfilePlan}`,
// },
// {
// path: `${Routes.ProfileModel}`,
// component: `@/pages${Routes.ProfileModel}`,
// },
// {
// path: `${Routes.ProfilePrompt}`,
// component: `@/pages${Routes.ProfilePrompt}`,
// },
// {
// path: Routes.ProfileMcp,
// component: `@/pages${Routes.ProfileMcp}`,
// },
// ],
// },
{
path: '/user-setting',
component: '@/pages/user-setting',