feat(web): AWEL flow 2.0 frontend codes (#1898)

Co-authored-by: Fangyin Cheng <staneyffer@gmail.com>
Co-authored-by: 谨欣 <echo.cmy@antgroup.com>
Co-authored-by: 严志勇 <yanzhiyong@tiansuixiansheng.com>
Co-authored-by: yanzhiyong <932374019@qq.com>
This commit is contained in:
Dreammy23
2024-08-28 12:39:13 +08:00
committed by GitHub
parent 9502251c08
commit 131bc7b89b
60 changed files with 2334 additions and 2243 deletions

4
.gitignore vendored
View File

@@ -158,6 +158,7 @@ thirdparty
#web
# dependencies
/web/node_modules
/web/yarn.lock
.idea
# next.js
@@ -185,6 +186,3 @@ thirdparty
/examples/**/*.gv.pdf
/i18n/locales/**/**/*_ai_translated.po
/i18n/locales/**/**/*~
/web_new/node_modules
/web_new/.next

View File

@@ -332,6 +332,17 @@ class Config(metaclass=Singleton):
os.getenv("MULTI_INSTANCE", "False").lower() == "true"
)
# file server configuration
# The host of the current file server, if None, get the host automatically
self.FILE_SERVER_HOST = os.getenv("FILE_SERVER_HOST")
self.FILE_SERVER_LOCAL_STORAGE_PATH = os.getenv(
"FILE_SERVER_LOCAL_STORAGE_PATH"
)
# multi-instance flag
self.WEBSERVER_MULTI_INSTANCE = (
os.getenv("MULTI_INSTANCE", "False").lower() == "true"
)
@property
def local_db_manager(self) -> "ConnectorManager":
from dbgpt.datasource.manages import ConnectorManager

View File

@@ -50,7 +50,7 @@ yarn install
### Usage
```sh
cp .env.example .env
cp .env.template .env
```
edit the `API_BASE_URL` to the real address

View File

@@ -1,5 +1,12 @@
import { AppListResponse, CreateAppParams, IAgent, IApp, NativeAppScenesResponse, StrategyResponse, TeamMode } from '@/types/app';
import { IFlowResponse } from '@/types/flow';
import {
AppListResponse,
CreateAppParams,
IAgent,
IApp,
NativeAppScenesResponse,
StrategyResponse,
TeamMode,
} from '@/types/app';
import { GET, POST } from '../index';
@@ -45,7 +52,9 @@ export const getAppStrategy = () => {
* 获取资源参数
*/
export const getResource = (data: Record<string, string>) => {
return GET<Record<string, string>, Record<string, any>[]>(`/api/v1/app/resources/list?type=${data.type}`);
return GET<Record<string, string>, Record<string, any>[]>(
`/api/v1/app/resources/list?type=${data.type}`
);
};
/**
* 创建native_app应用
@@ -61,13 +70,7 @@ export const getNativeAppScenes = () => {
export const getAppStrategyValues = (type: string) => {
return GET<string, string[]>(`/api/v1/llm-strategy/value/list?type=${type}`);
};
/**
* 创建awel_layout应用
* 获取工作流
*/
export const getFlows = ({ page, page_size }: { page: number; page_size: number }) => {
return GET<{ page: number; page_size: number }, IFlowResponse>(`/api/v1/serve/awel/flows?page=${page}&page_size=${page_size}`);
};
/**
* 查询应用权限
*/
@@ -77,6 +80,12 @@ export const getAppAdmins = (appCode: string) => {
/**
* 更新应用权限
*/
export const updateAppAdmins = (data: { app_code: string; admins: string[] }) => {
return POST<{ app_code: string; admins: string[] }, null>(`/api/v1/app/admins/update`, data);
export const updateAppAdmins = (data: {
app_code: string;
admins: string[];
}) => {
return POST<{ app_code: string; admins: string[] }, null>(
`/api/v1/app/admins/update`,
data
);
};

View File

@@ -1,9 +1,82 @@
import { IFlow, UpdateFLowAdminsParams } from '@/types/flow';
import { POST } from '../index';
import {
IFlow,
IFlowNode,
IFlowResponse,
IFlowUpdateParam,
IFlowRefreshParams,
IFlowExportParams,
IFlowImportParams,
IUploadFileRequestParams,
IUploadFileResponse,
} from '@/types/flow';
import { DELETE, GET, POST, PUT } from '../index';
/**
* 更新管理员
*/
export const updateFlowAdmins = (data: UpdateFLowAdminsParams) => {
return POST<UpdateFLowAdminsParams, IFlow>(`/api/v1/serve/awel/flow/admins`, data);
/** AWEL Flow */
export const addFlow = (data: IFlowUpdateParam) => {
return POST<IFlowUpdateParam, IFlow>('/api/v2/serve/awel/flows', data);
};
export const getFlows = (page?: number, page_size?: number) => {
return GET<any, IFlowResponse>('/api/v2/serve/awel/flows', {
page,
page_size,
});
};
export const getFlowById = (id: string) => {
return GET<null, IFlow>(`/api/v2/serve/awel/flows/${id}`);
};
export const updateFlowById = (id: string, data: IFlowUpdateParam) => {
return PUT<IFlowUpdateParam, IFlow>(`/api/v2/serve/awel/flows/${id}`, data);
};
export const deleteFlowById = (id: string) => {
return DELETE<null, null>(`/api/v2/serve/awel/flows/${id}`);
};
export const getFlowNodes = () => {
return GET<null, Array<IFlowNode>>(`/api/v2/serve/awel/nodes`);
};
export const refreshFlowNodeById = (data: IFlowRefreshParams) => {
return POST<IFlowRefreshParams, IFlowNode>(
'/api/v2/serve/awel/nodes/refresh',
data
);
};
export const debugFlow = (data: any) => {
return POST<any, IFlowNode>('/api/v2/serve/awel/flow/debug', data);
};
export const exportFlow = (data: IFlowExportParams) => {
return GET<IFlowExportParams, any>(
`/api/v2/serve/awel/flow/export/${data.uid}`,
data
);
};
export const importFlow = (data: IFlowImportParams) => {
return POST<IFlowImportParams, any>('/api/v2/serve/awel/flow/import', data);
};
export const uploadFile = (data: IUploadFileRequestParams) => {
return POST<IUploadFileRequestParams, Array<IUploadFileResponse>>(
'/api/v2/serve/file/files/dbgpt',
data
);
};
export const downloadFile = (fileId: string) => {
return GET<null, any>(`/api/v2/serve/file/files/dbgpt/${fileId}`);
};
// TODOwait for interface update
export const getFlowTemplateList = () => {
return GET<null, Array<any>>('/api/v2/serve/awel/flow/templates');
};
export const getFlowTemplateById = (id: string) => {
return GET<null, any>(`/api/v2/serve/awel/flow/templates/${id}`);
};

View File

@@ -5,7 +5,7 @@ import {
PostAgentPluginResponse,
PostAgentQueryParams,
} from '@/types/agent';
import { GetAppInfoParams, IApp, IAgent, IAppData } from '@/types/app';
import { GetAppInfoParams, IApp } from '@/types/app';
import {
ChatHistoryResponse,
DialogueListResponse,
@@ -17,7 +17,13 @@ import {
UserParam,
UserParamResponse,
} from '@/types/chat';
import { ChatFeedBackSchema, DbListResponse, DbSupportTypeResponse, PostDbParams, PostDbRefreshParams } from '@/types/db';
import {
ChatFeedBackSchema,
DbListResponse,
DbSupportTypeResponse,
PostDbParams,
PostDbRefreshParams,
} from '@/types/db';
import {
GetEditorSQLRoundRequest,
GetEditorySqlParams,
@@ -26,7 +32,6 @@ import {
PostEditorSQLRunParams,
PostSQLEditorSubmitParams,
} from '@/types/editor';
import { IFlow, IFlowNode, IFlowUpdateParam, IFlowResponse } from '@/types/flow';
import {
AddKnowledgeParams,
ArgumentsParams,
@@ -42,17 +47,24 @@ import {
ISyncBatchResponse,
SpaceConfig,
} from '@/types/knowledge';
import { BaseModelParams, IModelData, StartModelParams, SupportModel } from '@/types/model';
import {
BaseModelParams,
IModelData,
StartModelParams,
SupportModel,
} from '@/types/model';
import { AxiosRequestConfig } from 'axios';
import { DELETE, GET, POST, PUT } from '.';
import { GET, POST } from '.';
/** App */
export const postScenes = () => {
return POST<null, Array<SceneResponse>>('/api/v1/chat/dialogue/scenes');
};
export const newDialogue = (data: NewDialogueParam) => {
return POST<NewDialogueParam, IChatDialogueSchema>(`/api/v1/chat/dialogue/new?chat_mode=${data.chat_mode}&model_name=${data.model}`, data);
return POST<NewDialogueParam, IChatDialogueSchema>(
`/api/v1/chat/dialogue/new?chat_mode=${data.chat_mode}&model_name=${data.model}`,
data
);
};
export const addUser = (data: UserParam) => {
@@ -90,13 +102,19 @@ export const getUsableModels = () => {
return GET<null, Array<string>>('/api/v1/model/types');
};
export const postChatModeParamsList = (chatMode: string) => {
return POST<null, IDB[]>(`/api/v1/chat/mode/params/list?chat_mode=${chatMode}`);
return POST<null, IDB[]>(
`/api/v1/chat/mode/params/list?chat_mode=${chatMode}`
);
};
export const postChatModeParamsInfoList = (chatMode: string) => {
return POST<null, Record<string, string>>(`/api/v1/chat/mode/params/info?chat_mode=${chatMode}`);
return POST<null, Record<string, string>>(
`/api/v1/chat/mode/params/info?chat_mode=${chatMode}`
);
};
export const getChatHistory = (convId: string) => {
return GET<null, ChatHistoryResponse>(`/api/v1/chat/dialogue/messages/history?con_uid=${convId}`);
return GET<null, ChatHistoryResponse>(
`/api/v1/chat/dialogue/messages/history?con_uid=${convId}`
);
};
export const postChatModeParamsFileLoad = ({
convUid,
@@ -123,12 +141,14 @@ export const postChatModeParamsFileLoad = ({
'Content-Type': 'multipart/form-data',
},
...config,
},
}
);
};
export const clearChatHistory = (conUid: string) => {
return POST<null, Record<string, string>>(`/api/v1/chat/dialogue/clear?con_uid=${conUid}`);
return POST<null, Record<string, string>>(
`/api/v1/chat/dialogue/clear?con_uid=${conUid}`
);
};
/** Menu */
@@ -138,19 +158,27 @@ export const delDialogue = (conv_uid: string) => {
/** Editor */
export const getEditorSqlRounds = (id: string) => {
return GET<null, GetEditorSQLRoundRequest>(`/api/v1/editor/sql/rounds?con_uid=${id}`);
return GET<null, GetEditorSQLRoundRequest>(
`/api/v1/editor/sql/rounds?con_uid=${id}`
);
};
export const postEditorSqlRun = (data: PostEditorSQLRunParams) => {
return POST<PostEditorSQLRunParams>(`/api/v1/editor/sql/run`, data);
};
export const postEditorChartRun = (data: PostEditorChartRunParams) => {
return POST<PostEditorChartRunParams, PostEditorChartRunResponse>(`/api/v1/editor/chart/run`, data);
return POST<PostEditorChartRunParams, PostEditorChartRunResponse>(
`/api/v1/editor/chart/run`,
data
);
};
export const postSqlEditorSubmit = (data: PostSQLEditorSubmitParams) => {
return POST<PostSQLEditorSubmitParams>(`/api/v1/sql/editor/submit`, data);
};
export const getEditorSql = (id: string, round: string | number) => {
return POST<GetEditorySqlParams, string | Array<any>>('/api/v1/editor/sql', { con_uid: id, round });
return POST<GetEditorySqlParams, string | Array<any>>('/api/v1/editor/sql', {
con_uid: id,
round,
});
};
/** knowledge */
@@ -158,21 +186,36 @@ export const getArguments = (knowledgeName: string) => {
return POST<any, IArguments>(`/knowledge/${knowledgeName}/arguments`, {});
};
export const saveArguments = (knowledgeName: string, data: ArgumentsParams) => {
return POST<ArgumentsParams, IArguments>(`/knowledge/${knowledgeName}/argument/save`, data);
return POST<ArgumentsParams, IArguments>(
`/knowledge/${knowledgeName}/argument/save`,
data
);
};
export const getSpaceList = (data: any) => {
return POST<any, Array<ISpace>>('/knowledge/space/list', data);
};
export const getDocumentList = (spaceName: string, data: Record<string, number | Array<number>>) => {
return POST<Record<string, number | Array<number>>, IDocumentResponse>(`/knowledge/${spaceName}/document/list`, data);
export const getDocumentList = (
spaceName: string,
data: Record<string, number | Array<number>>
) => {
return POST<Record<string, number | Array<number>>, IDocumentResponse>(
`/knowledge/${spaceName}/document/list`,
data
);
};
export const getGraphVis = (spaceName: string, data: { limit: number }) => {
return POST<Record<string, number>, GraphVisResult>(`/knowledge/${spaceName}/graphvis`, data);
return POST<Record<string, number>, GraphVisResult>(
`/knowledge/${spaceName}/graphvis`,
data
);
};
export const addDocument = (knowledgeName: string, data: DocumentParams) => {
return POST<DocumentParams, number>(`/knowledge/${knowledgeName}/document/add`, data);
return POST<DocumentParams, number>(
`/knowledge/${knowledgeName}/document/add`,
data
);
};
export const addSpace = (data: AddKnowledgeParams) => {
@@ -180,27 +223,53 @@ export const addSpace = (data: AddKnowledgeParams) => {
};
export const getChunkStrategies = () => {
return GET<null, Array<IChunkStrategyResponse>>('/knowledge/document/chunkstrategies');
return GET<null, Array<IChunkStrategyResponse>>(
'/knowledge/document/chunkstrategies'
);
};
export const syncDocument = (spaceName: string, data: Record<string, Array<number>>) => {
return POST<Record<string, Array<number>>, string | null>(`/knowledge/${spaceName}/document/sync`, data);
export const syncDocument = (
spaceName: string,
data: Record<string, Array<number>>
) => {
return POST<Record<string, Array<number>>, string | null>(
`/knowledge/${spaceName}/document/sync`,
data
);
};
export const syncBatchDocument = (spaceName: string, data: Array<ISyncBatchParameter>) => {
return POST<Array<ISyncBatchParameter>, ISyncBatchResponse>(`/knowledge/${spaceName}/document/sync_batch`, data);
export const syncBatchDocument = (
spaceName: string,
data: Array<ISyncBatchParameter>
) => {
return POST<Array<ISyncBatchParameter>, ISyncBatchResponse>(
`/knowledge/${spaceName}/document/sync_batch`,
data
);
};
export const uploadDocument = (knowLedgeName: string, data: FormData) => {
return POST<FormData, number>(`/knowledge/${knowLedgeName}/document/upload`, data);
return POST<FormData, number>(
`/knowledge/${knowLedgeName}/document/upload`,
data
);
};
export const getChunkList = (spaceName: string, data: ChunkListParams) => {
return POST<ChunkListParams, IChunkList>(`/knowledge/${spaceName}/chunk/list`, data);
return POST<ChunkListParams, IChunkList>(
`/knowledge/${spaceName}/chunk/list`,
data
);
};
export const delDocument = (spaceName: string, data: Record<string, number>) => {
return POST<Record<string, number>, null>(`/knowledge/${spaceName}/document/delete`, data);
export const delDocument = (
spaceName: string,
data: Record<string, number>
) => {
return POST<Record<string, number>, null>(
`/knowledge/${spaceName}/document/delete`,
data
);
};
export const delSpace = (data: Record<string, string>) => {
@@ -226,21 +295,41 @@ export const getSupportModels = () => {
/** Agent */
export const postAgentQuery = (data: PostAgentQueryParams) => {
return POST<PostAgentQueryParams, PostAgentPluginResponse>('/api/v1/agent/query', data);
return POST<PostAgentQueryParams, PostAgentPluginResponse>(
'/api/v1/agent/query',
data
);
};
export const postAgentHubUpdate = (data?: PostAgentHubUpdateParams) => {
return POST<PostAgentHubUpdateParams>('/api/v1/agent/hub/update', data ?? { channel: '', url: '', branch: '', authorization: '' });
return POST<PostAgentHubUpdateParams>(
'/api/v1/agent/hub/update',
data ?? { channel: '', url: '', branch: '', authorization: '' }
);
};
export const postAgentMy = (user?: string) => {
return POST<undefined, PostAgentMyPluginResponse>('/api/v1/agent/my', undefined, { params: { user } });
return POST<undefined, PostAgentMyPluginResponse>(
'/api/v1/agent/my',
undefined,
{ params: { user } }
);
};
export const postAgentInstall = (pluginName: string, user?: string) => {
return POST('/api/v1/agent/install', undefined, { params: { plugin_name: pluginName, user }, timeout: 60000 });
return POST('/api/v1/agent/install', undefined, {
params: { plugin_name: pluginName, user },
timeout: 60000,
});
};
export const postAgentUninstall = (pluginName: string, user?: string) => {
return POST('/api/v1/agent/uninstall', undefined, { params: { plugin_name: pluginName, user }, timeout: 60000 });
return POST('/api/v1/agent/uninstall', undefined, {
params: { plugin_name: pluginName, user },
timeout: 60000,
});
};
export const postAgentUpload = (user = '', data: FormData, config?: Omit<AxiosRequestConfig, 'headers'>) => {
export const postAgentUpload = (
user = '',
data: FormData,
config?: Omit<AxiosRequestConfig, 'headers'>
) => {
return POST<FormData>('/api/v1/personal/agent/upload', data, {
params: { user },
headers: {
@@ -258,9 +347,18 @@ export const getChatFeedBackSelect = () => {
return GET<null, FeedBack>(`/api/v1/feedback/select`, undefined);
};
export const getChatFeedBackItme = (conv_uid: string, conv_index: number) => {
return GET<null, Record<string, string>>(`/api/v1/feedback/find?conv_uid=${conv_uid}&conv_index=${conv_index}`, undefined);
return GET<null, Record<string, string>>(
`/api/v1/feedback/find?conv_uid=${conv_uid}&conv_index=${conv_index}`,
undefined
);
};
export const postChatFeedBackForm = ({ data, config }: { data: ChatFeedBackSchema; config?: Omit<AxiosRequestConfig, 'headers'> }) => {
export const postChatFeedBackForm = ({
data,
config,
}: {
data: ChatFeedBackSchema;
config?: Omit<AxiosRequestConfig, 'headers'>;
}) => {
return POST<ChatFeedBackSchema, any>(`/api/v1/feedback/commit`, data, {
headers: {
'Content-Type': 'application/json',
@@ -271,27 +369,6 @@ export const postChatFeedBackForm = ({ data, config }: { data: ChatFeedBackSchem
/** prompt */
/** AWEL Flow */
export const addFlow = (data: IFlowUpdateParam) => {
return POST<IFlowUpdateParam, IFlow>('/api/v1/serve/awel/flows', data);
};
export const getFlowById = (id: string) => {
return GET<null, IFlow>(`/api/v1/serve/awel/flows/${id}`);
};
export const updateFlowById = (id: string, data: IFlowUpdateParam) => {
return PUT<IFlowUpdateParam, IFlow>(`/api/v1/serve/awel/flows/${id}`, data);
};
export const deleteFlowById = (id: string) => {
return DELETE<null, null>(`/api/v1/serve/awel/flows/${id}`);
};
export const getFlowNodes = () => {
return GET<null, Array<IFlowNode>>(`/api/v1/serve/awel/nodes`);
};
/** app */
export const collectApp = (data: Record<string, string>) => {
@@ -322,7 +399,9 @@ export const getAppInfo = (data: GetAppInfoParams) => {
};
export const getSupportDBList = (db_name = '') => {
return GET<null, Record<string, any>>(`/api/v1/permission/db/list?db_name=${db_name}`);
return GET<null, Record<string, any>>(
`/api/v1/permission/db/list?db_name=${db_name}`
);
};
export const recommendApps = (data: Record<string, string>) => {
@@ -336,7 +415,9 @@ export const modelSearch = (data: Record<string, string>) => {
};
export const getKnowledgeAdmins = (spaceId: string) => {
return GET<string, Record<string, any>>(`/knowledge/users/list?space_id=${spaceId}`);
return GET<string, Record<string, any>>(
`/knowledge/users/list?space_id=${spaceId}`
);
};
export const updateKnowledgeAdmins = (data: Record<string, string>) => {
return POST<Record<string, any>, any[]>(`/knowledge/users/update`, data);

View File

@@ -184,7 +184,7 @@ const Completion = ({ messages, onSubmit }: Props) => {
question={showMessages?.filter((e) => e?.role === 'human' && e?.order === content.order)[0]?.context}
knowledge_space={spaceNameOriginal || dbParam || ''}
/>
<Tooltip title={t('copy')}>
<Tooltip title={t('Copy_Btn')}>
<Button
onClick={() => onCopyContext(content?.context)}
slots={{ root: IconButton }}

View File

@@ -61,6 +61,7 @@ function GPTCard({
return icon;
}, [icon]);
// TODO: 算子资源标签
const tagNode = useMemo(() => {
if (!tags || !tags.length) return null;
return (

View File

@@ -3,7 +3,7 @@ import { Button, Form, Input, InputNumber, Modal, Select, Spin, Tooltip, message
import { useEffect, useMemo, useState } from 'react';
import { addOmcDB, apiInterceptors, getSupportDBList, postDbAdd, postDbEdit, postDbTestConnect } from '@/client/api';
import { DBOption, DBType, DbListResponse, PostDbParams } from '@/types/db';
import { isFileDb } from '@/pages/database';
import { isFileDb } from '@/pages/construct/database';
import { useTranslation } from 'react-i18next';
import { useDebounceFn } from 'ahooks';

View File

@@ -0,0 +1,234 @@
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, getFlowNodes } from '@/client/api';
import { CaretLeftOutlined, CaretRightOutlined } from '@ant-design/icons';
import type { CollapseProps } from 'antd';
import { Badge, Collapse, Input, Layout, Space } from 'antd';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import StaticNodes from './static-nodes';
import { IFlowNode } from '@/types/flow';
import { FLOW_NODES_KEY } from '@/utils';
const { Search } = Input;
const { Sider } = Layout;
type GroupType = {
category: string;
categoryLabel: string;
nodes: IFlowNode[];
};
const zeroWidthTriggerDefaultStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 16,
height: 48,
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
border: '1px solid #d6d8da',
borderRadius: 8,
right: -8,
};
const AddNodesSider: React.FC = () => {
const { t } = useTranslation();
const { mode } = useContext(ChatContext);
const [collapsed, setCollapsed] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState<string>('');
const [operators, setOperators] = useState<Array<IFlowNode>>([]);
const [resources, setResources] = useState<Array<IFlowNode>>([]);
const [operatorsGroup, setOperatorsGroup] = useState<GroupType[]>([]);
const [resourcesGroup, setResourcesGroup] = useState<GroupType[]>([]);
useEffect(() => {
getNodes();
}, []);
async function getNodes() {
const [_, data] = await apiInterceptors(getFlowNodes());
if (data && data.length > 0) {
localStorage.setItem(FLOW_NODES_KEY, JSON.stringify(data));
const operatorNodes = data.filter(
(node) => node.flow_type === 'operator'
);
const resourceNodes = data.filter(
(node) => node.flow_type === 'resource'
);
setOperators(operatorNodes);
setResources(resourceNodes);
setOperatorsGroup(groupNodes(operatorNodes));
setResourcesGroup(groupNodes(resourceNodes));
}
}
const triggerStyle: React.CSSProperties = useMemo(() => {
if (collapsed) {
return {
...zeroWidthTriggerDefaultStyle,
right: -16,
borderRadius: '0px 8px 8px 0',
borderLeft: '1px solid #d5e5f6',
};
}
return {
...zeroWidthTriggerDefaultStyle,
borderLeft: '1px solid #d6d8da',
};
}, [collapsed]);
function groupNodes(data: IFlowNode[]) {
const groups: GroupType[] = [];
const categoryMap: Record<
string,
{ category: string; categoryLabel: string; nodes: IFlowNode[] }
> = {};
data.forEach((item) => {
const { category, category_label } = item;
if (!categoryMap[category]) {
categoryMap[category] = {
category,
categoryLabel: category_label,
nodes: [],
};
groups.push(categoryMap[category]);
}
categoryMap[category].nodes.push(item);
});
return groups;
}
const operatorItems: CollapseProps['items'] = useMemo(() => {
if (!searchValue) {
return operatorsGroup.map(({ category, categoryLabel, nodes }) => ({
key: category,
label: categoryLabel,
children: <StaticNodes nodes={nodes} />,
extra: (
<Badge
showZero
count={nodes.length || 0}
style={{
backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
}}
/>
),
}));
} else {
const searchedNodes = operators.filter((node) =>
node.label.toLowerCase().includes(searchValue.toLowerCase())
);
return groupNodes(searchedNodes).map(
({ category, categoryLabel, nodes }) => ({
key: category,
label: categoryLabel,
children: <StaticNodes nodes={nodes} />,
extra: (
<Badge
showZero
count={nodes.length || 0}
style={{
backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
}}
/>
),
})
);
}
}, [operatorsGroup, searchValue]);
const resourceItems: CollapseProps['items'] = useMemo(() => {
if (!searchValue) {
return resourcesGroup.map(({ category, categoryLabel, nodes }) => ({
key: category,
label: categoryLabel,
children: <StaticNodes nodes={nodes} />,
extra: (
<Badge
showZero
count={nodes.length || 0}
style={{
backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
}}
/>
),
}));
} else {
const searchedNodes = resources.filter((node) =>
node.label.toLowerCase().includes(searchValue.toLowerCase())
);
return groupNodes(searchedNodes).map(
({ category, categoryLabel, nodes }) => ({
key: category,
label: categoryLabel,
children: <StaticNodes nodes={nodes} />,
extra: (
<Badge
showZero
count={nodes.length || 0}
style={{
backgroundColor: nodes.length > 0 ? '#52c41a' : '#7f9474',
}}
/>
),
})
);
}
}, [resourcesGroup, searchValue]);
function searchNode(val: string) {
setSearchValue(val);
}
return (
<Sider
className='flex justify-center items-start nodrag bg-[#ffffff80] border-r border-[#d5e5f6] dark:bg-[#ffffff29] dark:border-[#ffffff66]'
theme={mode}
width={280}
collapsible={true}
collapsed={collapsed}
collapsedWidth={0}
trigger={
collapsed ? (
<CaretRightOutlined className='text-base' />
) : (
<CaretLeftOutlined className='text-base' />
)
}
zeroWidthTriggerStyle={triggerStyle}
onCollapse={(collapsed) => setCollapsed(collapsed)}
>
<Space
direction='vertical'
className='w-[280px] pt-4 px-4 overflow-hidden overflow-y-auto scrollbar-default'
>
<p className='w-full text-base font-semibold text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] line-clamp-1'>
{t('add_node')}
</p>
<Search placeholder='Search node' onSearch={searchNode} allowClear />
<h2 className='font-semibold'>{t('operators')}</h2>
<Collapse
size='small'
bordered={false}
className='max-h-[calc((100vh-156px)/2)] overflow-hidden overflow-y-auto scrollbar-default'
defaultActiveKey={['']}
items={operatorItems}
/>
<h2 className='font-semibold'>{t('resource')}</h2>
<Collapse
size='small'
bordered={false}
className='max-h-[calc((100vh-156px)/2)] overflow-hidden overflow-y-auto scrollbar-default'
defaultActiveKey={['']}
items={resourceItems}
/>
</Space>
</Sider>
);
};
export default AddNodesSider;

View File

@@ -100,6 +100,7 @@ const AddNodes: React.FC = () => {
<div className="w-[320px] overflow-hidden overflow-y-auto scrollbar-default">
<p className="my-2 font-bold">{t('add_node')}</p>
<Search placeholder="Search node" onSearch={searchNode} />
<h2 className="my-2 ml-2 font-semibold">{t('operators')}</h2>
<Collapse
className="max-h-[300px] overflow-hidden overflow-y-auto scrollbar-default"
@@ -107,6 +108,7 @@ const AddNodes: React.FC = () => {
defaultActiveKey={['']}
items={operatorItems}
/>
<h2 className="my-2 ml-2 font-semibold">{t('resource')}</h2>
<Collapse
className="max-h-[300px] overflow-hidden overflow-y-auto scrollbar-default"
@@ -122,7 +124,7 @@ const AddNodes: React.FC = () => {
className="flex items-center justify-center rounded-full left-4 top-4"
style={{ zIndex: 1050 }}
icon={<PlusOutlined />}
></Button>
/>
</Popover>
);
};

View File

@@ -0,0 +1,104 @@
import { Modal, Form, Input, Button, Space, Radio, message } from 'antd';
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
import { apiInterceptors, exportFlow } from '@/client/api';
import { ReactFlowInstance } from 'reactflow';
import { useTranslation } from 'react-i18next';
type Props = {
reactFlow: ReactFlowInstance<any, any>;
flowInfo?: IFlowUpdateParam;
isExportFlowModalOpen: boolean;
setIsExportFlowModalOpen: (value: boolean) => void;
};
export const ExportFlowModal: React.FC<Props> = ({
reactFlow,
flowInfo,
isExportFlowModalOpen,
setIsExportFlowModalOpen,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
const onFlowExport = async (values: any) => {
if (values.format === 'json') {
const flowData = reactFlow.toObject() as IFlowData;
const blob = new Blob([JSON.stringify(flowData)], {
type: 'text/plain;charset=utf-8',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = values.file_name || 'flow.json';
a.click();
}else{
const linkUrl = `${process.env.API_BASE_URL}/api/v2/serve/awel/flow/export/${values.uid}?export_type=${values.export_type}&format=${values.format}`
window.open(linkUrl)
}
messageApi.success(t('Export_Flow_Success'));
setIsExportFlowModalOpen(false);
};
return (
<>
<Modal
centered
title={t('Export_Flow')}
open={isExportFlowModalOpen}
onCancel={() => setIsExportFlowModalOpen(false)}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
form={form}
className='mt-6'
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
onFinish={onFlowExport}
initialValues={{
export_type: 'json',
format: 'file',
uid: flowInfo?.uid,
}}
>
<Form.Item label={t('Export_File_Type')} name='export_type'>
<Radio.Group>
<Radio value='json'>JSON</Radio>
<Radio value='dbgpts'>DBGPTS</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label={t('Export_File_Format')} name='format'>
<Radio.Group>
<Radio value='file'>File</Radio>
<Radio value='json'>JSON</Radio>
</Radio.Group>
</Form.Item>
<Form.Item hidden name='uid'>
<Input />
</Form.Item>
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
<Space>
<Button
htmlType='button'
onClick={() => setIsExportFlowModalOpen(false)}
>
{t('cancel')}
</Button>
<Button type='primary' htmlType='submit'>
{t('verify')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{contextHolder}
</>
);
};

View File

@@ -0,0 +1,113 @@
import {
Modal,
Form,
Button,
message,
Upload,
UploadFile,
UploadProps,
GetProp,
Radio,
Space,
} from 'antd';
import { apiInterceptors, importFlow } from '@/client/api';
import { Node, Edge } from 'reactflow';
import { UploadOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
type Props = {
isImportModalOpen: boolean;
setNodes: React.Dispatch<
React.SetStateAction<Node<any, string | undefined>[]>
>;
setEdges: React.Dispatch<React.SetStateAction<Edge<any>[]>>;
setIsImportFlowModalOpen: (value: boolean) => void;
};
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
export const ImportFlowModal: React.FC<Props> = ({
setNodes,
setEdges,
isImportModalOpen,
setIsImportFlowModalOpen,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
const [fileList, setFileList] = useState<UploadFile[]>([]);
const onFlowImport = async (values: any) => {
values.file = values.file?.[0];
const formData: any = new FormData();
fileList.forEach((file) => {
formData.append('file', file as FileType);
});
const [, , res] = await apiInterceptors(importFlow(formData));
if (res?.success) {
messageApi.success(t('Export_Flow_Success'));
} else if (res?.err_msg) {
messageApi.error(res?.err_msg);
}
setIsImportFlowModalOpen(false);
};
return (
<>
<Modal
centered
title={t('Import_Flow')}
open={isImportModalOpen}
onCancel={() => setIsImportFlowModalOpen(false)}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
form={form}
className='mt-6'
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
onFinish={onFlowImport}
initialValues={{
save_flow: false,
}}
>
<Form.Item
name='file'
label={t('Select_File')}
valuePropName='fileList'
getValueFromEvent={(e) => (Array.isArray(e) ? e : e && e.fileList)}
rules={[{ required: true, message: 'Please upload a file' }]}
>
<Upload accept='.json,.zip' beforeUpload={() => false} maxCount={1}>
<Button icon={<UploadOutlined />}> {t('Upload')}</Button>
</Upload>
</Form.Item>
<Form.Item name='save_flow' label={t('Save_After_Import')}>
<Radio.Group>
<Radio value={true}>{t('Yes')}</Radio>
<Radio value={false}>{t('No')}</Radio>
</Radio.Group>
</Form.Item>
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
<Space>
<Button onClick={() => setIsImportFlowModalOpen(false)}>
{t('cancel')}
</Button>
<Button type='primary' htmlType='submit'>
{t('verify')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{contextHolder}
</>
);
};

View File

@@ -0,0 +1,3 @@
export * from './save-flow-modal';
export * from './export-flow-modal';
export * from './import-flow-modal';

View File

@@ -0,0 +1,203 @@
import { useState } from 'react';
import { Modal, Form, Input, Button, Space, message, Checkbox } from 'antd';
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
import { apiInterceptors, addFlow, updateFlowById } from '@/client/api';
import { mapHumpToUnderline } from '@/utils/flow';
import { useTranslation } from 'react-i18next';
import { ReactFlowInstance } from 'reactflow';
import { useSearchParams } from 'next/navigation';
const { TextArea } = Input;
type Props = {
reactFlow: ReactFlowInstance<any, any>;
flowInfo?: IFlowUpdateParam;
isSaveFlowModalOpen: boolean;
setIsSaveFlowModalOpen: (value: boolean) => void;
};
export const SaveFlowModal: React.FC<Props> = ({
reactFlow,
isSaveFlowModalOpen,
flowInfo,
setIsSaveFlowModalOpen,
}) => {
const [deploy, setDeploy] = useState(true);
const { t } = useTranslation();
const searchParams = useSearchParams();
const id = searchParams?.get('id') || '';
const [form] = Form.useForm<IFlowUpdateParam>();
const [messageApi, contextHolder] = message.useMessage();
function onLabelChange(e: React.ChangeEvent<HTMLInputElement>) {
const label = e.target.value;
// replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -.
let result = label
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_-]/g, '')
.toLowerCase();
result = result;
form.setFieldsValue({ name: result });
}
async function onSaveFlow() {
const {
name,
label,
description = '',
editable = false,
state = 'deployed',
} = form.getFieldsValue();
console.log(form.getFieldsValue());
const reactFlowObject = mapHumpToUnderline(
reactFlow.toObject() as IFlowData
);
if (id) {
const [, , res] = await apiInterceptors(
updateFlowById(id, {
name,
label,
description,
editable,
uid: id,
flow_data: reactFlowObject,
state,
})
);
if (res?.success) {
messageApi.success(t('save_flow_success'));
} else if (res?.err_msg) {
messageApi.error(res?.err_msg);
}
} else {
const [_, res] = await apiInterceptors(
addFlow({
name,
label,
description,
editable,
flow_data: reactFlowObject,
state,
})
);
if (res?.uid) {
messageApi.success(t('save_flow_success'));
const history = window.history;
history.pushState(null, '', `/flow/canvas?id=${res.uid}`);
}
}
setIsSaveFlowModalOpen(false);
}
return (
<>
<Modal
centered
title={t('flow_modal_title')}
open={isSaveFlowModalOpen}
onCancel={() => {
setIsSaveFlowModalOpen(false);
}}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
name='flow_form'
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 16 }}
className='mt-6 max-w-2xl'
initialValues={{ remember: true }}
onFinish={onSaveFlow}
autoComplete='off'
>
<Form.Item
label='Title'
name='label'
initialValue={flowInfo?.label}
rules={[{ required: true, message: 'Please input flow title!' }]}
>
<Input onChange={onLabelChange} />
</Form.Item>
<Form.Item
label='Name'
name='name'
initialValue={flowInfo?.name}
rules={[
{ required: true, message: 'Please input flow name!' },
() => ({
validator(_, value) {
const regex = /^[a-zA-Z0-9_\-]+$/;
if (!regex.test(value)) {
return Promise.reject(
'Can only contain numbers, letters, underscores, and dashes'
);
}
return Promise.resolve();
},
}),
]}
>
<Input />
</Form.Item>
<Form.Item
label='Description'
initialValue={flowInfo?.description}
name='description'
>
<TextArea rows={3} />
</Form.Item>
<Form.Item
label='Editable'
name='editable'
initialValue={flowInfo?.editable}
valuePropName='checked'
>
<Checkbox />
</Form.Item>
<Form.Item hidden name='state'>
<Input />
</Form.Item>
<Form.Item label='Deploy'>
<Checkbox
defaultChecked={
flowInfo?.state === 'deployed' || flowInfo?.state === 'running'
}
checked={deploy}
onChange={(e) => {
const val = e.target.checked;
form.setFieldValue('state', val ? 'deployed' : 'developing');
setDeploy(val);
}}
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 14, span: 8 }}>
<Space>
<Button
htmlType='button'
onClick={() => {
setIsSaveFlowModalOpen(false);
}}
>
{t('cancel')}
</Button>
<Button type='primary' htmlType='submit'>
{t('verify')}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{contextHolder}
</>
);
};

View File

@@ -4,28 +4,33 @@ import NodeParamHandler from './node-param-handler';
import classNames from 'classnames';
import { useState } from 'react';
import NodeHandler from './node-handler';
import { Popover, Tooltip } from 'antd';
import { CopyOutlined, DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { Form, Popover, Tooltip } from 'antd';
import {
CopyOutlined,
DeleteOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import { useReactFlow } from 'reactflow';
import IconWrapper from '../common/icon-wrapper';
import { getUniqueNodeId } from '@/utils/flow';
import { getUniqueNodeId, removeIndexFromNodeId } from '@/utils/flow';
import { cloneDeep } from 'lodash';
import { apiInterceptors, refreshFlowNodeById } from '@/client/api';
type CanvasNodeProps = {
data: IFlowNode;
};
const ICON_PATH_PREFIX = '/icons/node/';
function TypeLabel({ label }: { label: string }) {
return <div className="w-full h-8 bg-stone-100 dark:bg-zinc-700 px-2 flex items-center justify-center">{label}</div>;
return <div className='w-full h-8 align-middle font-semibold'>{label}</div>;
}
const forceTypeList = ['file', 'multiple_files', 'time','images','csv_file'];
const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
const node = data;
const { inputs, outputs, parameters, flow_type: flowType } = node;
const [isHovered, setIsHovered] = useState(false);
const reactFlow = useReactFlow();
const [form] = Form.useForm();
function onHover() {
setIsHovered(true);
@@ -68,81 +73,211 @@ const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
e.preventDefault();
e.stopPropagation();
reactFlow.setNodes((nodes) => nodes.filter((item) => item.id !== node.id));
reactFlow.setEdges((edges) => edges.filter((edge) => edge.source !== node.id && edge.target !== node.id));
reactFlow.setEdges((edges) =>
edges.filter((edge) => edge.source !== node.id && edge.target !== node.id)
);
}
function updateCurrentNodeValue(changedKey: string, changedVal: any) {
parameters.forEach((item) => {
if (item.name === changedKey) {
item.value = changedVal;
}
});
}
async function updateDependsNodeValue(changedKey: string, changedVal: any) {
const dependParamNodes = parameters.filter(({ ui }) =>
ui?.refresh_depends?.includes(changedKey)
);
if (dependParamNodes?.length === 0) return;
dependParamNodes.forEach(async (item) => {
const params = {
id: removeIndexFromNodeId(data?.id),
type_name: data.type_name,
type_cls: data.type_cls,
flow_type: 'operator' as const,
refresh: [
{
name: item.name,
depends: [
{
name: changedKey,
value: changedVal,
has_value: true,
},
],
},
],
};
const [_, res] = await apiInterceptors(refreshFlowNodeById(params));
// update value of the node
if (res) {
reactFlow.setNodes((nodes) =>
nodes.map((n) => {
return n.id === node.id
? {
...n,
data: {
...n.data,
parameters: res.parameters,
},
}
: n;
})
);
}
});
}
function onParameterValuesChange(changedValues: any, allValues: any) {
const [changedKey, changedVal] = Object.entries(changedValues)[0];
if (!allValues?.force && forceTypeList.includes(changedKey)) {
return;
}
updateCurrentNodeValue(changedKey, changedVal);
if (changedVal) {
updateDependsNodeValue(changedKey, changedVal);
}
}
function renderOutput(data: IFlowNode) {
if (flowType === 'operator' && outputs?.length > 0) {
return (
<>
<TypeLabel label="Outputs" />
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
<TypeLabel label='Outputs' />
{(outputs || []).map((output, index) => (
<NodeHandler key={`${data.id}_input_${index}`} node={data} data={output} type="source" label="outputs" index={index} />
<NodeHandler
key={`${data.id}_input_${index}`}
node={data}
data={output}
type='source'
label='outputs'
index={index}
/>
))}
</>
</div>
);
} else if (flowType === 'resource') {
// resource nodes show output default
return (
<>
<TypeLabel label="Outputs" />
<NodeHandler key={`${data.id}_input_0`} node={data} data={data} type="source" label="outputs" index={0} />
</>
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
<TypeLabel label='Outputs' />
<NodeHandler
key={`${data.id}_input_0`}
node={data}
data={data}
type='source'
label='outputs'
index={0}
/>
</div>
);
}
}
return (
<Popover
placement="rightTop"
placement='rightTop'
trigger={['hover']}
content={
<>
<IconWrapper className="hover:text-blue-500">
<CopyOutlined className="h-full text-lg cursor-pointer" onClick={copyNode} />
<IconWrapper className='hover:text-blue-500'>
<CopyOutlined
className='h-full text-lg cursor-pointer'
onClick={copyNode}
/>
</IconWrapper>
<IconWrapper className="mt-2 hover:text-red-500">
<DeleteOutlined className="h-full text-lg cursor-pointer" onClick={deleteNode} />
<IconWrapper className='mt-2 hover:text-red-500'>
<DeleteOutlined
className='h-full text-lg cursor-pointer'
onClick={deleteNode}
/>
</IconWrapper>
<IconWrapper className="mt-2">
<Tooltip title={<><p className="font-bold">{node.label}</p><p>{node.description}</p></>} placement="right">
<InfoCircleOutlined className="h-full text-lg cursor-pointer" />
<IconWrapper className='mt-2'>
<Tooltip
title={
<>
<p className='font-bold'>{node.label}</p>
<p>{node.description}</p>
</>
}
placement='right'
>
<InfoCircleOutlined className='h-full text-lg cursor-pointer' />
</Tooltip>
</IconWrapper>
</>
}
>
<div
className={classNames('w-72 h-auto rounded-xl shadow-md p-0 border bg-white dark:bg-zinc-800 cursor-grab', {
className={classNames(
'w-80 h-auto rounded-xl shadow-md px-2 py-4 border bg-white dark:bg-zinc-800 cursor-grab flex flex-col space-y-2 text-sm',
{
'border-blue-500': node.selected || isHovered,
'border-stone-400 dark:border-white': !node.selected && !isHovered,
'border-dashed': flowType !== 'operator',
'border-red-600': node.invalid,
})}
}
)}
onMouseEnter={onHover}
onMouseLeave={onLeave}
>
{/* icon and label */}
<div className="flex flex-row items-center p-2">
<Image src={'/icons/node/vis.png'} width={24} height={24} alt="" />
<p className="ml-2 text-lg font-bold text-ellipsis overflow-hidden whitespace-nowrap">{node.label}</p>
<div className='flex flex-row items-center'>
<Image src={'/icons/node/vis.png'} width={24} height={24} alt='' />
<p className='ml-2 text-lg font-bold text-ellipsis overflow-hidden whitespace-nowrap'>
{node.label}
</p>
</div>
{inputs && inputs.length > 0 && (
<>
<TypeLabel label="Inputs" />
{(inputs || []).map((input, index) => (
<NodeHandler key={`${node.id}_input_${index}`} node={node} data={input} type="target" label="inputs" index={index} />
{inputs?.length > 0 && (
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
<TypeLabel label='Inputs' />
<div className='flex flex-col space-y-2'>
{inputs?.map((item, index) => (
<NodeHandler
key={`${node.id}_input_${index}`}
node={node}
data={item}
type='target'
label='inputs'
index={index}
/>
))}
</>
</div>
</div>
)}
{parameters && parameters.length > 0 && (
<>
<TypeLabel label="Parameters" />
{(parameters || []).map((parameter, index) => (
<NodeParamHandler key={`${node.id}_param_${index}`} node={node} data={parameter} label="parameters" index={index} />
{parameters?.length > 0 && (
<div className='bg-zinc-100 dark:bg-zinc-700 rounded p-2'>
<TypeLabel label='Parameters' />
<Form
form={form}
layout='vertical'
onValuesChange={onParameterValuesChange}
className='flex flex-col space-y-3 text-neutral-500'
>
{parameters?.map((item, index) => (
<NodeParamHandler
key={`${node.id}_param_${index}`}
formValuesChange={onParameterValuesChange}
node={node}
paramData={item}
label='parameters'
index={index}
/>
))}
</>
</Form>
</div>
)}
{renderOutput(node)}
</div>
</Popover>

View File

@@ -94,15 +94,15 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
})}
>
<Handle
className="w-2 h-2"
className={classNames('w-2 h-2', type === 'source' ? '-mr-4' : '-ml-4')}
type={type}
position={type === 'source' ? Position.Right : Position.Left}
id={`${node.id}|${label}|${index}`}
isValidConnection={(connection) => isValidConnection(connection)}
/>
<Typography
className={classNames('p-2', {
'pr-4': label === 'outputs',
className={classNames('bg-white dark:bg-[#232734] w-full px-2 py-1 rounded text-neutral-500', {
'text-right': label === 'outputs',
})}
>
<Popconfirm
@@ -117,9 +117,10 @@ const NodeHandler: React.FC<NodeHandlerProps> = ({ node, data, type, label, inde
</div>
}
>
{['inputs', 'parameters'].includes(label) && <PlusOutlined className="mr-2 cursor-pointer" onClick={showRelatedNodes} />}
{['inputs', 'parameters'].includes(label) && <PlusOutlined className="cursor-pointer" onClick={showRelatedNodes} />}
</Popconfirm>
{data.type_name}:{label !== 'outputs' && <RequiredIcon optional={data.optional} />}
{label !== 'outputs' && <RequiredIcon optional={data.optional} />}
{data.type_name}
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />

View File

@@ -1,102 +1,153 @@
import { IFlowNode, IFlowNodeParameter } from '@/types/flow';
import { Checkbox, Input, InputNumber, Select, Tooltip } from 'antd';
import { Checkbox, Form, Input, InputNumber, Select } from 'antd';
import React from 'react';
import RequiredIcon from './required-icon';
import NodeHandler from './node-handler';
import { InfoCircleOutlined } from '@ant-design/icons';
import {
renderSelect,
renderCheckbox,
renderRadio,
renderCascader,
renderDatePicker,
renderInput,
renderSlider,
renderTreeSelect,
renderTimePicker,
renderTextArea,
renderUpload,
renderCodeEditor,
renderPassword,
renderVariables,
} from './node-renderer';
interface NodeParamHandlerProps {
formValuesChange:any;
node: IFlowNode;
data: IFlowNodeParameter;
paramData: IFlowNodeParameter;
label: 'inputs' | 'outputs' | 'parameters';
index: number; // index of array
}
// render node parameters item
const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ node, data, label, index }) => {
function handleChange(value: any) {
data.value = value;
}
const NodeParamHandler: React.FC<NodeParamHandlerProps> = ({ formValuesChange,node, paramData, label, index }) => {
// render node parameters based on AWEL1.0
function renderNodeWithoutUiParam(data: IFlowNodeParameter) {
let defaultValue = data.value ?? data.default;
if (data.category === 'resource') {
return <NodeHandler node={node} data={data} type="target" label={label} index={index} />;
} else if (data.category === 'common') {
let defaultValue = data.value !== null && data.value !== undefined ? data.value : data.default;
switch (data.type_name) {
case 'int':
case 'float':
return (
<div className="p-2 text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />
</Tooltip>
)}
</p>
<InputNumber
className="w-full"
defaultValue={defaultValue}
onChange={(value: number | null) => {
handleChange(value);
}}
/>
</div>
<Form.Item
className="mb-2 text-sm"
name={data.name}
initialValue={defaultValue}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
tooltip={data.description ? { title: data.description, icon: <InfoCircleOutlined /> } : ''}
>
<InputNumber className="w-full nodrag" />
</Form.Item>
);
case 'str':
return (
<div className="p-2 text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />
</Tooltip>
)}
</p>
<Form.Item
className="mb-2 text-sm"
name={data.name}
initialValue={defaultValue}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
tooltip={data.description ? { title: data.description, icon: <InfoCircleOutlined /> } : ''}
>
{data.options?.length > 0 ? (
<Select
className="w-full nodrag"
defaultValue={defaultValue}
options={data.options.map((item: any) => ({ label: item.label, value: item.value }))}
onChange={handleChange}
/>
<Select className="w-full nodrag" options={data.options.map((item: any) => ({ label: item.label, value: item.value }))} />
) : (
<Input
className="w-full"
defaultValue={defaultValue}
onChange={(e) => {
handleChange(e.target.value);
}}
/>
<Input className="w-full" />
)}
</div>
</Form.Item>
);
case 'bool':
defaultValue = defaultValue === 'False' ? false : defaultValue;
defaultValue = defaultValue === 'True' ? true : defaultValue;
return (
<div className="p-2 text-sm">
<p>
{data.label}:<RequiredIcon optional={data.optional} />
{data.description && (
<Tooltip title={data.description}>
<InfoCircleOutlined className="ml-2 cursor-pointer" />
</Tooltip>
)}
<Checkbox
className="ml-2"
defaultChecked={defaultValue}
onChange={(e) => {
handleChange(e.target.checked);
}}
/>
</p>
</div>
<Form.Item
className="mb-2 text-sm"
name={data.name}
initialValue={defaultValue}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
tooltip={data.description ? { title: data.description, icon: <InfoCircleOutlined /> } : ''}
>
<Checkbox className="ml-2" />
</Form.Item>
);
}
}
function renderComponentByType(type: string, data: IFlowNodeParameter,formValuesChange:any) {
switch (type) {
case 'select':
return renderSelect(data);
case 'cascader':
return renderCascader(data);
case 'checkbox':
return renderCheckbox(data);
case 'radio':
return renderRadio(data);
case 'input':
return renderInput(data);
case 'text_area':
return renderTextArea(data);
case 'slider':
return renderSlider(data);
case 'date_picker':
return renderDatePicker( data );
case 'time_picker':
return renderTimePicker({ data,formValuesChange });
case 'tree_select':
return renderTreeSelect(data);
case 'password':
return renderPassword(data);
case 'upload':
return renderUpload({ data,formValuesChange });
case 'variables':
return renderVariables(data);
case 'code_editor':
return renderCodeEditor(data);
default:
return null;
}
}
// render node parameters based on AWEL2.0
function renderNodeWithUiParam(data: IFlowNodeParameter,formValuesChange:any) {
const { refresh_depends, ui_type } = data.ui;
let defaultValue = data.value ?? data.default;
if (ui_type === 'slider' && data.is_list) {
defaultValue = [0,1]
}
return (
<Form.Item
className="mb-2"
initialValue={defaultValue}
name={data.name}
rules={[{ required: !data.optional }]}
label={<span className="text-neutral-500">{data.label}</span>}
{...(refresh_depends && { dependencies: refresh_depends })}
{...(data.description && { tooltip: { title: data.description, icon: <InfoCircleOutlined /> } })}
>
{renderComponentByType(ui_type, data,formValuesChange)}
</Form.Item>
);
}
if (paramData.category === 'resource') {
return <NodeHandler node={node} data={paramData} type="target" label={label} index={index} />;
} else if (paramData.category === 'common') {
return paramData?.ui ? renderNodeWithUiParam(paramData,formValuesChange) : renderNodeWithoutUiParam(paramData);
}
};
export default NodeParamHandler;

View File

@@ -0,0 +1,16 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Cascader } from 'antd';
export const renderCascader = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<Cascader
{...attr}
options={data.options}
placeholder="please select"
className="w-full nodrag"
/>
);
};

View File

@@ -0,0 +1,15 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Checkbox } from 'antd';
export const renderCheckbox = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
data.options?.length > 0 && (
<div className="bg-white p-2 rounded">
<Checkbox.Group {...attr} options={data.options} />
</div>
)
);
};

View File

@@ -0,0 +1,62 @@
import React, { useState, useMemo } from 'react';
import { Button, Form, Modal } from 'antd';
import Editor from '@monaco-editor/react';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { useTranslation } from 'react-i18next';
type Props = {
data: IFlowNodeParameter;
defaultValue?: any;
};
export const renderCodeEditor = (data: IFlowNodeParameter) => {
const { t } = useTranslation();
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const [isModalOpen, setIsModalOpen] = useState(false);
const showModal = () => {
setIsModalOpen(true);
};
const onOk = () => {
setIsModalOpen(false);
};
const onCancel = () => {
setIsModalOpen(false);
};
const modalWidth = useMemo(() => {
if (data?.ui?.editor?.width) {
return data?.ui?.editor?.width + 100;
}
return '80%';
}, [data?.ui?.editor?.width]);
return (
<div className="p-2 text-sm">
<Button type="default" onClick={showModal}>
{t('Open_Code_Editor')}
</Button>
<Modal title={t('Code_Editor')} width={modalWidth} open={isModalOpen} onOk={onOk} onCancel={onCancel}>
<Form.Item name={data?.name}>
<Editor
{...attr}
width={data?.ui?.editor?.width || '100%'}
height={data?.ui?.editor?.height || 200}
defaultLanguage={data?.ui?.language}
theme="vs-dark"
options={{
minimap: {
enabled: false,
},
wordWrap: 'on',
}}
/>
</Form.Item>
</Modal>
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { DatePicker } from 'antd';
export const renderDatePicker = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <DatePicker {...attr} className="w-full" placeholder="please select a date" />;
};

View File

@@ -0,0 +1,14 @@
export * from './select';
export * from './cascader';
export * from './date-picker';
export * from './input';
export * from './checkbox';
export * from './radio';
export * from './textarea';
export * from './slider';
export * from './time-picker';
export * from './tree-select';
export * from './code-editor';
export * from './upload';
export * from './password';
export * from './variables';

View File

@@ -0,0 +1,22 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Input } from 'antd';
import * as Icons from '@ant-design/icons';
const getIconComponent = (iconString: string) => {
const match = iconString.match(/^icon:(\w+)$/);
if (match) {
const iconName = match[1] as keyof typeof Icons;
const IconComponent = Icons[iconName];
// @ts-ignore
return IconComponent ? <IconComponent /> : null;
}
return null;
};
export const renderInput = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
attr.prefix = getIconComponent(data.ui?.attr?.prefix || '');
return <Input {...attr} className="w-full" placeholder="please input" allowClear />;
};

View File

@@ -0,0 +1,11 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Input } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
const { Password } = Input;
export const renderPassword = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <Password {...attr} placeholder="input password" />;
};

View File

@@ -0,0 +1,13 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Radio } from 'antd';
export const renderRadio = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<div className="bg-white p-2 rounded">
<Radio.Group {...attr} options={data.options} />
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Select } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
export const renderSelect = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data?.ui?.attr || {});
return <Select {...attr} className="w-full nodrag" placeholder="please select" options={data.options} />;
};

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Slider } from 'antd';
export const renderSlider = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<>
{data.is_list?
(<Slider range className="mt-8 nodrag" {...attr} />)
:(<Slider className="mt-8 nodrag" {...attr} />)}
</>
)
};

View File

@@ -0,0 +1,13 @@
import { IFlowNodeParameter } from '@/types/flow';
import { Input } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
import classNames from 'classnames';
const { TextArea } = Input;
export const renderTextArea = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<TextArea className="nowheel mb-3" {...attr} />
);
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { TimePicker } from 'antd';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import type { TimePickerProps } from 'antd';
type Props = {
formValuesChange:any,
data: IFlowNodeParameter;
};
export const renderTimePicker = (params: Props) => {
const { data ,formValuesChange} = params;
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const onChangeTime: TimePickerProps['onChange'] = (time, timeString) => {
formValuesChange({
time:timeString
},{force:true})
};
return <TimePicker {...attr} onChange={onChangeTime} className="w-full" placeholder="please select a moment" />;
};

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { TreeSelect } from 'antd';
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
export const renderTreeSelect = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return (
<TreeSelect
{...attr}
className="w-full nodrag"
treeDefaultExpandAll
treeData={data.options}
/>
);
};

View File

@@ -0,0 +1,90 @@
import React, { useState, useRef } from 'react';
import { UploadOutlined } from '@ant-design/icons';
import type { UploadProps } from 'antd';
import { Button, Upload, message,Form } from 'antd';
import { convertKeysToCamelCase } from '@/utils/flow';
import { IFlowNodeParameter } from '@/types/flow';
import { useTranslation } from 'react-i18next';
type Props = {
formValuesChange:any,
data: IFlowNodeParameter;
onChange?: (value: any) => void;
};
export const renderUpload = (params: Props) => {
const { t } = useTranslation();
const urlList = useRef<string[]>([]);
const { data ,formValuesChange} = params;
const form = Form.useFormInstance()
const attr = convertKeysToCamelCase(data.ui?.attr || {});
const [uploading, setUploading] = useState(false);
const [uploadType, setUploadType] = useState('');
const getUploadSuccessUrl = (url: string) => {
if (urlList.current.length === data.ui.attr.max_count) {
urlList.current.pop();
}
urlList.current.push(url);
if (data.ui.attr.max_count === 1) {
formValuesChange({file:urlList.current.toString()},{force:true})
}else{
formValuesChange({multiple_files:JSON.stringify(urlList.current)},{force:true})
}
};
const handleFileRemove = (file: any) => {
const index = urlList.current.indexOf(file.response.data[0].uri);
if (index !== -1) {
urlList.current.splice(index, 1);
}
if (data.ui.attr.max_count === 1) {
formValuesChange({file:urlList.current.toString()},{force:true})
}else{
formValuesChange({multiple_files:JSON.stringify(urlList.current)},{force:true})
}
};
const props: UploadProps = {
name: 'files',
action: process.env.API_BASE_URL + data.ui.action,
headers: {
authorization: 'authorization-text',
},
onChange(info) {
setUploading(true);
if (info.file.status !== 'uploading') {
}
if (info.file.status === 'done') {
setUploading(false);
message.success(`${info.file.response.data[0].file_name} ${t('Upload_Data_Successfully')}`);
getUploadSuccessUrl(info.file.response.data[0].uri);
} else if (info.file.status === 'error') {
setUploading(false);
message.error(`${info.file.response.data[0].file_name} ${t('Upload_Data_Failed')}`);
}
},
};
if (!uploadType && data.ui?.file_types && Array.isArray(data.ui?.file_types)) {
setUploadType(data.ui?.file_types.toString());
}
return (
<div className="p-2 text-sm text-center">
{data.is_list ? (
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={true} accept={uploadType}>
<Button loading={uploading} icon={<UploadOutlined />}>
{t('Upload_Data')}
</Button>
</Upload>
) : (
<Upload onRemove={handleFileRemove} {...props} {...attr} multiple={false} accept={uploadType}>
<Button loading={uploading} icon={<UploadOutlined />}>
{t('Upload_Data')}
</Button>
</Upload>
)}
</div>
);
};

View File

@@ -0,0 +1,9 @@
import { IFlowNodeParameter } from '@/types/flow';
import { convertKeysToCamelCase } from '@/utils/flow';
import { Input } from 'antd';
export const renderVariables = (data: IFlowNodeParameter) => {
const attr = convertKeysToCamelCase(data.ui?.attr || {});
return <Input {...attr} className="w-full" placeholder="please input" allowClear />;
};

View File

@@ -14,27 +14,28 @@ const StaticNodes: React.FC<{ nodes: IFlowNode[] }> = ({ nodes }) => {
if (nodes?.length > 0) {
return (
<List
className="overflow-hidden overflow-y-auto w-full"
itemLayout="horizontal"
className='overflow-hidden overflow-y-auto w-full'
size='small'
itemLayout='horizontal'
dataSource={nodes}
renderItem={(node) => (
<List.Item
className="cursor-move hover:bg-[#F1F5F9] dark:hover:bg-theme-dark p-0 py-2"
className='cursor-move hover:bg-[#F1F5F9] dark:hover:bg-theme-dark p-0 py-2'
draggable
onDragStart={(event) => onDragStart(event, node)}
>
<List.Item.Meta
className="flex items-center justify-center"
className='flex items-center justify-center'
avatar={<Avatar src={'/icons/node/vis.png'} size={'large'} />}
title={<p className="line-clamp-1 font-medium">{node.label}</p>}
description={<p className="line-clamp-2">{node.description}</p>}
title={<p className='line-clamp-1 font-medium'>{node.label}</p>}
description={<p className='line-clamp-2'>{node.description}</p>}
/>
</List.Item>
)}
/>
);
} else {
return <Empty className="px-2" description={t('no_node')} />;
return <Empty className='px-2' description={t('no_node')} />;
}
};

View File

@@ -10,7 +10,6 @@ export const ChatEn = {
thinking: 'Thinking',
stop_replying: 'Stop replying',
erase_memory: 'Erase Memory',
copy: 'Copy',
copy_nothing: 'Content copied is empty',
copy_success: 'Copy success',
copy_failed: 'Copy failed',

View File

@@ -15,17 +15,20 @@ export const CommonEn = {
Please_select_file: 'Please select one file',
Description: 'Description',
Storage: 'Storage',
Domain: 'Domain',
Please_input_the_description: 'Please input the description',
Please_select_the_storage: 'Please select the storage',
Please_select_the_domain_type: 'Please select the domain type',
Next: 'Next',
the_name_can_only_contain: 'the name can only contain numbers, letters, Chinese characters, "-" and "_"',
the_name_can_only_contain:
'the name can only contain numbers, letters, Chinese characters, "-" and "_"',
Text: 'Text',
'Fill your raw text': 'Fill your raw text',
URL: 'URL',
Fetch_the_content_of_a_URL: 'Fetch the content of a URL',
Document: 'Document',
Upload_a_document: 'Upload a document, document type can be PDF, CSV, Text, PowerPoint, Word, Markdown, Zip',
Upload_a_document:
'Upload a document, document type can be PDF, CSV, Text, PowerPoint, Word, Markdown, Zip',
Name: 'Name',
Text_Source: 'Text Source(Optional)',
Please_input_the_text_source: 'Please input the text source',
@@ -58,10 +61,12 @@ export const CommonEn = {
topk: 'topk',
the_top_k_vectors: 'the top k vectors based on similarity score',
recall_score: 'recall_score',
Set_a_threshold_score: 'Set a threshold score for the retrieval of similar vectors',
Set_a_threshold_score:
'Set a threshold score for the retrieval of similar vectors',
recall_type: 'recall_type',
model: 'model',
A_model_used: 'A model used to create vector representations of text or other data',
A_model_used:
'A model used to create vector representations of text or other data',
Automatic: 'Automatic',
Process: 'Process',
Automatic_desc: 'Automatically set segmentation and preprocessing rules.',
@@ -71,14 +76,16 @@ export const CommonEn = {
The_amount_of_overlap: 'The amount of overlap between adjacent data chunks',
Prompt: 'Prompt',
scene: 'scene',
A_contextual_parameter: 'A contextual parameter used to define the setting or environment in which the prompt is being used',
A_contextual_parameter:
'A contextual parameter used to define the setting or environment in which the prompt is being used',
template: 'template',
structure_or_format:
'A pre-defined structure or format for the prompt, which can help ensure that the AI system generates responses that are consistent with the desired style or tone.',
max_token: 'max_token',
max_iteration: 'max_iteration',
concurrency_limit: 'concurrency_limit',
The_maximum_number_of_tokens: 'The maximum number of tokens or words allowed in a prompt',
The_maximum_number_of_tokens:
'The maximum number of tokens or words allowed in a prompt',
Theme: 'Theme',
Port: 'Port',
Username: 'Username',
@@ -91,7 +98,8 @@ export const CommonEn = {
Show_Sidebar: 'UnFold',
language: 'Language',
choose_model: 'Please choose a model',
data_center_desc: 'DB-GPT also offers a user-friendly data center management interface for efficient data maintenance.',
data_center_desc:
'DB-GPT also offers a user-friendly data center management interface for efficient data maintenance.',
create_database: 'Create Database',
create_knowledge: 'Create Knowledge',
path: 'Path',
@@ -229,7 +237,8 @@ export const CommonEn = {
docs: 'Docs',
apps: 'All Apps',
please_enter_the_keywords: 'Please enter the keywords',
input_tip: 'Please select the model and enter the description to start quickly',
input_tip:
'Please select the model and enter the description to start quickly',
create_app: 'Create App',
copy_url: 'Click the Copy Share link',
double_click_open: 'Double click on Nail nail to open',
@@ -268,7 +277,7 @@ export const CommonEn = {
details: 'Details',
choose: 'Choose',
please_choose: 'Please choose',
want_delete: "Are you sure delete it?",
want_delete: 'Are you sure delete it?',
success: 'Success',
input_parameter: 'Input parameter',
output_structure: 'Output structure',
@@ -313,4 +322,6 @@ export const CommonEn = {
View_details: 'View details',
All: 'All',
Please_input_prompt_name: 'Please input prompt name',
Copy_Btn: 'Copy',
Delete_Btn: 'Delete',
} as const;

19
web/locales/en/flow.ts Normal file
View File

@@ -0,0 +1,19 @@
export const FlowEn = {
Upload_Data_Successfully: 'file uploaded successfully',
Upload_Data_Failed: 'file upload failed',
Upload_Data: 'Upload Data',
Code_Editor: 'Code Editor',
Open_Code_Editor: 'Open Code Editor',
Export_Flow_Success: 'Export flow success',
Import_Flow_Success: 'Import flow success',
Import: 'Import',
Export: 'Export',
Import_Flow: 'Import Flow',
Export_Flow: 'Export Flow',
Select_File: 'Select File',
Save_After_Import: 'Save after import',
Export_File_Type: 'File_Type',
Export_File_Format: 'File_Format',
Yes: 'Yes',
No: 'No',
};

View File

@@ -1,8 +1,10 @@
import { ChatEn } from './chat';
import { CommonEn } from './common';
import { ChatEn } from "./chat";
import { CommonEn } from "./common";
import { FlowEn } from "./flow";
const en = {
...ChatEn,
...FlowEn,
...CommonEn,
};

View File

@@ -10,7 +10,6 @@ export const ChatZh: Resources['translation'] = {
dialog_list: '对话列表',
delete_chat: '删除会话',
delete_chat_confirm: '您确认要删除会话吗?',
input_tips: '可以问我任何问题shift + Enter 换行',
sent: '发送',
answer_again: '重新回答',
@@ -18,7 +17,6 @@ export const ChatZh: Resources['translation'] = {
thinking: '正在思考中',
stop_replying: '停止回复',
erase_memory: '清除记忆',
copy: '复制',
copy_success: '复制成功',
copy_failed: '复制失败',
copy_nothing: '内容复制为空',

View File

@@ -1,4 +1,4 @@
import { CommonEn } from '../en/common';
import { CommonEn } from "../en/common";
type I18nKeys = keyof typeof CommonEn;
@@ -6,318 +6,324 @@ interface Resources {
translation: Record<I18nKeys, string>;
}
export const CommonZh: Resources['translation'] = {
Knowledge_Space: '知识库',
space: '知识库',
Vector: '向量',
Owner: '创建人',
Count: '文档数',
File_type_Invalid: '文件类型错误',
Knowledge_Space_Config: '知识库配置',
Choose_a_Datasource_type: '知识库类型',
Segmentation: '分片',
No_parameter: '不需要配置分片参数',
Knowledge_Space_Name: '知识库名称',
Please_input_the_name: '请输入名称',
Please_input_the_owner: '请输入创建人',
Please_select_file: '请至少选择一个文件',
Description: '描述',
Storage: '存储类型',
Please_input_the_description: '请输入描述',
Please_select_the_storage:'请选择存储类型',
Please_select_the_domain_type: '请选择领域类型',
Next: '下一步',
the_name_can_only_contain: '名称只能包含数字、字母、中文字符、-或_',
Text: '文本',
'Fill your raw text': '填写您的原始文本',
URL: '网址',
Fetch_the_content_of_a_URL: '获取 URL 的内容',
Document: '文档',
Upload_a_document: '上传文档文档类型可以是PDF、CSV、Text、PowerPoint、Word、Markdown、Zip',
Name: '名称',
Text_Source: '文本来源(可选)',
Please_input_the_text_source: '请输入文本来源',
Sync: '同步',
Back: '上一步',
Finish: '完成',
Web_Page_URL: '网页网址',
Please_input_the_Web_Page_URL: '请输入网页网址',
Select_or_Drop_file: '选择或拖拽文件',
Documents: '文档',
Chat: '对话',
Add_Datasource: '添加数据源',
View_Graph: '查看图谱',
Arguments: '参数',
Type: '类型',
Size: '切片',
Last_Sync: '上次同步时间',
Status: '状态',
Result: '结果',
Details: '明细',
Delete: '删除',
Operation: '操作',
Submit: '提交',
close: '关闭',
Chunks: '切片',
Content: '内容',
Meta_Data: '元数据',
Please_select_a_file: '请上传一个文件',
Please_input_the_text: '请输入文本',
Embedding: '嵌入',
topk: 'TopK',
the_top_k_vectors: '基于相似度得分的前 k 个向量',
recall_score: '召回分数',
Set_a_threshold_score: '设置相似向量检索的阈值分数',
recall_type: '召回类型',
model: '模型',
A_model_used: '用于创建文本或其他数据的矢量表示的模型',
Automatic: '自动切片',
Process: '切片处理',
Automatic_desc: '自动设置分割和预处理规则。',
chunk_size: '块大小',
The_size_of_the_data_chunks: '处理中使用的数据块的大小',
chunk_overlap: '块重叠',
The_amount_of_overlap: '相邻数据块之间的重叠量',
scene: '场景',
A_contextual_parameter: '用于定义使用提示的设置或环境的上下文参数',
template: '模板',
structure_or_format: '预定义的提示结构或格式,有助于确保人工智能系统生成与所需风格或语气一致的响应。',
max_token: '最大令牌',
max_iteration: '最大迭代',
concurrency_limit: '并发限制',
The_maximum_number_of_tokens: '提示中允许的最大标记或单词数',
Theme: '主题',
Port: '端口',
Username: '用户名',
Password: '密码',
Remark: '备注',
Edit: '编辑',
Database: '数据库',
Data_Source: '数据中心',
Close_Sidebar: '收起',
Show_Sidebar: '展开',
language: '语言',
choose_model: '请选择一个模型',
data_center_desc: 'DB-GPT支持数据库交互和基于文档的对话它还提供了一个用户友好的数据中心管理界面。',
create_database: '创建数据库',
create_knowledge: '创建知识库',
create_flow: '创建工作流',
path: '路径',
model_manage: '模型管理',
stop_model_success: '模型停止成功',
create_model: '创建模型',
model_select_tips: '请选择一个模型',
submit: '提交',
start_model_success: '启动模型成功',
download_model_tip: '请先下载模型!',
Plugins: '插件列表',
try_again: '刷新重试',
no_data: '暂无数据',
Prompt: '提示词',
Open_Sidebar: '展开',
verify: '确认',
cancel: '取消',
Edit_Success: '编辑成功',
Add: '新增',
Add_Success: '新增成功',
Error_Message: '出错了',
Please_Input: '请输入',
Prompt_Info_Scene: '场景',
Prompt_Info_Sub_Scene: '次级场景',
Prompt_Info_Name: '名称',
Prompt_Info_Content: '内容',
Public: '公共',
Private: '私有',
Lowest: '渣渣',
Missed: '没理解',
Lost: '答不了',
Incorrect: '答错了',
Verbose: '较啰嗦',
Best: '真棒',
Rating: '评分',
Q_A_Category: '问答类别',
Q_A_Rating: '问答评分',
export const CommonZh: Resources["translation"] = {
Knowledge_Space: "知识库",
space: "知识库",
Vector: "向量",
Owner: "创建人",
Count: "文档数",
File_type_Invalid: "文件类型错误",
Knowledge_Space_Config: "知识库配置",
Choose_a_Datasource_type: "知识库类型",
Segmentation: "分片",
No_parameter: "不需要配置分片参数",
Knowledge_Space_Name: "知识库名称",
Please_input_the_name: "请输入名称",
Please_input_the_owner: "请输入创建人",
Please_select_file: "请至少选择一个文件",
Description: "描述",
Storage: "存储类型",
Domain: "领域类型",
Please_input_the_description: "请输入描述",
Please_select_the_storage: "请选择存储类型",
Please_select_the_domain_type: "请选择领域类型",
Next: "下一步",
the_name_can_only_contain: "名称只能包含数字、字母、中文字符、-或_",
Text: "文本",
"Fill your raw text": "填写您的原始文本",
URL: "网址",
Fetch_the_content_of_a_URL: "获取 URL 的内容",
Document: "文档",
Upload_a_document:
"上传文档文档类型可以是PDF、CSV、Text、PowerPoint、Word、Markdown、Zip",
Name: "名称",
Text_Source: "文本来源(可选)",
Please_input_the_text_source: "请输入文本来源",
Sync: "同步",
Back: "上一步",
Finish: "完成",
Web_Page_URL: "网页网址",
Please_input_the_Web_Page_URL: "请输入网页网址",
Select_or_Drop_file: "选择或拖拽文件",
Documents: "文档",
Chat: "对话",
Add_Datasource: "添加数据源",
View_Graph: "查看图谱",
Arguments: "参数",
Type: "类型",
Size: "切片",
Last_Sync: "上次同步时间",
Status: "状态",
Result: "结果",
Details: "明细",
Delete: "删除",
Operation: "操作",
Submit: "提交",
close: "关闭",
Chunks: "切片",
Content: "内容",
Meta_Data: "元数据",
Please_select_a_file: "请上传一个文件",
Please_input_the_text: "请输入文本",
Embedding: "嵌入",
topk: "TopK",
the_top_k_vectors: "基于相似度得分的前 k 个向量",
recall_score: "召回分数",
Set_a_threshold_score: "设置相似向量检索的阈值分数",
recall_type: "召回类型",
model: "模型",
A_model_used: "用于创建文本或其他数据的矢量表示的模型",
Automatic: "自动切片",
Process: "切片处理",
Automatic_desc: "自动设置分割和预处理规则。",
chunk_size: "块大小",
The_size_of_the_data_chunks: "处理中使用的数据块的大小",
chunk_overlap: "块重叠",
The_amount_of_overlap: "相邻数据块之间的重叠量",
scene: "场景",
A_contextual_parameter: "用于定义使用提示的设置或环境的上下文参数",
template: "模板",
structure_or_format:
"预定义的提示结构或格式,有助于确保人工智能系统生成与所需风格或语气一致的响应。",
max_token: "最大令牌",
max_iteration: "最大迭代",
concurrency_limit: "并发限制",
The_maximum_number_of_tokens: "提示中允许的最大标记或单词数",
Theme: "主题",
Port: "端口",
Username: "用户名",
Password: "密码",
Remark: "备注",
Edit: "编辑",
Database: "数据库",
Data_Source: "数据中心",
Close_Sidebar: "收起",
Show_Sidebar: "展开",
language: "语言",
choose_model: "请选择一个模型",
data_center_desc:
"DB-GPT支持数据库交互和基于文档的对话它还提供了一个用户友好的数据中心管理界面。",
create_database: "创建数据库",
create_knowledge: "创建知识库",
create_flow: "创建工作流",
path: "路径",
model_manage: "模型管理",
stop_model_success: "模型停止成功",
create_model: "创建模型",
model_select_tips: "请选择一个模型",
submit: "提交",
start_model_success: "启动模型成功",
download_model_tip: "请先下载模型!",
Plugins: "插件列表",
try_again: "刷新重试",
no_data: "暂无数据",
Prompt: "提示词",
Open_Sidebar: "展开",
verify: "确认",
cancel: "取消",
Edit_Success: "编辑成功",
Add: "新增",
Add_Success: "新增成功",
Error_Message: "出错了",
Please_Input: "请输入",
Prompt_Info_Scene: "场景",
Prompt_Info_Sub_Scene: "次级场景",
Prompt_Info_Name: "名称",
Prompt_Info_Content: "内容",
Public: "公共",
Private: "私有",
Lowest: "渣渣",
Missed: "没理解",
Lost: "答不了",
Incorrect: "答错了",
Verbose: "较啰嗦",
Best: "真棒",
Rating: "评分",
Q_A_Category: "问答类别",
Q_A_Rating: "问答评分",
feed_back_desc:
'0: 无结果\n' +
'1: 有结果,但是在文不对题,没有理解问题\n' +
'2: 有结果,理解了问题,但是提示回答不了这个问题\n' +
'3: 有结果,理解了问题,并做出回答,但是回答的结果错误\n' +
'4: 有结果,理解了问题,回答结果正确,但是比较啰嗦,缺乏总结\n' +
'5: 有结果,理解了问题,回答结果正确,推理正确,并给出了总结,言简意赅\n',
input_count: '共计输入',
input_unit: '字',
Click_Select: '点击选择',
Quick_Start: '快速开始',
Select_Plugins: '选择插件',
Search: '搜索',
Reset: '重置',
Update_From_Github: '更新Github插件',
Upload: '上传',
Market_Plugins: '插件市场',
My_Plugins: '我的插件',
Del_Knowledge_Tips: '你确定删除该知识库吗',
Del_Document_Tips: '你确定删除该文档吗',
Tips: '提示',
Limit_Upload_File_Count_Tips: '一次只能上传一个文件',
To_Plugin_Market: '前往插件市场',
Summary: '总结',
stacked_column_chart: '堆叠柱状图',
column_chart: '柱状图',
percent_stacked_column_chart: '百分比堆叠柱状图',
grouped_column_chart: '簇形柱状图',
time_column: '簇形柱状图',
pie_chart: '饼图',
line_chart: '折线图',
area_chart: '面积图',
stacked_area_chart: '堆叠面积图',
scatter_plot: '散点图',
bubble_chart: '气泡图',
stacked_bar_chart: '堆叠条形图',
bar_chart: '条形图',
percent_stacked_bar_chart: '百分比堆叠条形图',
grouped_bar_chart: '簇形条形图',
water_fall_chart: '瀑布图',
table: '表格',
multi_line_chart: '多折线图',
multi_measure_column_chart: '多指标柱形图',
multi_measure_line_chart: '多指标折线图',
Advices: '自动推荐',
Retry: '重试',
Load_more: '加载更多',
new_chat: '创建会话',
choice_agent_tip: '请选择代理',
no_context_tip: '请输入你的问题',
Terminal: '终端',
used_apps: '最近使用',
app_in_mind: '没有心仪的应用?去',
explore: '探索广场',
Discover_more: '发现更多',
sdk_insert: 'SDK接入',
my_apps: '我的应用',
awel_flow: 'AWEL 工作流',
save: '保存',
add_node: '添加节点',
no_node: '没有可编排节点',
connect_warning: '节点无法连接',
flow_modal_title: '保存工作流',
flow_name: '工作流名称',
flow_description: '工作流描述',
flow_name_required: '请输入工作流名称',
flow_description_required: '请输入工作流描述',
save_flow_success: '保存工作流成功',
delete_flow_confirm: '确定删除该工作流吗?',
related_nodes: '关联节点',
language_select_tips: '请选择语言',
add_resource: '添加资源',
team_modal: '工作模式',
App: '应用程序',
resource: '资源',
resource_name: '资源名',
resource_type: '资源类型',
resource_value: '参数',
resource_dynamic: '动态',
Please_input_the_work_modal: '请选择工作模式',
available_resources: '可用资源',
edit_new_applications: '编辑新的应用',
collect: '收藏',
collected: '已收藏',
create: '创建',
Agents: '智能体',
edit_application: '编辑应用',
add_application: '添加应用',
app_name: '应用名称',
input_app_name: '请输入应用名称',
LLM_strategy: '模型策略',
please_select_LLM_strategy: '请选择模型策略',
LLM_strategy_value: '模型策略参数',
please_select_LLM_strategy_value: '请选择模型策略参数',
operators: '算子',
Chinese: '中文',
English: '英文',
docs: '文档',
apps: '全部',
please_enter_the_keywords: '请输入关键词',
input_tip: '请选择模型,输入描述快速开始',
create_app: '创建应用',
copy_url: '单击复制分享链接',
double_click_open: '双击钉钉打开',
construct: '应用管理',
chat_online: '在线对话',
recommend_apps: '热门推荐',
all_apps: '全部应用',
latest_apps: '最新应用',
my_collected_apps: '我的收藏',
collect_success: '收藏成功',
cancel_success: '取消成功',
published: '已发布',
unpublished: '未发布',
start_chat: '开始对话',
native_app: '原生应用',
native_type: '应用类型',
temperature: '温度',
update: '更新',
refreshSuccess: '刷新成功',
Download: '下载',
app_type_select: '请选择应用类型',
please_select_param: '请选择参数',
please_select_model: '请选择模型',
please_input_temperature: '请输入temperature值',
select_workflow: '选择工作流',
please_select_workflow: '请选择工作流',
recommended_questions: '推荐问题',
question: '问题',
please_input_recommended_questions: '请输入推荐问题',
is_effective: '是否生效',
add_question: '添加问题',
update_success: '更新成功',
update_failed: '更新失败',
please_select_prompt: '请选择一个提示词',
details: '详情',
choose: '选择',
please_choose: '请先选择',
"0: 无结果\n" +
"1: 有结果,但是在文不对题,没有理解问题\n" +
"2: 有结果,理解了问题,但是提示回答不了这个问题\n" +
"3: 有结果,理解了问题,并做出回答,但是回答的结果错误\n" +
"4: 有结果,理解了问题,回答结果正确,但是比较啰嗦,缺乏总结\n" +
"5: 有结果,理解了问题,回答结果正确,推理正确,并给出了总结,言简意赅\n",
input_count: "共计输入",
input_unit: "字",
Click_Select: "点击选择",
Quick_Start: "快速开始",
Select_Plugins: "选择插件",
Search: "搜索",
Reset: "重置",
Update_From_Github: "更新Github插件",
Upload: "上传",
Market_Plugins: "插件市场",
My_Plugins: "我的插件",
Del_Knowledge_Tips: "你确定删除该知识库吗",
Del_Document_Tips: "你确定删除该文档吗",
Tips: "提示",
Limit_Upload_File_Count_Tips: "一次只能上传一个文件",
To_Plugin_Market: "前往插件市场",
Summary: "总结",
stacked_column_chart: "堆叠柱状图",
column_chart: "柱状图",
percent_stacked_column_chart: "百分比堆叠柱状图",
grouped_column_chart: "簇形柱状图",
time_column: "簇形柱状图",
pie_chart: "饼图",
line_chart: "折线图",
area_chart: "面积图",
stacked_area_chart: "堆叠面积图",
scatter_plot: "散点图",
bubble_chart: "气泡图",
stacked_bar_chart: "堆叠条形图",
bar_chart: "条形图",
percent_stacked_bar_chart: "百分比堆叠条形图",
grouped_bar_chart: "簇形条形图",
water_fall_chart: "瀑布图",
table: "表格",
multi_line_chart: "多折线图",
multi_measure_column_chart: "多指标柱形图",
multi_measure_line_chart: "多指标折线图",
Advices: "自动推荐",
Retry: "重试",
Load_more: "加载更多",
new_chat: "创建会话",
choice_agent_tip: "请选择代理",
no_context_tip: "请输入你的问题",
Terminal: "终端",
used_apps: "最近使用",
app_in_mind: "没有心仪的应用?去",
explore: "探索广场",
Discover_more: "发现更多",
sdk_insert: "SDK接入",
my_apps: "我的应用",
awel_flow: "AWEL 工作流",
save: "保存",
add_node: "添加节点",
no_node: "没有可编排节点",
connect_warning: "节点无法连接",
flow_modal_title: "保存工作流",
flow_name: "工作流名称",
flow_description: "工作流描述",
flow_name_required: "请输入工作流名称",
flow_description_required: "请输入工作流描述",
save_flow_success: "保存工作流成功",
delete_flow_confirm: "确定删除该工作流吗?",
related_nodes: "关联节点",
language_select_tips: "请选择语言",
add_resource: "添加资源",
team_modal: "工作模式",
App: "应用程序",
resource: "资源",
resource_name: "资源名",
resource_type: "资源类型",
resource_value: "参数",
resource_dynamic: "动态",
Please_input_the_work_modal: "请选择工作模式",
available_resources: "可用资源",
edit_new_applications: "编辑新的应用",
collect: "收藏",
collected: "已收藏",
create: "创建",
Agents: "智能体",
edit_application: "编辑应用",
add_application: "添加应用",
app_name: "应用名称",
input_app_name: "请输入应用名称",
LLM_strategy: "模型策略",
please_select_LLM_strategy: "请选择模型策略",
LLM_strategy_value: "模型策略参数",
please_select_LLM_strategy_value: "请选择模型策略参数",
operators: "算子",
Chinese: "中文",
English: "英文",
docs: "文档",
apps: "全部",
please_enter_the_keywords: "请输入关键词",
input_tip: "请选择模型,输入描述快速开始",
create_app: "创建应用",
copy_url: "单击复制分享链接",
double_click_open: "双击钉钉打开",
construct: "应用管理",
chat_online: "在线对话",
recommend_apps: "热门推荐",
all_apps: "全部应用",
latest_apps: "最新应用",
my_collected_apps: "我的收藏",
collect_success: "收藏成功",
cancel_success: "取消成功",
published: "已发布",
unpublished: "未发布",
start_chat: "开始对话",
native_app: "原生应用",
native_type: "应用类型",
temperature: "温度",
update: "更新",
refreshSuccess: "刷新成功",
Download: "下载",
app_type_select: "请选择应用类型",
please_select_param: "请选择参数",
please_select_model: "请选择模型",
please_input_temperature: "请输入temperature值",
select_workflow: "选择工作流",
please_select_workflow: "请选择工作流",
recommended_questions: "推荐问题",
question: "问题",
please_input_recommended_questions: "请输入推荐问题",
is_effective: "是否生效",
add_question: "添加问题",
update_success: "更新成功",
update_failed: "更新失败",
please_select_prompt: "请选择一个提示词",
details: "详情",
choose: "选择",
please_choose: "请先选择",
want_delete: "你确定要删除吗?",
success: '成功',
input_parameter: '输入参数',
output_structure: '输出结构',
User_input: '用户输入',
LLM_test: 'LLM测试',
Output_verification: '输出验证',
select_scene: '请选择场景',
select_type: '请选择类型',
Please_complete_the_input_parameters: '请填写完整的输入参数',
Please_fill_in_the_user_input: '请填写用户输入内容',
help: '我可以帮您:',
Refresh_status: '刷新状态',
Recall_test: '召回测试',
synchronization: '一键同步',
Synchronization_initiated: '同步已发起,请稍后',
Edit_document: '编辑文档',
Document_name: '文档名',
Correlation_problem: '关联问题',
Add_problem: '添加问题',
New_knowledge_base: '新增知识库',
yuque: '语雀文档',
Get_yuque_document: '获取语雀文档的内容',
document_url: '文档地址',
input_document_url: '请输入文档地址',
Get_token: '请先获取团队知识库tokentoken获取',
Reference_link: '参考链接',
document_token: '文档token',
input_document_token: '请输入文档token',
input_question: '请输入问题',
detail: '详情',
Manual_entry: '手动录入',
Data_content: '数据内容',
Main_content: '主要内容',
Auxiliary_data: '辅助数据',
enter_question_first: '请先输入问题',
unpublish: '取消发布',
publish: '发布应用',
Update_successfully: '更新成功',
Create_successfully: '创建成功',
Update_failure: '更新失败',
Create_failure: '创建失败',
View_details: '查看详情',
All: '全部',
Please_input_prompt_name: '请输入prompt名称',
success: "成功",
input_parameter: "输入参数",
output_structure: "输出结构",
User_input: "用户输入",
LLM_test: "LLM测试",
Output_verification: "输出验证",
select_scene: "请选择场景",
select_type: "请选择类型",
Please_complete_the_input_parameters: "请填写完整的输入参数",
Please_fill_in_the_user_input: "请填写用户输入内容",
help: "我可以帮您:",
Refresh_status: "刷新状态",
Recall_test: "召回测试",
synchronization: "一键同步",
Synchronization_initiated: "同步已发起,请稍后",
Edit_document: "编辑文档",
Document_name: "文档名",
Correlation_problem: "关联问题",
Add_problem: "添加问题",
New_knowledge_base: "新增知识库",
yuque: "语雀文档",
Get_yuque_document: "获取语雀文档的内容",
document_url: "文档地址",
input_document_url: "请输入文档地址",
Get_token: "请先获取团队知识库tokentoken获取",
Reference_link: "参考链接",
document_token: "文档token",
input_document_token: "请输入文档token",
input_question: "请输入问题",
detail: "详情",
Manual_entry: "手动录入",
Data_content: "数据内容",
Main_content: "主要内容",
Auxiliary_data: "辅助数据",
enter_question_first: "请先输入问题",
unpublish: "取消发布",
publish: "发布应用",
Update_successfully: "更新成功",
Create_successfully: "创建成功",
Update_failure: "更新失败",
Create_failure: "创建失败",
View_details: "查看详情",
All: "全部",
Please_input_prompt_name: "请输入prompt名称",
Copy_Btn: '复制',
Delete_Btn: '删除'
} as const;

21
web/locales/zh/flow.ts Normal file
View File

@@ -0,0 +1,21 @@
import { Select } from 'antd';
export const FlowZn = {
Upload_Data_Successfully: '文件上传成功',
Upload_Data_Failed: '文件上传失败',
Upload_Data: '上传数据',
Code_Editor: '代码编辑器',
Open_Code_Editor: '打开代码编辑器',
Export_Flow_Success: '导出工作流成功',
Import_Flow_Success: '导入工作流成功',
Import: '导入',
Export: '导出',
Import_Flow: '导入工作流',
Export_Flow: '导出工作流',
Select_File: '选择文件',
Save_After_Import: '导入后保存',
Export_File_Type: '文件类型',
Export_File_Format: '文件格式',
Yes: '是',
No: '否',
};

View File

@@ -1,9 +1,11 @@
import { CommonZh } from './common';
import { ChatZh } from './chat';
import { CommonZh } from "./common";
import { ChatZh } from "./chat";
import { FlowZn } from "./flow";
const zh = {
...CommonZh,
...ChatZh,
...FlowZn,
...CommonZh,
};
export default zh;

View File

@@ -28,7 +28,6 @@ const zeroWidthTriggerDefaultStyle: React.CSSProperties = {
};
/**
*
* 会话项
*/
const MenuItem: React.FC<{ item: any; refresh?: any; order: React.MutableRefObject<number>; historyLoading?: boolean }> = ({

View File

@@ -1,8 +1,11 @@
import { ChatContext, ChatContextProvider } from '@/app/chat-context';
import { addUser, apiInterceptors } from '@/client/api';
import SideBar from '@/components/layout/side-bar';
import TopProgressBar from '@/components/layout/top-progress-bar';
import { STORAGE_LANG_KEY, STORAGE_USERINFO_KEY, STORAGE_USERINFO_VALID_TIME_KEY } from '@/utils/constants/index';
import {
STORAGE_LANG_KEY,
STORAGE_USERINFO_KEY,
STORAGE_USERINFO_VALID_TIME_KEY,
} from '@/utils/constants/index';
import { App, ConfigProvider, MappingAlgorithm, theme } from 'antd';
import enUS from 'antd/locale/en_US';
import zhCN from 'antd/locale/zh_CN';
@@ -43,7 +46,9 @@ function CssWrapper({ children }: { children: React.ReactElement }) {
}, [mode]);
useEffect(() => {
i18n.changeLanguage && i18n.changeLanguage(window.localStorage.getItem(STORAGE_LANG_KEY) || 'zh');
i18n.changeLanguage?.(
window.localStorage.getItem(STORAGE_LANG_KEY) || 'zh'
);
}, [i18n]);
return (
@@ -61,7 +66,6 @@ function LayoutWrapper({ children }: { children: React.ReactNode }) {
const router = useRouter();
// 登录检测
const handleAuth = async () => {
setIsLogin(false);
@@ -77,10 +81,13 @@ function LayoutWrapper({ children }: { children: React.ReactNode }) {
user_channel: `dbgpt`,
user_no: `001`,
nick_name: `dbgpt`,
}
};
if (user) {
localStorage.setItem(STORAGE_USERINFO_KEY, JSON.stringify(user));
localStorage.setItem(STORAGE_USERINFO_VALID_TIME_KEY, Date.now().toString());
localStorage.setItem(
STORAGE_USERINFO_VALID_TIME_KEY,
Date.now().toString()
);
setIsLogin(true);
}
};
@@ -98,16 +105,28 @@ function LayoutWrapper({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
return (
<div className="flex w-screen h-screen overflow-hidden">
<div className='flex w-screen h-screen overflow-hidden'>
<Head>
<meta name="viewport" content="initial-scale=1.0, width=device-width, maximum-scale-1" />
<meta
name='viewport'
content='initial-scale=1.0, width=device-width, maximum-scale=1'
/>
</Head>
{router.pathname !== '/construct/app/extra' && (
<div className={classNames('transition-[width]', isMenuExpand ? 'w-60' : 'w-20', 'hidden', 'md:block')}>
<div
className={classNames(
'transition-[width]',
isMenuExpand ? 'w-60' : 'w-20',
'hidden',
'md:block'
)}
>
<SideBar />
</div>
)}
<div className="flex flex-col flex-1 relative overflow-hidden">{children}</div>
<div className='flex flex-col flex-1 relative overflow-hidden'>
{children}
</div>
<FloatHelper />
</div>
);

View File

@@ -1,34 +0,0 @@
import MarketPlugins from '@/components/agent/market-plugins';
import MyPlugins from '@/components/agent/my-plugins';
import { Tabs } from 'antd';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
function Agent() {
const { t } = useTranslation();
const [activeKey, setActiveKey] = useState('market');
const items: Required<Parameters<typeof Tabs>[0]['items']> = useMemo(
() => [
{
key: 'market',
label: t('Market_Plugins'),
children: <MarketPlugins />,
},
{
key: 'my',
label: t('My_Plugins'),
children: activeKey === 'market' ? null : <MyPlugins />,
},
],
[t, activeKey],
);
return (
<div className="h-screen p-4 md:p-6 overflow-y-auto">
<Tabs activeKey={activeKey} items={items} onChange={setActiveKey} />
</div>
);
}
export default Agent;

View File

@@ -1,114 +0,0 @@
import AppModal from '@/components/app/app-modal';
import AppCard from '@/components/app/app-card';
import { Button, Spin, Tabs, TabsProps } from 'antd';
import React, { useEffect, useState } from 'react';
import { apiInterceptors, getAppList } from '@/client/api';
import { IApp } from '@/types/app';
import { PlusOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import MyEmpty from '@/components/common/MyEmpty';
type TabKey = 'app' | 'collected';
type ModalType = 'edit' | 'add';
export default function App() {
const { t } = useTranslation();
const [open, setOpen] = useState<boolean>(false);
const [spinning, setSpinning] = useState<boolean>(false);
const [activeKey, setActiveKey] = useState<TabKey>('app');
const [apps, setApps] = useState<IApp[]>([]);
const [curApp, setCurApp] = useState<IApp>();
const [modalType, setModalType] = useState<ModalType>('add');
const handleCreate = () => {
setModalType('add');
setOpen(true);
};
const handleCancel = () => {
setOpen(false);
};
const handleEdit = (app: any) => {
setModalType('edit');
setCurApp(app);
setOpen(true);
};
const handleTabChange = (activeKey: string) => {
setActiveKey(activeKey as TabKey);
if (activeKey === 'collected') {
initData({ is_collected: true });
} else {
initData();
}
};
const initData = async (params = {}) => {
setSpinning(true);
const [error, data] = await apiInterceptors(getAppList(params));
if (error) {
setSpinning(false);
return;
}
if (!data) return;
setApps(data.app_list || []);
setSpinning(false);
};
useEffect(() => {
initData();
}, []);
const renderAppList = (data: { isCollected: boolean }) => {
const isNull = data.isCollected ? apps.every((item) => !item.is_collected) : apps.length === 0;
return (
<div>
{!data.isCollected && (
<Button onClick={handleCreate} type="primary" className="mb-4" icon={<PlusOutlined />}>
{t('create')}
</Button>
)}
{!isNull ? (
<div className=" w-full flex flex-wrap pb-0 gap-4">
{apps.map((app, index) => {
return <AppCard handleEdit={handleEdit} key={index} app={app} updateApps={initData} isCollected={activeKey === 'collected'} />;
})}
</div>
) : (
<MyEmpty />
)}
</div>
);
};
const items: TabsProps['items'] = [
{
key: 'app',
label: t('App'),
children: renderAppList({ isCollected: false }),
},
{
key: 'collected',
label: t('collected'),
children: renderAppList({ isCollected: true }),
},
];
return (
<>
<Spin spinning={spinning}>
<div className="h-screen w-full p-4 md:p-6 overflow-y-auto">
<Tabs defaultActiveKey="app" items={items} onChange={handleTabChange} />
{open && (
<AppModal app={modalType === 'edit' ? curApp : {}} type={modalType} updateApps={initData} open={open} handleCancel={handleCancel} />
)}
</div>
</Spin>
</>
);
}

View File

@@ -174,7 +174,7 @@ function Database() {
onDelete(item);
}}
>
{t('Delete_Btn')}
</span>
),
},

View File

@@ -1,20 +1,48 @@
import { addFlow, apiInterceptors, getFlowById, updateFlowById } from '@/client/api';
import { apiInterceptors, getFlowById } from '@/client/api';
import MuiLoading from '@/components/common/loading';
import AddNodes from '@/components/flow/add-nodes';
import AddNodesSider from '@/components/flow/add-nodes-sider';
import ButtonEdge from '@/components/flow/button-edge';
import CanvasNode from '@/components/flow/canvas-node';
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
import { FrownOutlined, SaveOutlined } from '@ant-design/icons';
import { App, Button, Checkbox, Divider, Form, Input, Modal, Space, message, notification } from 'antd';
import {
checkFlowDataRequied,
getUniqueNodeId,
mapUnderlineToHump,
} from '@/utils/flow';
import {
ExportOutlined,
FrownOutlined,
ImportOutlined,
SaveOutlined,
} from '@ant-design/icons';
import { Divider, Space, Tooltip, message, notification } from 'antd';
import { useSearchParams } from 'next/navigation';
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
import React, {
DragEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, Node } from 'reactflow';
import ReactFlow, {
Background,
Connection,
Controls,
ReactFlowProvider,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
Node,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { useRouter } from 'next/router';
const { TextArea } = Input;
import {
SaveFlowModal,
ExportFlowModal,
ImportFlowModal,
} from '@/components/flow/canvas-modal';
interface Props {
// Define your component props here
@@ -24,9 +52,7 @@ const edgeTypes = { buttonedge: ButtonEdge };
const Canvas: React.FC<Props> = () => {
const { t } = useTranslation();
const { message } = App.useApp();
const { replace } = useRouter();
const [form] = Form.useForm();
const searchParams = useSearchParams();
const id = searchParams?.get('id') || '';
const reactFlow = useReactFlow();
@@ -35,8 +61,10 @@ const Canvas: React.FC<Props> = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
const [isSaveFlowModalOpen, setIsSaveFlowModalOpen] = useState(false);
const [isExportFlowModalOpen, setIsExportFlowModalOpen] = useState(false);
const [isImportModalOpen, setIsImportFlowModalOpen] = useState(false);
async function getFlowData() {
setLoading(true);
@@ -81,7 +109,7 @@ const Canvas: React.FC<Props> = () => {
};
}
return node;
}),
})
);
}
@@ -98,13 +126,18 @@ const Canvas: React.FC<Props> = () => {
(event: DragEvent) => {
event.preventDefault();
const reactFlowBounds = reactFlowWrapper.current!.getBoundingClientRect();
const sidebarWidth = (
document.getElementsByClassName('ant-layout-sider')?.[0] as HTMLElement
)?.offsetWidth; // get sidebar width
let nodeStr = event.dataTransfer.getData('application/reactflow');
if (!nodeStr || typeof nodeStr === 'undefined') {
return;
}
const nodeData = JSON.parse(nodeStr);
const position = reactFlow.screenToFlowPosition({
x: event.clientX - reactFlowBounds.left,
x: event.clientX - reactFlowBounds.left + sidebarWidth,
y: event.clientY - reactFlowBounds.top,
});
const nodeId = getUniqueNodeId(nodeData, reactFlow.getNodes());
@@ -129,10 +162,10 @@ const Canvas: React.FC<Props> = () => {
};
}
return node;
}),
})
);
},
[reactFlow],
[reactFlow]
);
const onDragOver = useCallback((event: DragEvent) => {
@@ -140,18 +173,7 @@ const Canvas: React.FC<Props> = () => {
event.dataTransfer.dropEffect = 'move';
}, []);
function labelChange(e: React.ChangeEvent<HTMLInputElement>) {
const label = e.target.value;
// replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -.
let result = label
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_-]/g, '')
.toLowerCase();
result = result;
form.setFieldsValue({ name: result });
}
function clickSave() {
function onSave() {
const flowData = reactFlow.toObject() as IFlowData;
const [check, node, message] = checkFlowDataRequied(flowData);
if (!check && message) {
@@ -169,52 +191,68 @@ const Canvas: React.FC<Props> = () => {
};
}
return item;
}),
})
);
return notification.error({ message: 'Error', description: message, icon: <FrownOutlined className="text-red-600" /> });
return notification.error({
message: 'Error',
description: message,
icon: <FrownOutlined className='text-red-600' />,
});
}
setIsModalVisible(true);
setIsSaveFlowModalOpen(true);
}
async function handleSaveFlow() {
const { name, label, description = '', editable = false, deploy = false } = form.getFieldsValue();
const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData);
if (id) {
const [, , res] = await apiInterceptors(updateFlowById(id, {
name,
label,
description,
editable,
uid: id,
flow_data: reactFlowObject,
state: deploy ? 'deployed' : 'developing',
}));
setIsModalVisible(false);
if (res?.success) {
message.success('编辑成功');
replace('/construct/flow');
setIsModalVisible(false);
} else if (res?.err_msg) {
message.error(res?.err_msg);
function onExport() {
setIsExportFlowModalOpen(true);
}
} else {
const [_, res] = await apiInterceptors(addFlow({ name, label, description, editable, flow_data: reactFlowObject, state: deploy ? 'deployed' : 'developing' }));
setIsModalVisible(false);
replace('/construct/flow');
message.success('创建成功');
function onImport() {
setIsImportFlowModalOpen(true);
}
const getButtonList = () => {
const buttonList = [
{
title: t('Import'),
icon: <ImportOutlined className='block text-xl' onClick={onImport} />,
},
{
title: t('save'),
icon: <SaveOutlined className='block text-xl' onClick={onSave} />,
},
];
if (id !== '') {
buttonList.unshift({
title: t('Export'),
icon: <ExportOutlined className='block text-xl' onClick={onExport} />,
});
}
return buttonList;
};
return (
<>
<MuiLoading visible={loading} />
<div className="my-2 mx-4 flex flex-row justify-end items-center">
<div className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 flext justify-center items-center hover:text-blue-500 dark:hover:text-zinc-100">
<SaveOutlined className="block text-xl" onClick={clickSave} />
</div>
</div>
<Divider className="mt-0 mb-0" />
<div className="h-[calc(100vh-60px)] w-full" ref={reactFlowWrapper}>
<div className='flex flex-row'>
<AddNodesSider />
<div className='flex flex-col flex-1'>
<Space className='my-2 mx-4 flex flex-row justify-end'>
{getButtonList().map(({ title, icon }) => (
<Tooltip
key={title}
title={title}
className='w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 hover:text-blue-500 dark:hover:text-zinc-100'
>
{icon}
</Tooltip>
))}
</Space>
<Divider className='mt-0 mb-0' />
<div className='h-[calc(100vh-48px)] w-full' ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
@@ -230,78 +268,39 @@ const Canvas: React.FC<Props> = () => {
fitView
deleteKeyCode={['Backspace', 'Delete']}
>
<Controls className="flex flex-row items-center" position="bottom-center" />
<Background color="#aaa" gap={16} />
<AddNodes />
<Controls
className='flex flex-row items-center'
position='bottom-center'
/>
<Background color='#aaa' gap={16} />
{/* <AddNodes /> */}
</ReactFlow>
</div>
<Modal
title={t('flow_modal_title')}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false);
}}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
name="flow_form"
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={handleSaveFlow}
autoComplete="off"
>
<Form.Item label="Title" name="label" initialValue={flowInfo?.label} rules={[{ required: true, message: 'Please input flow title!' }]}>
<Input onChange={labelChange} />
</Form.Item>
<Form.Item
label="Name"
name="name"
initialValue={flowInfo?.name}
rules={[
{ required: true, message: 'Please input flow name!' },
() => ({
validator(_, value) {
const regex = /^[a-zA-Z0-9_\-]+$/;
if (!regex.test(value)) {
return Promise.reject('Can only contain numbers, letters, underscores, and dashes');
}
return Promise.resolve();
},
}),
]}
>
<Input />
</Form.Item>
<Form.Item label="Description" initialValue={flowInfo?.description} name="description">
<TextArea rows={3} />
</Form.Item>
<Form.Item label="Editable" name="editable" initialValue={id ? flowInfo?.editable : true} valuePropName="checked">
<Checkbox></Checkbox>
</Form.Item>
<Form.Item label="Deploy" name="deploy" initialValue={id ? (flowInfo?.state === 'deployed' || flowInfo?.state === 'running') : true} valuePropName="checked">
<Checkbox></Checkbox>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Space>
<Button
htmlType="button"
onClick={() => {
setIsModalVisible(false);
}}
>
Cancel
</Button>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
</div>
<MuiLoading visible={loading} />
<SaveFlowModal
reactFlow={reactFlow}
flowInfo={flowInfo}
isSaveFlowModalOpen={isSaveFlowModalOpen}
setIsSaveFlowModalOpen={setIsSaveFlowModalOpen}
/>
<ExportFlowModal
reactFlow={reactFlow}
flowInfo={flowInfo}
isExportFlowModalOpen={isExportFlowModalOpen}
setIsExportFlowModalOpen={setIsExportFlowModalOpen}
/>
<ImportFlowModal
setNodes={setNodes}
setEdges={setEdges}
isImportModalOpen={isImportModalOpen}
setIsImportFlowModalOpen={setIsImportFlowModalOpen}
/>
</>
);
};

View File

@@ -1,12 +1,31 @@
import BlurredCard, { ChatButton, InnerDropdown } from '@/new-components/common/blurredCard';
import BlurredCard, {
ChatButton,
InnerDropdown,
} from '@/new-components/common/blurredCard';
import ConstructLayout from '@/new-components/layout/Construct';
import { ChatContext } from '@/app/chat-context';
import { apiInterceptors, deleteFlowById, getFlows, newDialogue, updateFlowAdmins, addFlow } from '@/client/api';
import {
apiInterceptors,
deleteFlowById,
getFlows,
newDialogue,
addFlow,
} from '@/client/api';
import MyEmpty from '@/components/common/MyEmpty';
import { IFlow, IFlowUpdateParam } from '@/types/flow';
import { PlusOutlined } from '@ant-design/icons';
import { useRequest } from 'ahooks';
import { Button, Modal, Popconfirm, Select, Spin, Tag, message, Form, Input, Checkbox } from 'antd';
import {
Button,
Modal,
Popconfirm,
Spin,
Tag,
message,
Form,
Input,
Checkbox,
} from 'antd';
import { t } from 'i18next';
import { concat, debounce } from 'lodash';
import moment from 'moment';
@@ -19,18 +38,14 @@ function Flow() {
const router = useRouter();
const { model } = useContext(ChatContext);
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<Pick<IFlow, 'label' | 'name'>>();
const [flowList, setFlowList] = useState<Array<IFlow>>([]);
const [adminOpen, setAdminOpen] = useState<boolean>(false);
const [curFlow, setCurFLow] = useState<IFlow>();
const [admins, setAdmins] = useState<string[]>([]);
const copyFlowTemp = useRef<IFlow>();
const [showModal, setShowModal] = useState(false);
const [deploy, setDeploy] = useState(false);
const [editable, setEditable] = useState(false);
const [form] = Form.useForm<Pick<IFlow, 'label' | 'name'>>();
// 分页信息
const totalRef = useRef<{
current_page: number;
@@ -41,18 +56,14 @@ function Flow() {
const scrollRef = useRef<HTMLDivElement>(null);
// 获取列表
const {
run: getFlowListRun,
loading,
refresh: refreshFlowList,
} = useRequest(
const { run: getFlowListRun, loading } = useRequest(
async (params: any) =>
await apiInterceptors(
getFlows({
page: 1,
page_size: 12,
...params,
}),
})
),
{
cacheKey: 'query-flow-list',
@@ -66,7 +77,7 @@ function Flow() {
};
},
throttleWait: 300,
},
}
);
const { i18n } = useTranslation();
@@ -112,7 +123,9 @@ function Flow() {
}, [loading, handleScroll, loadMoreData]);
const handleChat = async (flow: IFlow) => {
const [, res] = await apiInterceptors(newDialogue({ chat_mode: 'chat_agent' }));
const [, res] = await apiInterceptors(
newDialogue({ chat_mode: 'chat_agent' })
);
if (res) {
const queryStr = qs.stringify({
scene: 'chat_flow',
@@ -131,30 +144,6 @@ function Flow() {
}
}
useEffect(() => {
if (curFlow?.admins?.length) {
setAdmins(curFlow?.admins);
} else {
setAdmins([]);
}
}, [curFlow]);
// 更新管理员
const { run: updateAdmins, loading: adminLoading } = useRequest(
async (value: string[]) => await apiInterceptors(updateFlowAdmins({ uid: curFlow?.uid || '', admins: value })),
{
manual: true,
onSuccess: (data) => {
const [error] = data;
if (!error) {
message.success('更新成功');
} else {
message.error('更新失败');
}
},
},
);
const handleCopy = (flow: IFlow) => {
copyFlowTemp.current = flow;
form.setFieldValue('label', `${flow.label} Copy`);
@@ -166,7 +155,8 @@ function Flow() {
const onFinish = async (val: { name: string; label: string }) => {
if (!copyFlowTemp.current) return;
const { source, uid, dag_id, gmt_created, gmt_modified, state, ...params } = copyFlowTemp.current;
const { source, uid, dag_id, gmt_created, gmt_modified, state, ...params } =
copyFlowTemp.current;
const data: IFlowUpdateParam = {
...params,
editable,
@@ -181,18 +171,15 @@ function Flow() {
}
};
const handleChange = async (value: string[]) => {
setAdmins(value);
await updateAdmins(value);
await refreshFlowList();
};
return (
<ConstructLayout>
<Spin spinning={loading}>
<div className="relative h-screen w-full p-4 md:p-6 overflow-y-auto" ref={scrollRef}>
<div className="flex justify-between items-center mb-6">
<div className="flex items-center gap-4">
<div
className='relative h-screen w-full p-4 md:p-6 overflow-y-auto'
ref={scrollRef}
>
<div className='flex justify-between items-center mb-6'>
<div className='flex items-center gap-4'>
{/* <Input
variant="filled"
prefix={<SearchOutlined />}
@@ -204,9 +191,9 @@ function Flow() {
/> */}
</div>
<div className="flex items-center gap-4">
<div className='flex items-center gap-4'>
<Button
className="border-none text-white bg-button-gradient"
className='border-none text-white bg-button-gradient'
icon={<PlusOutlined />}
onClick={() => {
router.push('/construct/flow/canvas');
@@ -216,13 +203,13 @@ function Flow() {
</Button>
</div>
</div>
<div className="flex flex-wrap mx-[-8px] pb-12 justify-start items-stretch">
<div className='flex flex-wrap mx-[-8px] pb-12 justify-start items-stretch'>
{flowList.map((flow) => (
<BlurredCard
description={flow.description}
name={flow.name}
key={flow.uid}
logo="/pictures/flow.png"
logo='/pictures/flow.png'
onClick={() => {
router.push('/construct/flow/canvas?id=' + flow.uid);
}}
@@ -230,19 +217,6 @@ function Flow() {
<InnerDropdown
menu={{
items: [
// {
// key: 'edit',
// label: (
// <span
// onClick={() => {
// setAdminOpen(true);
// setCurFLow(flow);
// }}
// >
// 权限管理
// </span>
// ),
// },
{
key: 'copy',
label: (
@@ -251,15 +225,20 @@ function Flow() {
handleCopy(flow);
}}
>
{t('copy')}
{t('Copy_Btn')}
</span>
),
},
{
key: 'del',
label: (
<Popconfirm title="Are you sure to delete this flow?" onConfirm={() => deleteFlow(flow)}>
<span className="text-red-400"></span>
<Popconfirm
title='Are you sure to delete this flow?'
onConfirm={() => deleteFlow(flow)}
>
<span className='text-red-400'>
{t('Delete_Btn')}
</span>
</Popconfirm>
),
},
@@ -270,16 +249,36 @@ function Flow() {
rightTopHover={false}
Tags={
<div>
<Tag color={flow.source === 'DBGPT-WEB' ? 'green' : 'blue'}>{flow.source}</Tag>
<Tag color={flow.editable ? 'green' : 'gray'}>{flow.editable ? 'Editable' : 'Can not Edit'}</Tag>
<Tag color={flow.state === 'load_failed' ? 'red' : flow.state === 'running' ? 'green' : 'blue'}>{flow.state}</Tag>
<Tag color={flow.source === 'DBGPT-WEB' ? 'green' : 'blue'}>
{flow.source}
</Tag>
<Tag color={flow.editable ? 'green' : 'gray'}>
{flow.editable ? 'Editable' : 'Can not Edit'}
</Tag>
<Tag
color={
flow.state === 'load_failed'
? 'red'
: flow.state === 'running'
? 'green'
: 'blue'
}
>
{flow.state}
</Tag>
</div>
}
LeftBottom={
<div key={i18n.language + 'flow'} className="flex gap-2">
<div key={i18n.language + 'flow'} className='flex gap-2'>
<span>{flow?.nick_name}</span>
<span></span>
{flow?.gmt_modified && <span>{moment(flow?.gmt_modified).fromNow() + ' ' + t('update')}</span>}
{flow?.gmt_modified && (
<span>
{moment(flow?.gmt_modified).fromNow() +
' ' +
t('update')}
</span>
)}
</div>
}
RightBottom={
@@ -292,40 +291,26 @@ function Flow() {
}
/>
))}
{flowList.length === 0 && <MyEmpty description="No flow found" />}
{flowList.length === 0 && <MyEmpty description='No flow found' />}
</div>
</div>
</Spin>
<Modal title="权限管理" open={adminOpen} onCancel={() => setAdminOpen(false)} footer={null}>
<div className="py-4">
<div className="mb-1">0</div>
<Select
mode="tags"
value={admins}
style={{ width: '100%' }}
onChange={handleChange}
tokenSeparators={[',']}
options={admins?.map((item: string) => ({ label: item, value: item }))}
loading={adminLoading}
/>
</div>
</Modal>
<Modal
open={showModal}
title="Copy AWEL Flow"
title='Copy AWEL Flow'
onCancel={() => {
setShowModal(false);
}}
footer={false}
>
<Form form={form} onFinish={onFinish} className="mt-6">
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Form form={form} onFinish={onFinish} className='mt-6'>
<Form.Item name='name' label='Name' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="label" label="Label" rules={[{ required: true }]}>
<Form.Item name='label' label='Label' rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="editable">
<Form.Item label='editable'>
<Checkbox
value={editable}
checked={editable}
@@ -335,7 +320,7 @@ function Flow() {
}}
/>
</Form.Item>
<Form.Item label="deploy">
<Form.Item label='deploy'>
<Checkbox
value={deploy}
checked={deploy}
@@ -345,8 +330,8 @@ function Flow() {
}}
/>
</Form.Item>
<div className="flex justify-end">
<Button type="primary" htmlType="submit">
<div className='flex justify-end'>
<Button type='primary' htmlType='submit'>
{t('Submit')}
</Button>
</div>

View File

@@ -1,233 +0,0 @@
import React, { useMemo, useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import { Badge, Button, Card, Drawer, Empty, Modal, message, Spin } from 'antd';
import FormDialog from '@/components/database/form-dialog';
import { apiInterceptors, getDbList, getDbSupportType, postDbDelete, postDbRefresh } from '@/client/api';
import { DeleteFilled, EditFilled, PlusOutlined, RedoOutlined } from '@ant-design/icons';
import { DBOption, DBType, DbListResponse, DbSupportTypeResponse } from '@/types/db';
import MuiLoading from '@/components/common/loading';
import { dbMapper } from '@/utils';
import GPTCard from '@/components/common/gpt-card';
import { useTranslation } from 'react-i18next';
type DBItem = DbListResponse[0];
export function isFileDb(dbTypeList: DBOption[], dbType: DBType) {
return dbTypeList.find((item) => item.value === dbType)?.isFileDb;
}
function Database() {
const { t } = useTranslation();
const [dbList, setDbList] = useState<DbListResponse>([]);
const [dbSupportList, setDbSupportList] = useState<DbSupportTypeResponse>([]);
const [loading, setLoading] = useState(false);
const [modal, setModal] = useState<{ open: boolean; info?: DBItem; dbType?: DBType }>({ open: false });
const [draw, setDraw] = useState<{ open: boolean; dbList?: DbListResponse; name?: string; type?: DBType }>({ open: false });
const [refreshLoading, setRefreshLoading] = useState(false);
const getDbSupportList = async () => {
const [, data] = await apiInterceptors(getDbSupportType());
setDbSupportList(data ?? []);
};
const refreshDbList = async () => {
setLoading(true);
const [, data] = await apiInterceptors(getDbList());
setDbList(data ?? []);
setLoading(false);
};
const dbTypeList = useMemo(() => {
const supportDbList = dbSupportList.map((item) => {
const { db_type, is_file_db } = item;
return { ...dbMapper[db_type], value: db_type, isFileDb: is_file_db };
}) as DBOption[];
const unSupportDbList = Object.keys(dbMapper)
.filter((item) => !supportDbList.some((db) => db.value === item))
.map((item) => ({ ...dbMapper[item], value: dbMapper[item].label, disabled: true })) as DBOption[];
return [...supportDbList, ...unSupportDbList];
}, [dbSupportList]);
const onModify = (item: DBItem) => {
setModal({ open: true, info: item });
};
const onDelete = (item: DBItem) => {
Modal.confirm({
title: 'Tips',
content: `Do you Want to delete the ${item.db_name}?`,
onOk() {
return new Promise<void>(async (resolve, reject) => {
try {
const [err] = await apiInterceptors(postDbDelete(item.db_name));
if (err) {
message.error(err.message);
reject();
return;
}
message.success('success');
refreshDbList();
resolve();
} catch (e: any) {
reject();
}
});
},
});
};
const onRefresh = async (item: DBItem) => {
setRefreshLoading(true);
const [, res] = await apiInterceptors(postDbRefresh({ db_name: item.db_name, db_type: item.db_type }));
if (res) message.success(t('refreshSuccess'));
setRefreshLoading(false);
};
const dbListByType = useMemo(() => {
const mapper = dbTypeList.reduce((acc, item) => {
acc[item.value] = dbList.filter((dbConn) => dbConn.db_type === item.value);
return acc;
}, {} as Record<DBType, DbListResponse>);
return mapper;
}, [dbList, dbTypeList]);
useAsyncEffect(async () => {
await refreshDbList();
await getDbSupportList();
}, []);
const handleDbTypeClick = (info: DBOption) => {
const dbItems = dbList.filter((item) => item.db_type === info.value);
setDraw({ open: true, dbList: dbItems, name: info.label, type: info.value });
};
return (
<div className="relative p-4 md:p-6 min-h-full overflow-y-auto">
<MuiLoading visible={loading} />
<div className="mb-4">
<Button
type="primary"
className="flex items-center"
icon={<PlusOutlined />}
onClick={() => {
setModal({ open: true });
}}
>
{t('create')}
</Button>
</div>
<div className="flex flex-wrap gap-2 md:gap-4">
{dbTypeList.map((item) => (
<Badge key={item.value} count={dbListByType[item.value].length} className="min-h-fit">
<GPTCard
className="h-full"
title={item.label}
desc={item.desc ?? ''}
disabled={item.disabled}
icon={item.icon}
onClick={() => {
if (item.disabled) return;
handleDbTypeClick(item);
}}
/>
</Badge>
))}
</div>
<FormDialog
open={modal.open}
dbTypeList={dbTypeList}
choiceDBType={modal.dbType}
editValue={modal.info}
dbNames={dbList.map((item) => item.db_name)}
onSuccess={() => {
setModal({ open: false });
refreshDbList();
}}
onClose={() => {
setModal({ open: false });
}}
/>
<Drawer
title={draw.name}
placement="right"
onClose={() => {
setDraw({ open: false });
}}
open={draw.open}
>
{draw.type && dbListByType[draw.type] && dbListByType[draw.type].length ? (
<Spin spinning={refreshLoading}>
<Button
type="primary"
className="mb-4 flex items-center"
icon={<PlusOutlined />}
onClick={() => {
setModal({ open: true, dbType: draw.type });
}}
>
Create
</Button>
{dbListByType[draw.type].map((item) => (
<Card
key={item.db_name}
title={item.db_name}
extra={
<div className="flex items-center gap-3">
<RedoOutlined
style={{ color: 'gray' }}
onClick={() => {
onRefresh(item);
}}
/>
<EditFilled
className="mr-2"
style={{ color: '#1b7eff' }}
onClick={() => {
onModify(item);
}}
/>
<DeleteFilled
style={{ color: '#ff1b2e' }}
onClick={() => {
onDelete(item);
}}
/>
</div>
}
className="mb-4"
>
{item.db_path ? (
<p>path: {item.db_path}</p>
) : (
<>
<p>host: {item.db_host}</p>
<p>username: {item.db_user}</p>
<p>port: {item.db_port}</p>
</>
)}
<p>remark: {item.comment}</p>
</Card>
))}
</Spin>
) : (
<Empty image={Empty.PRESENTED_IMAGE_DEFAULT}>
<Button
type="primary"
className="flex items-center mx-auto"
icon={<PlusOutlined />}
onClick={() => {
setModal({ open: true, dbType: draw.type });
}}
>
Create Now
</Button>
</Empty>
)}
</Drawer>
</div>
);
}
export default Database;

View File

@@ -1,319 +0,0 @@
import { addFlow, apiInterceptors, getFlowById, updateFlowById } from '@/client/api';
import MuiLoading from '@/components/common/loading';
import AddNodes from '@/components/flow/add-nodes';
import ButtonEdge from '@/components/flow/button-edge';
import CanvasNode from '@/components/flow/canvas-node';
import { IFlowData, IFlowUpdateParam } from '@/types/flow';
import { checkFlowDataRequied, getUniqueNodeId, mapHumpToUnderline, mapUnderlineToHump } from '@/utils/flow';
import { FrownOutlined, SaveOutlined } from '@ant-design/icons';
import { Button, Checkbox, Divider, Form, Input, Modal, Space, message, notification } from 'antd';
import { useSearchParams } from 'next/navigation';
import React, { DragEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactFlow, { Background, Connection, Controls, ReactFlowProvider, addEdge, useEdgesState, useNodesState, useReactFlow, Node } from 'reactflow';
import 'reactflow/dist/style.css';
const { TextArea } = Input;
interface Props {
// Define your component props here
}
const nodeTypes = { customNode: CanvasNode };
const edgeTypes = { buttonedge: ButtonEdge };
const Canvas: React.FC<Props> = () => {
const { t } = useTranslation();
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<IFlowUpdateParam>();
const searchParams = useSearchParams();
const id = searchParams?.get('id') || '';
const reactFlow = useReactFlow();
const [loading, setLoading] = useState(false);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [flowInfo, setFlowInfo] = useState<IFlowUpdateParam>();
const [deploy, setDeploy] = useState(true);
async function getFlowData() {
setLoading(true);
const [_, data] = await apiInterceptors(getFlowById(id));
if (data) {
const flowData = mapUnderlineToHump(data.flow_data);
setFlowInfo(data);
setNodes(flowData.nodes);
setEdges(flowData.edges);
}
setLoading(false);
}
useEffect(() => {
id && getFlowData();
}, [id]);
useEffect(() => {
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.returnValue = message;
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, []);
function onNodesClick(event: any, clickedNode: Node) {
reactFlow.setNodes((nds) =>
nds.map((node) => {
if (node.id === clickedNode.id) {
node.data = {
...node.data,
selected: true,
};
} else {
node.data = {
...node.data,
selected: false,
};
}
return node;
}),
);
}
function onConnect(connection: Connection) {
const newEdge = {
...connection,
type: 'buttonedge',
id: `${connection.source}|${connection.target}`,
};
setEdges((eds) => addEdge(newEdge, eds));
}
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
const reactFlowBounds = reactFlowWrapper.current!.getBoundingClientRect();
let nodeStr = event.dataTransfer.getData('application/reactflow');
if (!nodeStr || typeof nodeStr === 'undefined') {
return;
}
const nodeData = JSON.parse(nodeStr);
const position = reactFlow.screenToFlowPosition({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
const nodeId = getUniqueNodeId(nodeData, reactFlow.getNodes());
nodeData.id = nodeId;
const newNode = {
id: nodeId,
position,
type: 'customNode',
data: nodeData,
};
setNodes((nds) =>
nds.concat(newNode).map((node) => {
if (node.id === newNode.id) {
node.data = {
...node.data,
selected: true,
};
} else {
node.data = {
...node.data,
selected: false,
};
}
return node;
}),
);
},
[reactFlow],
);
const onDragOver = useCallback((event: DragEvent) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
function labelChange(e: React.ChangeEvent<HTMLInputElement>) {
const label = e.target.value;
// replace spaces with underscores, convert uppercase letters to lowercase, remove characters other than digits, letters, _, and -.
let result = label
.replace(/\s+/g, '_')
.replace(/[^a-z0-9_-]/g, '')
.toLowerCase();
result = result;
form.setFieldsValue({ name: result });
}
function clickSave() {
const flowData = reactFlow.toObject() as IFlowData;
const [check, node, message] = checkFlowDataRequied(flowData);
if (!check && message) {
setNodes((nds) =>
nds.map((item) => {
if (item.id === node?.id) {
item.data = {
...item.data,
invalid: true,
};
} else {
item.data = {
...item.data,
invalid: false,
};
}
return item;
}),
);
return notification.error({ message: 'Error', description: message, icon: <FrownOutlined className="text-red-600" /> });
}
setIsModalVisible(true);
}
async function handleSaveFlow() {
const { name, label, description = '', editable = false, state = 'deployed' } = form.getFieldsValue();
const reactFlowObject = mapHumpToUnderline(reactFlow.toObject() as IFlowData);
if (id) {
const [, , res] = await apiInterceptors(updateFlowById(id, { name, label, description, editable, uid: id, flow_data: reactFlowObject, state }));
setIsModalVisible(false);
if (res?.success) {
messageApi.success(t('save_flow_success'));
} else if (res?.err_msg) {
messageApi.error(res?.err_msg);
}
} else {
const [_, res] = await apiInterceptors(addFlow({ name, label, description, editable, flow_data: reactFlowObject, state }));
if (res?.uid) {
messageApi.success(t('save_flow_success'));
const history = window.history;
history.pushState(null, '', `/flow/canvas?id=${res.uid}`);
}
setIsModalVisible(false);
}
}
return (
<>
<MuiLoading visible={loading} />
<div className="my-2 mx-4 flex flex-row justify-end items-center">
<div className="w-8 h-8 rounded-md bg-stone-300 dark:bg-zinc-700 dark:text-zinc-200 flext justify-center items-center hover:text-blue-500 dark:hover:text-zinc-100">
<SaveOutlined className="block text-xl" onClick={clickSave} />
</div>
</div>
<Divider className="mt-0 mb-0" />
<div className="h-[calc(100vh-60px)] w-full" ref={reactFlowWrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodesClick}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
minZoom={0.1}
fitView
deleteKeyCode={['Backspace', 'Delete']}
>
<Controls className="flex flex-row items-center" position="bottom-center" />
<Background color="#aaa" gap={16} />
<AddNodes />
</ReactFlow>
</div>
<Modal
title={t('flow_modal_title')}
open={isModalVisible}
onCancel={() => {
setIsModalVisible(false);
}}
cancelButtonProps={{ className: 'hidden' }}
okButtonProps={{ className: 'hidden' }}
>
<Form
name="flow_form"
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
style={{ maxWidth: 600 }}
initialValues={{ remember: true }}
onFinish={handleSaveFlow}
autoComplete="off"
>
<Form.Item label="Title" name="label" initialValue={flowInfo?.label} rules={[{ required: true, message: 'Please input flow title!' }]}>
<Input onChange={labelChange} />
</Form.Item>
<Form.Item
label="Name"
name="name"
initialValue={flowInfo?.name}
rules={[
{ required: true, message: 'Please input flow name!' },
() => ({
validator(_, value) {
const regex = /^[a-zA-Z0-9_\-]+$/;
if (!regex.test(value)) {
return Promise.reject('Can only contain numbers, letters, underscores, and dashes');
}
return Promise.resolve();
},
}),
]}
>
<Input />
</Form.Item>
<Form.Item label="Description" initialValue={flowInfo?.description} name="description">
<TextArea rows={3} />
</Form.Item>
<Form.Item label="Editable" name="editable" initialValue={flowInfo?.editable} valuePropName="checked">
<Checkbox />
</Form.Item>
<Form.Item hidden name="state">
<Input />
</Form.Item>
<Form.Item label="Deploy">
<Checkbox
defaultChecked={flowInfo?.state === 'deployed' || flowInfo?.state === 'running'}
value={deploy}
onChange={(e) => {
const val = e.target.checked;
form.setFieldValue('state', val ? 'deployed' : 'developing');
setDeploy(val);
}}
/>
</Form.Item>
<Form.Item wrapperCol={{ offset: 8, span: 16 }}>
<Space>
<Button
htmlType="button"
onClick={() => {
setIsModalVisible(false);
}}
>
Cancel
</Button>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
{contextHolder}
</>
);
};
export default function CanvasWrapper() {
return (
<ReactFlowProvider>
<Canvas />
</ReactFlowProvider>
);
}

View File

@@ -1,117 +0,0 @@
import { apiInterceptors, getFlows, addFlow } from '@/client/api';
import MyEmpty from '@/components/common/MyEmpty';
import MuiLoading from '@/components/common/loading';
import FlowCard from '@/components/flow/flow-card';
import { IFlow, IFlowUpdateParam } from '@/types/flow';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Checkbox, Form, Input, Modal, message } from 'antd';
import Link from 'next/link';
import React, { useEffect, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
function Flow() {
const { t } = useTranslation();
const [showModal, setShowModal] = useState(false);
const [loading, setLoading] = useState(false);
const [flowList, setFlowList] = useState<Array<IFlow>>([]);
const [deploy, setDeploy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm<Pick<IFlow, 'label' | 'name'>>();
const copyFlowTemp = useRef<IFlow>();
async function getFlowList() {
setLoading(true);
const [_, data] = await apiInterceptors(getFlows());
setLoading(false);
setFlowList(data?.items ?? []);
}
useEffect(() => {
getFlowList();
}, []);
function updateFlowList(uid: string) {
setFlowList((flows) => flows.filter((flow) => flow.uid !== uid));
}
const handleCopy = (flow: IFlow) => {
copyFlowTemp.current = flow;
form.setFieldValue('label', `${flow.label} Copy`);
form.setFieldValue('name', `${flow.name}_copy`);
setDeploy(false);
setShowModal(true);
};
const onFinish = async (val: { name: string; label: string }) => {
if (!copyFlowTemp.current) return;
const { source, uid, dag_id, gmt_created, gmt_modified, state, ...params } = copyFlowTemp.current;
const data: IFlowUpdateParam = {
...params,
editable: true,
state: deploy ? 'deployed' : 'developing',
...val,
};
const [err] = await apiInterceptors(addFlow(data));
if (!err) {
messageApi.success(t('save_flow_success'));
setShowModal(false);
getFlowList();
}
};
return (
<div className="relative p-4 md:p-6 min-h-full overflow-y-auto">
{contextHolder}
<MuiLoading visible={loading} />
<div className="mb-4">
<Link href="/flow/canvas">
<Button type="primary" className="flex items-center" icon={<PlusOutlined />}>
New AWEL Flow
</Button>
</Link>
</div>
<div className="flex flex-wrap gap-2 md:gap-4 justify-start items-stretch">
{flowList.map((flow) => (
<FlowCard key={flow.uid} flow={flow} deleteCallback={updateFlowList} onCopy={handleCopy} />
))}
{flowList.length === 0 && <MyEmpty description="No flow found" />}
</div>
<Modal
open={showModal}
title="Copy AWEL Flow"
onCancel={() => {
setShowModal(false);
}}
footer={false}
>
<Form form={form} onFinish={onFinish} className="mt-6">
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="label" label="Label" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="Deploy">
<Checkbox
value={deploy}
onChange={(e) => {
const val = e.target.checked;
setDeploy(val);
}}
/>
</Form.Item>
<div className="flex justify-end">
<Button type="primary" htmlType="submit">
{t('Submit')}
</Button>
</div>
</Form>
</Modal>
</div>
);
}
export default Flow;

View File

@@ -1,106 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { Breadcrumb, Card, Empty, Pagination, Spin } from 'antd';
import { useTranslation } from 'react-i18next';
import { apiInterceptors, getChunkList } from '@/client/api';
import DocIcon from '@/components/knowledge/doc-icon';
const DEDAULT_PAGE_SIZE = 10;
function ChunkList() {
const router = useRouter();
const { t } = useTranslation();
const [chunkList, setChunkList] = useState<any>([]);
const [total, setTotal] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const {
query: { id, spaceName },
} = useRouter();
const fetchChunks = async () => {
const [_, data] = await apiInterceptors(
getChunkList(spaceName as string, {
document_id: id as string,
page: 1,
page_size: DEDAULT_PAGE_SIZE,
}),
);
setChunkList(data?.data);
setTotal(data?.total!);
};
const loaderMore = async (page: number, page_size: number) => {
setLoading(true);
const [_, data] = await apiInterceptors(
getChunkList(spaceName as string, {
document_id: id as string,
page,
page_size,
}),
);
setChunkList(data?.data || []);
setLoading(false);
};
useEffect(() => {
spaceName && id && fetchChunks();
}, [id, spaceName]);
return (
<div className="h-full overflow-y-scroll relative px-2">
<Breadcrumb
className="m-6"
items={[
{
title: 'Knowledge',
onClick() {
router.back();
},
path: '/knowledge',
},
{
title: spaceName,
},
]}
/>
<Spin spinning={loading}>
<div className="flex justify-center flex-col">
{chunkList?.length > 0 ? (
chunkList?.map((chunk: any) => {
return (
<Card
key={chunk.id}
className="mt-2"
title={
<>
<DocIcon type={chunk.doc_type} />
<span>{chunk.doc_name}</span>
</>
}
>
<p className="font-semibold">{t('Content')}:</p>
<p>{chunk?.content}</p>
<p className="font-semibold">{t('Meta_Data')}: </p>
<p>{chunk?.meta_info}</p>
</Card>
);
})
) : (
<Empty image={Empty.PRESENTED_IMAGE_DEFAULT}></Empty>
)}
</div>
</Spin>
<Pagination
className="mx-2 my-4 float-right right-6 bottom-4"
defaultCurrent={1}
defaultPageSize={DEDAULT_PAGE_SIZE}
total={total}
showTotal={(total) => `Total ${total} items`}
onChange={loaderMore}
/>
</div>
);
}
export default ChunkList;

View File

@@ -1,137 +0,0 @@
import React, { useEffect,useRef, useState } from 'react';
import cytoscape from 'cytoscape';
import euler from 'cytoscape-euler';
import { Button } from 'antd';
import { RollbackOutlined } from '@ant-design/icons';
cytoscape.use(euler)
import { apiInterceptors,getGraphVis } from '@/client/api';
import { useRouter } from 'next/router';
const LAYOUTCONFIG = {
name: 'euler',
springLength: 340,
fit: false,
springCoeff: 0.0008,
mass: 20,
dragCoeff: 1,
gravity: -20,
pull: 0.009,
randomize: false,
padding: 0,
maxIterations: 1000,
maxSimulationTime: 4000,
}
function GraphVis() {
const myRef = useRef<HTMLDivElement>(null);
const LIMIT = 500
const router = useRouter();
const fetchGraphVis = async () => {
const [_, data] = await apiInterceptors(getGraphVis(spaceName as string,{limit:LIMIT}))
if(myRef.current && data){
let processedData = processResult(data)
renderGraphVis(processedData)
}
}
const processResult = (data:{nodes:Array<any>,edges:Array<any>}) => {
let nodes:any[] = []
let edges:any[] = []
data.nodes.forEach((node:any)=>{
let n = {
data:{
id:node.vid,
displayName:node.vid,
}
}
nodes.push(n)
})
data.edges.forEach((edge:any)=>{
let e = {
data:{
id:edge.src+'_'+edge.dst+'_'+edge.label,
source:edge.src,
target:edge.dst,
displayName:edge.label
}
}
edges.push(e)
})
return {
nodes,
edges
}
}
const renderGraphVis = (data:any)=> {
let dom = myRef.current as HTMLDivElement
let cy = cytoscape(
{
container:myRef.current,
elements:data,
zoom:0.3,
pixelRatio: 'auto',
style:[
{
selector: 'node',
style: {
width: 60,
height: 60,
color: '#fff',
'text-outline-color': '#37D4BE',
'text-outline-width': 2,
'text-valign': 'center',
'text-halign': 'center',
'background-color': '#37D4BE',
'label': 'data(displayName)'
}
},
{
selector: 'edge',
style: {
'width': 1,
color: '#fff',
'label': 'data(displayName)',
'line-color': '#66ADFF',
'font-size': 14,
'target-arrow-shape': 'vee',
'control-point-step-size': 40,
'curve-style': 'bezier',
'text-background-opacity': 1,
'text-background-color': '#66ADFF',
'target-arrow-color': '#66ADFF',
'text-background-shape': 'roundrectangle',
'text-border-color': '#000',
'text-wrap': 'wrap',
'text-valign': 'top',
'text-halign': 'center',
'text-background-padding':'5',
}
}
]
}
)
cy.layout(LAYOUTCONFIG).run()
cy.pan({
x: dom.clientWidth / 2,
y: dom.clientHeight / 2
})
}
const back = ()=>{
router.push(`/knowledge`);
}
const {
query: { spaceName },
} = useRouter();
useEffect(()=>{
spaceName && fetchGraphVis()
})
return (
<div className="p-4 h-full overflow-y-scroll relative px-2">
<div>
<Button onClick={back} icon={<RollbackOutlined />}> Back </Button>
</div>
<div className='h-full w-full' ref={myRef}></div>
</div>
);
}
export default GraphVis;

View File

@@ -1,123 +0,0 @@
import React, { useState, useEffect } from 'react';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Modal, Steps } from 'antd';
import SpaceCard from '@/components/knowledge/space-card';
import { File, ISpace, StepChangeParams, IStorage, SpaceConfig } from '@/types/knowledge';
import { apiInterceptors, getSpaceList, getSpaceConfig } from '@/client/api';
import { useTranslation } from 'react-i18next';
import DocUploadForm from '@/components/knowledge/doc-upload-form';
import SpaceForm from '@/components/knowledge/space-form';
import DocTypeForm from '@/components/knowledge/doc-type-form';
import Segmentation from '@/components/knowledge/segmentation';
import classNames from 'classnames';
const Knowledge = () => {
const [spaceList, setSpaceList] = useState<Array<ISpace> | null>([]);
const [isAddShow, setIsAddShow] = useState<boolean>(false);
const [activeStep, setActiveStep] = useState<number>(0);
const [spaceName, setSpaceName] = useState<string>('');
const [files, setFiles] = useState<Array<File>>([]);
const [docType, setDocType] = useState<string>('');
const [spaceConfig, setSpaceConfig] = useState<IStorage | null>(null);
const { t } = useTranslation();
const addKnowledgeSteps = [
{ title: t('Knowledge_Space_Config') },
{ title: t('Choose_a_Datasource_type') },
{ title: t('Upload') },
{ title: t('Segmentation') },
];
async function getSpaces() {
const [_, data] = await apiInterceptors(getSpaceList());
setSpaceList(data);
}
async function getSpaceConfigs() {
const [_, data] = await apiInterceptors(getSpaceConfig());
if (!data) return null;
setSpaceConfig(data.storage);
}
useEffect(() => {
getSpaces();
getSpaceConfigs();
}, []);
const handleStepChange = ({ label, spaceName, docType = '', files, pace = 1 }: StepChangeParams) => {
if (label === 'finish') {
setIsAddShow(false);
getSpaces();
setSpaceName('');
setDocType('');
} else if (label === 'forward') {
activeStep === 0 && getSpaces();
setActiveStep((step) => step + pace);
} else {
setActiveStep((step) => step - pace);
}
files && setFiles(files);
spaceName && setSpaceName(spaceName);
docType && setDocType(docType);
};
function onAddDoc(spaceName: string) {
const space = spaceList?.find((item) => item?.name === spaceName);
setSpaceName(spaceName);
setActiveStep(space?.domain_type === 'FinancialReport' ? 2 : 1);
setIsAddShow(true);
if (space?.domain_type === 'FinancialReport') {
setDocType('DOCUMENT');
}
}
return (
<div className="bg-[#FAFAFA] dark:bg-transparent w-full h-full">
<div className="page-body p-4 md:p-6 h-full overflow-auto">
<Button
type="primary"
className="flex items-center"
icon={<PlusOutlined />}
onClick={() => {
setIsAddShow(true);
}}
>
Create
</Button>
<div className="flex flex-wrap mt-4 gap-2 md:gap-4">
{spaceList?.map((space: ISpace) => (
<SpaceCard key={space.id} space={space} onAddDoc={onAddDoc} getSpaces={getSpaces} />
))}
</div>
</div>
<Modal
title="Add Knowledge"
centered
open={isAddShow}
destroyOnClose={true}
onCancel={() => {
setIsAddShow(false);
}}
width={1000}
afterClose={() => {
setActiveStep(0);
getSpaces();
}}
footer={null}
>
<Steps current={activeStep} items={addKnowledgeSteps} />
{activeStep === 0 && <SpaceForm handleStepChange={handleStepChange} spaceConfig={spaceConfig} />}
{activeStep === 1 && <DocTypeForm handleStepChange={handleStepChange} />}
<DocUploadForm
className={classNames({ hidden: activeStep !== 2 })}
spaceName={spaceName}
docType={docType}
handleStepChange={handleStepChange}
/>
{activeStep === 3 && <Segmentation spaceName={spaceName} docType={docType} uploadFiles={files} handleStepChange={handleStepChange} />}
</Modal>
</div>
);
};
export default Knowledge;

View File

@@ -1,62 +0,0 @@
import { apiInterceptors, getModelList } from '@/client/api';
import ModelCard from '@/components/model/model-card';
import ModelForm from '@/components/model/model-form';
import { IModelData } from '@/types/model';
import { Button, Modal } from 'antd';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
function Models() {
const { t } = useTranslation();
const [models, setModels] = useState<Array<IModelData>>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
async function getModels() {
const [, res] = await apiInterceptors(getModelList());
setModels(res ?? []);
}
useEffect(() => {
getModels();
}, []);
return (
<div className="p-4 md:p-6 overflow-y-auto">
<Button
className="mb-4"
type="primary"
onClick={() => {
setIsModalOpen(true);
}}
>
{t('create_model')}
</Button>
<div className="flex flex-wrap gap-2 md:gap-4">
{models.map((item) => (
<ModelCard info={item} key={item.model_name} />
))}
</div>
<Modal
width={800}
open={isModalOpen}
title={t('create_model')}
onCancel={() => {
setIsModalOpen(false);
}}
footer={null}
>
<ModelForm
onCancel={() => {
setIsModalOpen(false);
}}
onSuccess={() => {
setIsModalOpen(false);
getModels();
}}
/>
</Modal>
</div>
);
}
export default Models;

View File

@@ -1,181 +0,0 @@
import { useState, useEffect, useRef, Ref } from 'react';
import type { ColumnsType } from 'antd/es/table';
import type { FormInstance, MenuProps } from 'antd';
import { Menu, Table, Button, Tooltip, Modal } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import GroupsIcon from '@mui/icons-material/Groups';
import PersonIcon from '@mui/icons-material/Person';
import { useTranslation } from 'react-i18next';
import { addPrompt, apiInterceptors, getPromptList, postScenes, updatePrompt } from '@/client/api';
import { IPrompt } from '@/types/prompt';
import PromptForm from '@/components/prompt/prompt-form';
import { TFunction } from 'i18next';
const getItems = (t: TFunction) => [
{
label: t('Public') + ' Prompts',
key: 'common',
icon: <GroupsIcon />,
},
{
label: t('Private') + ' Prompts',
key: 'private',
icon: <PersonIcon />,
},
];
const getColumns = (t: TFunction, handleEdit: (prompt: IPrompt) => void): ColumnsType<IPrompt> => [
{
title: t('Prompt_Info_Name'),
dataIndex: 'prompt_name',
key: 'prompt_name',
},
{
title: t('Prompt_Info_Scene'),
dataIndex: 'chat_scene',
key: 'chat_scene',
},
{
title: t('Prompt_Info_Sub_Scene'),
dataIndex: 'sub_chat_scene',
key: 'sub_chat_scene',
},
{
title: t('Prompt_Info_Content'),
dataIndex: 'content',
key: 'content',
render: (content) => (
<Tooltip placement="topLeft" title={content}>
{content}
</Tooltip>
),
},
{
title: t('Operation'),
dataIndex: 'operate',
key: 'operate',
render: (_, record) => (
<Button
onClick={() => {
handleEdit(record);
}}
type="primary"
>
{t('Edit')}
</Button>
),
},
];
type FormType = Ref<FormInstance<any>> | undefined;
const Prompt = () => {
const { t } = useTranslation();
const [promptType, setPromptType] = useState<string>('common');
const [promptList, setPromptList] = useState<Array<IPrompt>>();
const [loading, setLoading] = useState<boolean>(false);
const [prompt, setPrompt] = useState<IPrompt>();
const [showModal, setShowModal] = useState<boolean>(false);
const [scenes, setScenes] = useState<Array<Record<string, string>>>();
const formRef = useRef<FormType>();
const getPrompts = async () => {
setLoading(true);
const body = {
prompt_type: promptType,
current: 1,
pageSize: 1000,
hideOnSinglePage: true,
showQuickJumper: true,
};
const [_, data] = await apiInterceptors(getPromptList(body));
setPromptList(data!);
setLoading(false);
};
const getScenes = async () => {
const [, res] = await apiInterceptors(postScenes());
setScenes(res?.map((scene) => ({ value: scene.chat_scene, label: scene.scene_name })));
};
const onFinish = async (newPrompt: IPrompt) => {
if (prompt) {
await apiInterceptors(updatePrompt({ ...newPrompt, prompt_type: promptType }));
} else {
await apiInterceptors(addPrompt({ ...newPrompt, prompt_type: promptType }));
}
getPrompts();
handleClose();
};
const handleEditBtn = (prompt: IPrompt) => {
setPrompt(prompt);
setShowModal(true);
};
const handleAddBtn = () => {
setShowModal(true);
setPrompt(undefined);
};
const handleClose = () => {
setShowModal(false);
};
const handleMenuChange: MenuProps['onClick'] = (e) => {
const type = e.key;
setPromptType(type);
};
useEffect(() => {
getPrompts();
}, [promptType]);
useEffect(() => {
getScenes();
}, []);
return (
<div>
<Menu onClick={handleMenuChange} selectedKeys={[promptType]} mode="horizontal" items={getItems(t)} />
<div className="px-6 py-4">
<div className="flex flex-row-reverse mb-4">
<Button className="flex items-center" onClick={handleAddBtn}>
<PlusOutlined />
{t('Add')} Prompts
</Button>
{promptType === 'common' && (
<Button className="mr-2 flex items-center" disabled>
<PlusOutlined />
{t('Add')} Prompts {t('template')}
</Button>
)}
</div>
<Table
columns={getColumns(t, handleEditBtn)}
dataSource={promptList}
loading={loading}
rowKey={(record) => record.prompt_name}
scroll={{ y: 600 }}
/>
</div>
<Modal
title={`${prompt ? t('Edit') : t('Add')} Prompts`}
destroyOnClose
open={showModal}
onCancel={handleClose}
cancelText={t('cancel')}
okText={t('submit')}
onOk={() => {
// @ts-ignore
formRef.current?.submit();
}}
>
<PromptForm scenes={scenes} ref={formRef as FormType} prompt={prompt} onFinish={onFinish} />
</Modal>
</div>
);
};
export default Prompt;

View File

@@ -1,6 +1,14 @@
import { File } from 'buffer';
import { Node } from 'reactflow';
export type FlowState = 'deployed' | 'developing' | 'initializing' | 'testing' | 'disabled' | 'running' | 'load_failed';
export type FlowState =
| 'deployed'
| 'developing'
| 'initializing'
| 'testing'
| 'disabled'
| 'running'
| 'load_failed';
export type IFlowUpdateParam = {
name: string;
@@ -13,6 +21,21 @@ export type IFlowUpdateParam = {
state?: FlowState;
};
export type IFlowRefreshParams = {
id: string;
type_name: string;
type_cls: string;
flow_type: 'resource' | 'operator';
refresh: {
name: string;
depends?: Array<{
name: string;
value: any;
has_value: boolean;
}>;
}[];
};
export type IFlow = {
dag_id: string;
gmt_created: string;
@@ -52,6 +75,25 @@ export type IFlowNodeParameter = {
options?: any;
value: any;
is_list?: boolean;
ui: IFlowNodeParameterUI;
};
export type IFlowNodeParameterUI = {
ui_type: string;
language: string;
file_types: string;
action: string;
attr: {
disabled: boolean;
[key: string]: any;
};
editor?: {
width: number;
height: number;
};
show_input?: boolean;
refresh?: boolean;
refresh_depends?: string[];
};
export type IFlowNodeInput = {
@@ -139,7 +181,29 @@ export type IFlowData = {
viewport: IFlowDataViewport;
};
export interface UpdateFLowAdminsParams {
export type IFlowExportParams = {
uid: string;
admins: string[];
}
export_type?: 'json' | 'dbgpts';
format?: 'json' | 'file';
file_name?: string;
user_name?: string;
sys_code?: string;
};
export type IFlowImportParams = {
file: File;
save_flow?: boolean;
};
export type IUploadFileRequestParams = {
files: Array<File>;
user_name?: string;
sys_code?: string;
};
export type IUploadFileResponse = {
file_name: string;
file_id: string;
bucket: string;
uri?: string;
};

View File

@@ -11,6 +11,12 @@ export const getUniqueNodeId = (nodeData: IFlowNode, nodes: Node[]) => {
return `${nodeData.id}_${count}`;
};
// function getUniqueNodeId will add '_${count}' to id, so we need to remove it when we want to get the original id
export const removeIndexFromNodeId = (id: string) => {
const indexPattern = /_\d+$/;
return id.replace(indexPattern, '');
};
// 驼峰转下划线,接口协议字段命名规范
export const mapHumpToUnderline = (flowData: IFlowData) => {
/**
@@ -98,3 +104,31 @@ export const checkFlowDataRequied = (flowData: IFlowData) => {
}
return result;
};
export const convertKeysToCamelCase = (obj: Record<string, any>): Record<string, any> => {
function toCamelCase(str: string): string {
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
}
function isObject(value: any): boolean {
return value && typeof value === 'object' && !Array.isArray(value);
}
function convert(obj: any): any {
if (Array.isArray(obj)) {
return obj.map((item) => convert(item));
} else if (isObject(obj)) {
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = toCamelCase(key);
newObj[newKey] = convert(obj[key]);
}
}
return newObj;
}
return obj;
}
return convert(obj);
};