+
{loading ? (
) : (
- //
{kbs
?.slice(0, 6)
@@ -43,9 +45,7 @@ export function Datasets() {
showDatasetRenameModal={showDatasetRenameModal}
>
))}
-
-
-
+ {}
//
)}
diff --git a/web/src/pages/login-next/index.tsx b/web/src/pages/login-next/index.tsx
index 23a8f1b83..64bf2f165 100644
--- a/web/src/pages/login-next/index.tsx
+++ b/web/src/pages/login-next/index.tsx
@@ -26,7 +26,6 @@ import {
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
-import { Eye, EyeOff } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { BgSvg } from './bg';
@@ -247,7 +246,7 @@ const Login = () => {
}
{...field}
/>
-
+ */}
diff --git a/web/src/pages/user-setting/components/user-setting-header/index.tsx b/web/src/pages/user-setting/components/user-setting-header/index.tsx
new file mode 100644
index 000000000..931e2681d
--- /dev/null
+++ b/web/src/pages/user-setting/components/user-setting-header/index.tsx
@@ -0,0 +1,44 @@
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+import { PropsWithChildren } from 'react';
+
+export const UserSettingHeader = ({
+ name,
+ description,
+}: {
+ name: string;
+ description?: string;
+}) => {
+ return (
+ <>
+
+ {name}
+ {description && (
+ {description}
+ )}
+
+ {/*
*/}
+ >
+ );
+};
+
+export function Title({ children }: PropsWithChildren) {
+ return
{children};
+}
+
+type ProfileSettingWrapperCardProps = {
+ header: React.ReactNode;
+} & PropsWithChildren;
+
+export function ProfileSettingWrapperCard({
+ header,
+ children,
+}: ProfileSettingWrapperCardProps) {
+ return (
+
+
+ {header}
+
+ {children}
+
+ );
+}
diff --git a/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx b/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx
index 82a4333aa..29527dd9a 100644
--- a/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx
+++ b/web/src/pages/user-setting/data-source/data-source-detail-page/log-table.tsx
@@ -217,8 +217,8 @@ export const DataSourceLogsTable = ({
))
) : (
-
- No results.
+
+ {t('common.noData')}
)}
diff --git a/web/src/pages/user-setting/data-source/index.tsx b/web/src/pages/user-setting/data-source/index.tsx
index 1e27dc6a3..80ceea1d7 100644
--- a/web/src/pages/user-setting/data-source/index.tsx
+++ b/web/src/pages/user-setting/data-source/index.tsx
@@ -3,8 +3,11 @@ import { useTranslation } from 'react-i18next';
import Spotlight from '@/components/spotlight';
import { Button } from '@/components/ui/button';
-import { Separator } from '@/components/ui/separator';
import { Plus } from 'lucide-react';
+import {
+ ProfileSettingWrapperCard,
+ UserSettingHeader,
+} from '../components/user-setting-header';
import AddDataSourceModal from './add-datasource-modal';
import { AddedSourceCard } from './component/added-source-card';
import { DataSourceInfo, DataSourceKey } from './contant';
@@ -93,60 +96,57 @@ const DataSource = () => {
};
return (
-
+
+ }
+ >
-
- {/*
*/}
-
-
- {t('setting.dataSources')}
-
- {t('setting.datasourceDescription')}
+
+
+
+ {categorizedList.map((item, index) => (
+
+ ))}
-
-
- {/* */}
-
-
-
- {categorizedList.map((item, index) => (
-
- ))}
-
-
-
- {/* */}
-
- {t('setting.availableSources')}
-
- {t('setting.availableSourcesDescription')}
+
+
+
+ {/* */}
+
+ {dataSourceTemplates.map((item, index) => (
+
+ ))}
-
-
-
- {/* */}
-
- {dataSourceTemplates.map((item, index) => (
-
- ))}
-
-
-
-
+
+
+
- {addingModalVisible && (
-
{
- console.log(data);
- handleAddOk(data);
- }}
- sourceData={addSource}
- >
- )}
-
+ {addingModalVisible && (
+
{
+ console.log(data);
+ handleAddOk(data);
+ }}
+ sourceData={addSource}
+ >
+ )}
+
+
);
};
diff --git a/web/src/pages/user-setting/index.tsx b/web/src/pages/user-setting/index.tsx
index 3f5644234..ec6bf015a 100644
--- a/web/src/pages/user-setting/index.tsx
+++ b/web/src/pages/user-setting/index.tsx
@@ -44,12 +44,7 @@ const UserSetting = () => {
)}
>
-
diff --git a/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx b/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx
new file mode 100644
index 000000000..3029d176a
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/edit-mcp-dialog.tsx
@@ -0,0 +1,178 @@
+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
((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 & { 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>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: DefaultValues,
+ });
+
+ const handleTest: MouseEventHandler = useCallback((e) => {
+ e.stopPropagation();
+ setIsTriggeredBySaving(false);
+ }, []);
+
+ const handleSave: MouseEventHandler = useCallback(() => {
+ setIsTriggeredBySaving(true);
+ }, []);
+
+ const handleOk = async (values: z.infer) => {
+ 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 (
+
+ }
+ open={collapseOpen}
+ onOpenChange={setCollapseOpen}
+ rightContent={
+
+ }
+ >
+
+ {nextTools?.map((x) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {t('common.save')}
+
+
+
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/edit-mcp-form.tsx b/web/src/pages/user-setting/mcp/edit-mcp-form.tsx
new file mode 100644
index 000000000..d6b49db11
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/edit-mcp-form.tsx
@@ -0,0 +1,171 @@
+'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
& {
+ form: UseFormReturn;
+ setFieldChanged: Dispatch>;
+}) {
+ const { t } = useTranslation();
+ const FormSchema = useBuildFormSchema();
+
+ function onSubmit(data: z.infer) {
+ onOk?.(data);
+ }
+
+ return (
+
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/import-mcp-dialog/import-mcp-form.tsx b/web/src/pages/user-setting/mcp/import-mcp-dialog/import-mcp-form.tsx
new file mode 100644
index 000000000..d34507308
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/import-mcp-dialog/import-mcp-form.tsx
@@ -0,0 +1,72 @@
+'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 { IModalProps } from '@/interfaces/common';
+import { TagRenameId } from '@/pages/add-knowledge/constant';
+import { useTranslation } from 'react-i18next';
+
+export function ImportMcpForm({ hideModal, onOk }: IModalProps) {
+ 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>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: { platform: Platform.RAGFlow },
+ });
+
+ async function onSubmit(data: z.infer) {
+ const ret = await onOk?.(data);
+ if (ret) {
+ hideModal?.();
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/import-mcp-dialog/index.tsx b/web/src/pages/user-setting/mcp/import-mcp-dialog/index.tsx
new file mode 100644
index 000000000..76b35387b
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/import-mcp-dialog/index.tsx
@@ -0,0 +1,36 @@
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { LoadingButton } from '@/components/ui/loading-button';
+import { IModalProps } from '@/interfaces/common';
+import { TagRenameId } from '@/pages/add-knowledge/constant';
+import { useTranslation } from 'react-i18next';
+import { ImportMcpForm } from './import-mcp-form';
+
+export function ImportMcpDialog({
+ hideModal,
+ onOk,
+ loading,
+}: IModalProps) {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/index.tsx b/web/src/pages/user-setting/mcp/index.tsx
new file mode 100644
index 000000000..b403f1bc5
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/index.tsx
@@ -0,0 +1,157 @@
+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/user-setting-header';
+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 (
+
+
+ {t('mcp.mcpServers')}
+
+
+
+ {t('mcp.customizeTheListOfMcpServers')}
+
+
+
+
+
+
+
+
+ >
+ }
+ >
+ {isSelectionMode && (
+
+
+
+
+ {t('mcp.selected')} {selectedList.length}
+
+
+
+
+
+
+
+
+ )}
+
+ {data.mcp_servers.map((item) => (
+
+ ))}
+
+
+
+
+ {editVisible && (
+
+ )}
+ {importVisible && (
+
+ )}
+
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/mcp-card.tsx b/web/src/pages/user-setting/mcp/mcp-card.tsx
new file mode 100644
index 000000000..b0cfcd162
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/mcp-card.tsx
@@ -0,0 +1,74 @@
+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 &
+ Pick;
+
+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 (
+
+
+
+
+ {data.name}
+
+
+ {isSelectionMode ? (
+ {
+ e.stopPropagation();
+ }}
+ />
+ ) : (
+
+ )}
+
+
+
+
+
+ {toolLength} {t('mcp.cachedTools')}
+
+
{formatDate(data.update_date)}
+
+
+
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/mcp-operation.tsx b/web/src/pages/user-setting/mcp/mcp-operation.tsx
new file mode 100644
index 000000000..9bbd9e5c8
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/mcp-operation.tsx
@@ -0,0 +1,43 @@
+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) {
+ const { t } = useTranslation();
+ const { deleteMcpServer } = useDeleteMcpServer();
+ const { handleExportMcpJson } = useExportMcp();
+
+ const handleDelete: MouseEventHandler = useCallback(() => {
+ deleteMcpServer([mcpId]);
+ }, [deleteMcpServer, mcpId]);
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/tool-card.tsx b/web/src/pages/user-setting/mcp/tool-card.tsx
new file mode 100644
index 000000000..123b4a611
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/tool-card.tsx
@@ -0,0 +1,16 @@
+import { IMCPTool } from '@/interfaces/database/mcp';
+
+export type McpToolCardProps = {
+ data: IMCPTool;
+};
+
+export function McpToolCard({ data }: McpToolCardProps) {
+ return (
+
+ {data.name}
+
+ {data.description}
+
+
+ );
+}
diff --git a/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx b/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx
new file mode 100644
index 000000000..2afef8347
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/use-bulk-operate-mcp.tsx
@@ -0,0 +1,56 @@
+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>([]);
+ 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: ,
+ onClick: handleExportMcpJson(selectedList),
+ },
+ {
+ id: 'delete',
+ label: t('common.delete'),
+ icon: ,
+ onClick: handleDelete,
+ },
+ ];
+
+ return {
+ list,
+ selectedList,
+ handleSelectChange,
+ handleDelete,
+ handleExportMcp: handleExportMcpJson(selectedList),
+ handleSelectAll,
+ };
+}
+
+export type UseBulkOperateMCPReturnType = ReturnType;
diff --git a/web/src/pages/user-setting/mcp/use-edit-mcp.ts b/web/src/pages/user-setting/mcp/use-edit-mcp.ts
new file mode 100644
index 000000000..09fc83be5
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/use-edit-mcp.ts
@@ -0,0 +1,53 @@
+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;
diff --git a/web/src/pages/user-setting/mcp/use-export-mcp.ts b/web/src/pages/user-setting/mcp/use-export-mcp.ts
new file mode 100644
index 000000000..636290c91
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/use-export-mcp.ts
@@ -0,0 +1,21 @@
+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,
+ };
+}
diff --git a/web/src/pages/user-setting/mcp/use-import-mcp.ts b/web/src/pages/user-setting/mcp/use-import-mcp.ts
new file mode 100644
index 000000000..33e844191
--- /dev/null
+++ b/web/src/pages/user-setting/mcp/use-import-mcp.ts
@@ -0,0 +1,73 @@
+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,
+ };
+};
diff --git a/web/src/pages/user-setting/profile/index.tsx b/web/src/pages/user-setting/profile/index.tsx
index da19afa0d..dceb2cdf3 100644
--- a/web/src/pages/user-setting/profile/index.tsx
+++ b/web/src/pages/user-setting/profile/index.tsx
@@ -20,7 +20,6 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
-import { Separator } from '@/components/ui/separator';
import { useTranslate } from '@/hooks/common-hooks';
import { TimezoneList } from '@/pages/user-setting/constants';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -29,6 +28,10 @@ import { Loader2Icon, PenLine } from 'lucide-react';
import { FC, useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
+import {
+ ProfileSettingWrapperCard,
+ UserSettingHeader,
+} from '../components/user-setting-header';
import { EditType, modalTitle, useProfile } from './hooks/use-profile';
const baseSchema = z.object({
@@ -123,18 +126,17 @@ const ProfilePage: FC = () => {
// };
return (
-
+ //
+
+ }
+ >
- {/* Header */}
-
-
+
{/* Main Content */}
{/* Name */}
@@ -411,7 +413,8 @@ const ProfilePage: FC = () => {
)}
-
+
+ //
);
};
diff --git a/web/src/pages/user-setting/setting-model/components/modal-card.tsx b/web/src/pages/user-setting/setting-model/components/modal-card.tsx
index 90950e64e..270f9b4b5 100644
--- a/web/src/pages/user-setting/setting-model/components/modal-card.tsx
+++ b/web/src/pages/user-setting/setting-model/components/modal-card.tsx
@@ -97,7 +97,7 @@ export const ModelProviderCard: FC
= ({
className="px-3 py-1 text-sm bg-bg-input hover:bg-bg-input text-text-primary rounded-md transition-colors flex items-center space-x-1"
>
{visible ? t('hideModels') : t('showMoreModels')}
- {visible ? : }
+ {!visible ? : }