From 036097243afd09e9f738b0dfe61e85f7b4baf597 Mon Sep 17 00:00:00 2001 From: papapatrick <109422393+Patrickill@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:05:58 +0800 Subject: [PATCH] perf: workflow&plugins json config import and export (#2592) --- .../web/components/common/Icon/constants.ts | 1 + .../common/Icon/icons/common/importLight.svg | 2 +- .../common/Icon/icons/configmap.svg | 4 + .../common/Icon/icons/file/uploadFile.svg | 5 +- .../web/components/common/MyMenu/index.tsx | 31 +-- packages/web/i18n/en/app.json | 4 + packages/web/i18n/en/common.json | 4 + packages/web/i18n/zh/app.json | 4 + packages/web/i18n/zh/common.json | 4 + .../components/WorkflowComponents/AppCard.tsx | 132 ++++++++++--- .../Flow/ImportSettings.tsx | 182 +++++++++++++++--- 11 files changed, 302 insertions(+), 71 deletions(-) create mode 100644 packages/web/components/common/Icon/icons/configmap.svg diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 8375960e9..022ad5fd1 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -5,6 +5,7 @@ export const iconPaths = { visible: () => import('./icons/visible.svg'), change: () => import('./icons/change.svg'), chatSend: () => import('./icons/chatSend.svg'), + configmap: () => import('./icons/configmap.svg'), closeSolid: () => import('./icons/closeSolid.svg'), collectionLight: () => import('./icons/collectionLight.svg'), collectionSolid: () => import('./icons/collectionSolid.svg'), diff --git a/packages/web/components/common/Icon/icons/common/importLight.svg b/packages/web/components/common/Icon/icons/common/importLight.svg index 8e3f5b4d6..b83f8efdd 100644 --- a/packages/web/components/common/Icon/icons/common/importLight.svg +++ b/packages/web/components/common/Icon/icons/common/importLight.svg @@ -1,4 +1,4 @@ - + + + + diff --git a/packages/web/components/common/Icon/icons/file/uploadFile.svg b/packages/web/components/common/Icon/icons/file/uploadFile.svg index 92b5b8728..44d2ea567 100644 --- a/packages/web/components/common/Icon/icons/file/uploadFile.svg +++ b/packages/web/components/common/Icon/icons/file/uploadFile.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + diff --git a/packages/web/components/common/MyMenu/index.tsx b/packages/web/components/common/MyMenu/index.tsx index 357284114..7ce9e207a 100644 --- a/packages/web/components/common/MyMenu/index.tsx +++ b/packages/web/components/common/MyMenu/index.tsx @@ -23,6 +23,7 @@ export type Props = { trigger?: 'hover' | 'click'; iconSize?: string; iconRadius?: string; + menuItemStyles?: MenuItemProps; placement?: PlacementWithLogical; menuList: { label?: string; @@ -45,7 +46,15 @@ const MyMenu = ({ Button, menuList, iconRadius, - placement = 'bottom-start' + placement = 'bottom-start', + menuItemStyles = { + borderRadius: 'sm', + py: 2, + px: 3, + display: 'flex', + alignItems: 'center', + fontSize: 'sm' + } }: Props) => { const typeMapStyle: Record = { primary: { @@ -75,14 +84,6 @@ const MyMenu = ({ } } }; - const menuItemStyles: MenuItemProps = { - borderRadius: 'sm', - py: 2, - px: 3, - display: 'flex', - alignItems: 'center', - fontSize: 'sm' - }; const { isPc } = useSystem(); const ref = useRef(null); @@ -167,7 +168,7 @@ const MyMenu = ({ { + onClick={(e) => { e.stopPropagation(); setIsOpen(false); child.onClick && child.onClick(); @@ -185,12 +186,16 @@ const MyMenu = ({ mr={3} /> )} - - + + {child.label} {child.description && ( - + {child.description} )} diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 7cf92e8bb..0a303ccd7 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -48,6 +48,7 @@ "export_config_successful": "Config copied, please check for important data", "export_configs": "Export Configs", "feedback_count": "User Feedback", + "file_recover": "The file will overwrite the current content", "file_upload": "file_upload", "file_upload_tip": "After it is enabled, you can upload documents/pictures. Documents are kept for 7 days and pictures for 15 days. Use of this feature may incur additional charges. To ensure the user experience, select an AI model with a large context length when using this function.", "go_to_chat": "To chat", @@ -56,6 +57,7 @@ "image_upload_tip": "Be sure to select a visual model that can handle the picture", "import_configs": "Import Configs", "import_configs_failed": "Failed to import configs, please ensure configs are valid!", + "import_configs_success": "Import successful", "interval": { "12_hours": "every 12 hours", "2_hours": "every 2 hours", @@ -86,6 +88,8 @@ }, "move_app": "Move app", "no": "no", + "not_json_file": "Please select a JSON file", + "or_drag_JSON": "Or drag in a JSON file", "paste_config": "Paste Config", "plugin_cost_per_times": "{{cost}}/per time", "plugin_dispatch": "Plugins", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 9332a4001..70d82483e 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -227,6 +227,7 @@ "confirm": { "Common Tip": "Operation confirmation" }, + "copy_to_clipboard": "copy to clipboard", "course": { "Read Course": "Read course" }, @@ -238,6 +239,7 @@ "too_many_request": "Too many requests, please try again later.", "unKnow": "An unexpected error occurred~" }, + "export_to_json": "Export to JSON", "failed": "fail", "folder": { "Drag Tip": "Drag me", @@ -257,6 +259,7 @@ "jsonEditor": { "Parse error": "JSON may be incorrect, please check carefully" }, + "json_config": "JSON configuration", "link": { "UnValid": "Invalid link" }, @@ -289,6 +292,7 @@ }, "undo_tip": "unde ctrl z", "undo_tip_mac": "undo ⌘ z ", + "upload_file": "Upload files", "zoomin_tip": "zoomIn ctrl -", "zoomin_tip_mac": "zoomIn ⌘ -", "zoomout_tip": "zoomOut ctrl +", diff --git a/packages/web/i18n/zh/app.json b/packages/web/i18n/zh/app.json index 31b1c73b8..22809318b 100644 --- a/packages/web/i18n/zh/app.json +++ b/packages/web/i18n/zh/app.json @@ -48,6 +48,7 @@ "export_config_successful": "已复制配置,自动过滤部分敏感信息,请注意检查是否仍有敏感数据", "export_configs": "导出配置", "feedback_count": "用户反馈", + "file_recover": "文件将覆盖当前内容", "file_upload": "文件上传", "file_upload_tip": "开启后,可以上传文档/图片。文档保留7天,图片保留15天。使用该功能可能产生较多额外费用。为保证使用体验,使用该功能时,请选择上下文长度较大的AI模型。", "go_to_chat": "去对话", @@ -56,6 +57,7 @@ "image_upload_tip": "请确保选择可处理图片的视觉模型", "import_configs": "导入配置", "import_configs_failed": "导入配置失败,请确保配置正常!", + "import_configs_success": "导入成功", "interval": { "12_hours": "每12小时", "2_hours": "每2小时", @@ -85,7 +87,9 @@ "unit": "号" }, "move_app": "移动应用", + "not_json_file": "请选择JSON文件", "paste_config": "粘贴配置", + "or_drag_JSON": "或拖入JSON文件", "plugin_cost_per_times": "{{cost}}/次", "plugin_dispatch": "插件调用", "plugin_dispatch_tip": "给模型附加额外的能力,具体调用哪些插件,将由模型自主决定。\n若选择了插件,知识库调用将自动作为一个特殊的插件。", diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index ad65373e0..3aafcdd3b 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -112,6 +112,7 @@ "common": { "Action": "操作", "Add": "添加", + "copy_to_clipboard": "复制到剪贴板", "Add New": "新增", "Add Success": "添加成功", "All": "全部", @@ -122,6 +123,7 @@ "Confirm": "确认", "Confirm Create": "确认创建", "Confirm Import": "确认导入", + "export_to_json": "导出为 JSON", "Confirm Move": "移动到这", "Confirm Update": "确认更新", "Confirm to leave the page": "确认离开该页面?", @@ -193,6 +195,7 @@ "Save Success": "保存成功", "Save_and_exit": "保存并退出", "Search": "搜索", + "json_config": "JSON 配置", "Select File Failed": "选择文件异常", "Select template": "选择模板", "Set Avatar": "点击设置头像", @@ -289,6 +292,7 @@ }, "undo_tip": "撤销 ctrl z", "undo_tip_mac": "撤销 ⌘ z ", + "upload_file": "上传文件", "zoomin_tip": "缩小 ctrl -", "zoomin_tip_mac": "缩小 ⌘ -", "zoomout_tip": "放大 ctrl +", diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx index 73a435610..912f0080f 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/AppCard.tsx @@ -14,6 +14,9 @@ import { useCopyData } from '@/web/common/hooks/useCopyData'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import MyTag from '@fastgpt/web/components/common/Tag/index'; import { publishStatusStyle } from '../constants'; +import MyPopover from '@fastgpt/web/components/common/MyPopover'; +import { fileDownload } from '@/web/common/file/utils'; +import { AppChatConfigType } from '@fastgpt/global/core/app/type'; const ImportSettings = dynamic(() => import('./Flow/ImportSettings')); @@ -26,32 +29,16 @@ const AppCard = ({ }) => { const { t } = useTranslation(); const { appT } = useI18n(); - const { copyData } = useCopyData(); const { feConfigs } = useSystemStore(); const { appDetail, onOpenInfoEdit, onOpenTeamTagModal, onDelApp, currentTab } = useContextSelector(AppContext, (v) => v); - const { historiesDefaultData, flowData2StoreDataAndCheck, onSaveWorkflow, isSaving } = - useContextSelector(WorkflowContext, (v) => v); + const { historiesDefaultData, onSaveWorkflow, isSaving } = useContextSelector( + WorkflowContext, + (v) => v + ); const { isOpen: isOpenImport, onOpen: onOpenImport, onClose: onCloseImport } = useDisclosure(); - const onExportWorkflow = useCallback(async () => { - const data = flowData2StoreDataAndCheck(); - if (data) { - copyData( - JSON.stringify( - { - nodes: filterSensitiveNodesData(data.nodes), - edges: data.edges, - chatConfig: appDetail.chatConfig - }, - null, - 2 - ), - appT('export_config_successful') - ); - } - }, [appDetail.chatConfig, appT, copyData, flowData2StoreDataAndCheck]); const InfoMenu = useCallback( ({ children }: { children: React.ReactNode }) => { @@ -84,9 +71,11 @@ const AppCard = ({ onClick: onOpenImport }, { - label: appT('export_configs'), - icon: 'export', - onClick: onExportWorkflow + label: ExportPopover({ + chatConfig: appDetail.chatConfig, + appName: appDetail.name + }), + onClick: () => {} } ] } @@ -124,6 +113,8 @@ const AppCard = ({ ); }, [ + appDetail.chatConfig, + appDetail.name, appDetail.permission.hasWritePer, appDetail.permission.isOwner, appT, @@ -131,7 +122,6 @@ const AppCard = ({ feConfigs?.show_team_chat, historiesDefaultData, onDelApp, - onExportWorkflow, onOpenImport, onOpenInfoEdit, onOpenTeamTagModal, @@ -192,4 +182,98 @@ const AppCard = ({ return Render; }; +function ExportPopover({ + chatConfig, + appName +}: { + chatConfig: AppChatConfigType; + appName: string; +}) { + const { t } = useTranslation(); + const { copyData } = useCopyData(); + const { flowData2StoreDataAndCheck } = useContextSelector(WorkflowContext, (v) => v); + const data = flowData2StoreDataAndCheck(); + const onExportWorkflow = useCallback(async () => { + const data = flowData2StoreDataAndCheck(); + if (data) { + copyData( + JSON.stringify( + { + nodes: filterSensitiveNodesData(data.nodes), + edges: data.edges, + chatConfig: chatConfig + }, + null, + 2 + ), + t('app:export_config_successful') + ); + } + }, [chatConfig, copyData, flowData2StoreDataAndCheck, t]); + + return ( + + + {t('app:export_configs')} + + } + > + {({ onClose }) => ( + + + + {t('common:common.copy_to_clipboard')} + + { + if (!data) return; + fileDownload({ + text: JSON.stringify( + { + nodes: filterSensitiveNodesData(data.nodes), + edges: data.edges, + chatConfig: chatConfig + }, + null, + 2 + ), + type: 'application/json;charset=utf-8', + filename: `${appName}.json` + }); + }} + > + + {t('common:common.export_to_json')} + + + )} + + ); +} + export default AppCard; diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx index 8dd3c41e3..72bb48217 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/ImportSettings.tsx @@ -1,11 +1,14 @@ -import React, { useState } from 'react'; -import { Textarea, Button, ModalBody, ModalFooter } from '@chakra-ui/react'; +import React, { DragEvent, useCallback, useMemo, useState } from 'react'; +import { Textarea, Button, ModalBody, ModalFooter, Flex, Box } from '@chakra-ui/react'; import MyModal from '@fastgpt/web/components/common/MyModal'; import { useToast } from '@fastgpt/web/hooks/useToast'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../context'; import { useI18n } from '@/web/context/I18n'; import { useTranslation } from 'next-i18next'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { useSelectFile } from '@/web/common/file/hooks/useSelectFile'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; type Props = { onClose: () => void; }; @@ -13,46 +16,161 @@ type Props = { const ImportSettings = ({ onClose }: Props) => { const { appT } = useI18n(); const { toast } = useToast(); + const { File, onOpen } = useSelectFile({ + fileType: 'json', + multiple: false + }); + const { isPc } = useSystem(); const initData = useContextSelector(WorkflowContext, (v) => v.initData); + const [isDragging, setIsDragging] = useState(false); const [value, setValue] = useState(''); const { t } = useTranslation(); + const handleDragEnter = useCallback((e: DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const handleDragLeave = useCallback((e: DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }, []); + const handleDrop = useCallback(async (e: DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + readJSONFile(file); + setIsDragging(false); + }, []); + + const readJSONFile = useCallback( + (file: File) => { + const reader = new FileReader(); + reader.onload = (e) => { + if (!file.name.endsWith('.json')) { + toast({ + title: t('app:not_json_file'), + status: 'error' + }); + return; + } + if (e.target) { + const res = JSON.parse(e.target.result as string); + setValue(JSON.stringify(res, null, 2)); + } + }; + reader.readAsText(file); + }, + [t, toast] + ); + + const onSelectFile = useCallback( + async (e: File[]) => { + const file = e[0]; + readJSONFile(file); + console.log(file); + }, + [readJSONFile] + ); return ( + + {appT('import_configs')} + + } > - - ); };