mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-09-02 17:45:31 +00:00
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:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
);
|
||||
};
|
||||
|
@@ -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}`);
|
||||
};
|
||||
|
||||
// TODO:wait 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}`);
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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 }}
|
||||
|
@@ -61,6 +61,7 @@ function GPTCard({
|
||||
return icon;
|
||||
}, [icon]);
|
||||
|
||||
// TODO: 算子资源标签
|
||||
const tagNode = useMemo(() => {
|
||||
if (!tags || !tags.length) return null;
|
||||
return (
|
||||
|
@@ -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';
|
||||
|
||||
|
234
web/components/flow/add-nodes-sider.tsx
Normal file
234
web/components/flow/add-nodes-sider.tsx
Normal 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;
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
104
web/components/flow/canvas-modal/export-flow-modal.tsx
Normal file
104
web/components/flow/canvas-modal/export-flow-modal.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
113
web/components/flow/canvas-modal/import-flow-modal.tsx
Normal file
113
web/components/flow/canvas-modal/import-flow-modal.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
3
web/components/flow/canvas-modal/index.ts
Normal file
3
web/components/flow/canvas-modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './save-flow-modal';
|
||||
export * from './export-flow-modal';
|
||||
export * from './import-flow-modal';
|
203
web/components/flow/canvas-modal/save-flow-modal.tsx
Normal file
203
web/components/flow/canvas-modal/save-flow-modal.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
@@ -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" />
|
||||
|
@@ -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;
|
||||
|
16
web/components/flow/node-renderer/cascader.tsx
Normal file
16
web/components/flow/node-renderer/cascader.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
};
|
15
web/components/flow/node-renderer/checkbox.tsx
Normal file
15
web/components/flow/node-renderer/checkbox.tsx
Normal 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>
|
||||
)
|
||||
);
|
||||
};
|
62
web/components/flow/node-renderer/code-editor.tsx
Normal file
62
web/components/flow/node-renderer/code-editor.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/flow/node-renderer/date-picker.tsx
Normal file
9
web/components/flow/node-renderer/date-picker.tsx
Normal 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" />;
|
||||
};
|
14
web/components/flow/node-renderer/index.ts
Normal file
14
web/components/flow/node-renderer/index.ts
Normal 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';
|
22
web/components/flow/node-renderer/input.tsx
Normal file
22
web/components/flow/node-renderer/input.tsx
Normal 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 />;
|
||||
};
|
11
web/components/flow/node-renderer/password.tsx
Normal file
11
web/components/flow/node-renderer/password.tsx
Normal 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" />;
|
||||
};
|
13
web/components/flow/node-renderer/radio.tsx
Normal file
13
web/components/flow/node-renderer/radio.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/flow/node-renderer/select.tsx
Normal file
9
web/components/flow/node-renderer/select.tsx
Normal 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} />;
|
||||
};
|
16
web/components/flow/node-renderer/slider.tsx
Normal file
16
web/components/flow/node-renderer/slider.tsx
Normal 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} />)}
|
||||
</>
|
||||
)
|
||||
};
|
13
web/components/flow/node-renderer/textarea.tsx
Normal file
13
web/components/flow/node-renderer/textarea.tsx
Normal 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} />
|
||||
);
|
||||
};
|
23
web/components/flow/node-renderer/time-picker.tsx
Normal file
23
web/components/flow/node-renderer/time-picker.tsx
Normal 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" />;
|
||||
};
|
17
web/components/flow/node-renderer/tree-select.tsx
Normal file
17
web/components/flow/node-renderer/tree-select.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
90
web/components/flow/node-renderer/upload.tsx
Normal file
90
web/components/flow/node-renderer/upload.tsx
Normal 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>
|
||||
);
|
||||
};
|
9
web/components/flow/node-renderer/variables.tsx
Normal file
9
web/components/flow/node-renderer/variables.tsx
Normal 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 />;
|
||||
};
|
@@ -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')} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -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',
|
||||
|
@@ -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
19
web/locales/en/flow.ts
Normal 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',
|
||||
};
|
@@ -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,
|
||||
};
|
||||
|
||||
|
@@ -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: '内容复制为空',
|
||||
|
@@ -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: '请先获取团队知识库token,token获取',
|
||||
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: "请先获取团队知识库token,token获取",
|
||||
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
21
web/locales/zh/flow.ts
Normal 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: '否',
|
||||
};
|
@@ -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;
|
||||
|
@@ -28,7 +28,6 @@ const zeroWidthTriggerDefaultStyle: React.CSSProperties = {
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* 会话项
|
||||
*/
|
||||
const MenuItem: React.FC<{ item: any; refresh?: any; order: React.MutableRefObject<number>; historyLoading?: boolean }> = ({
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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;
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -174,7 +174,7 @@ function Database() {
|
||||
onDelete(item);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
{t('Delete_Btn')}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
@@ -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>
|
||||
);
|
||||
}
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
@@ -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;
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
Reference in New Issue
Block a user