mirror of
https://gitee.com/infiniflow/ragflow.git
synced 2025-12-06 07:19:03 +08:00
### 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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
77
web/src/components/feedback-dialog.tsx
Normal file
77
web/src/components/feedback-dialog.tsx
Normal 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;
|
||||
@@ -1,13 +0,0 @@
|
||||
.uploader {
|
||||
:global {
|
||||
.ant-upload-list {
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uploadLimit {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -1,4 +0,0 @@
|
||||
.delete {
|
||||
// height: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
33
web/src/components/prompt-dialog.tsx
Normal file
33
web/src/components/prompt-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.selectFilesCollapse {
|
||||
:global(.ant-collapse-header) {
|
||||
padding-left: 22px;
|
||||
}
|
||||
margin-bottom: 32px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.selectFilesTitle {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,6 @@ export enum Operator {
|
||||
Retrieval = 'Retrieval',
|
||||
Categorize = 'Categorize',
|
||||
Message = 'Message',
|
||||
Relevant = 'Relevant',
|
||||
RewriteQuestion = 'RewriteQuestion',
|
||||
DuckDuckGo = 'DuckDuckGo',
|
||||
Wikipedia = 'Wikipedia',
|
||||
|
||||
@@ -856,7 +856,6 @@ export default {
|
||||
generate: 'Generieren',
|
||||
answer: 'Interagieren',
|
||||
categorize: 'Kategorisieren',
|
||||
relevant: 'Relevant',
|
||||
rewriteQuestion: 'Umschreiben',
|
||||
rewrite: 'Umschreiben',
|
||||
begin: 'Beginn',
|
||||
|
||||
@@ -1217,7 +1217,6 @@ Example: Virtual Hosted Style`,
|
||||
generate: 'Generate',
|
||||
answer: 'Interact',
|
||||
categorize: 'Categorize',
|
||||
relevant: 'Relevant',
|
||||
rewriteQuestion: 'Rewrite',
|
||||
rewrite: 'Rewrite',
|
||||
begin: 'Begin',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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>;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user