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

### What problem does this PR solve?

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


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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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